diff --git a/doc/upgrade.rst b/doc/upgrade.rst
index b711bc576..b14f2b8aa 100644
--- a/doc/upgrade.rst
+++ b/doc/upgrade.rst
@@ -209,7 +209,7 @@ Required changes to :file:`settings.py`
- Remove ``NEW_ADMIN_URL`` variable
-- Remove ``new_admin_url`` context processor
+- Remove ``new_admin_url`` and ``top_notifications`` context processors
- Remove ``ckeditor`` related apps from ``INSTALLED_APPS`` variable
diff --git a/frontend/src/api/webmail.js b/frontend/src/api/webmail.js
index 249686b9a..067b42a2b 100644
--- a/frontend/src/api/webmail.js
+++ b/frontend/src/api/webmail.js
@@ -94,8 +94,8 @@ export default {
}
return repository.post('/webmail/emails/flag/', body)
},
- sendEmail(body) {
- return repository.post('/webmail/emails/send/', body)
+ sendEmailFromComposeSession(uid, body) {
+ return repository.post(`/webmail/compose-sessions/${uid}/send/`, body)
},
getComposeSession(uid) {
return repository.get(`/webmail/compose-sessions/${uid}/`)
diff --git a/frontend/src/components/contacts/ContactList.vue b/frontend/src/components/contacts/ContactList.vue
index 9ec7f9150..544f3efa1 100644
--- a/frontend/src/components/contacts/ContactList.vue
+++ b/frontend/src/components/contacts/ContactList.vue
@@ -46,6 +46,12 @@
+
+
+ {{ item.display_name }}
+
+ {{ item.fist_name }} {{ item.last_name }}
+
{{ displayEmails(item.emails) }}
diff --git a/frontend/src/components/webmail/AttachmentsDialog.vue b/frontend/src/components/webmail/AttachmentsDialog.vue
index 01b033d0c..55457bbd0 100644
--- a/frontend/src/components/webmail/AttachmentsDialog.vue
+++ b/frontend/src/components/webmail/AttachmentsDialog.vue
@@ -90,7 +90,7 @@ const removeAttachment = async (attachment) => {
attachments.value.splice(index, 1)
}
-api.getUploadedAttachments(props.sessionUid).then((resp) => {
- attachments.value = resp.data
+api.getComposeSession(props.sessionUid).then((resp) => {
+ attachments.value = resp.data.attachments
})
diff --git a/frontend/src/components/webmail/ComposeEmailForm.vue b/frontend/src/components/webmail/ComposeEmailForm.vue
index 73d74b29f..2324acca9 100644
--- a/frontend/src/components/webmail/ComposeEmailForm.vue
+++ b/frontend/src/components/webmail/ComposeEmailForm.vue
@@ -263,7 +263,7 @@ const submit = async () => {
body.bcc = bcc
}
try {
- await api.sendEmail(body)
+ await api.sendEmailFromComposeSession(route.query.uid, body)
router.push({ name: 'MailboxView' })
displayNotification({ msg: $gettext('Email sent') })
} catch (error) {
diff --git a/frontend/src/layouts/admin/AdminLayout.vue b/frontend/src/layouts/admin/AdminLayout.vue
index c03c9cd8f..1e7f1f922 100644
--- a/frontend/src/layouts/admin/AdminLayout.vue
+++ b/frontend/src/layouts/admin/AdminLayout.vue
@@ -134,7 +134,7 @@ onMounted(() => {
capabilitiesApi.getCapabilities().then((response) => {
isRspamdVisible.value = 'rspamd' in response.data.capabilities
if (isRspamdVisible.value) {
- rspamd_url.value =
+ rspamdUrl.value =
response.data.capabilities.rspamd.location || '/rspamd'
}
})
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 7320bffea..2e84f77c0 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -34,7 +34,7 @@ export default defineConfig({
}),
],
base: '/',
- define: { 'process.env': {}, __VUE_OPTIONS_API__: false },
+ define: { 'process.env': {} },
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
diff --git a/modoboa/admin/api/v1/serializers.py b/modoboa/admin/api/v1/serializers.py
index a7b0932b3..9b2a22ed7 100644
--- a/modoboa/admin/api/v1/serializers.py
+++ b/modoboa/admin/api/v1/serializers.py
@@ -1,7 +1,5 @@
"""Admin serializers."""
-from typing import List
-
from django.contrib.auth import password_validation
from django.core.exceptions import ValidationError
from django.shortcuts import get_object_or_404
@@ -277,7 +275,7 @@ class AccountSerializer(serializers.ModelSerializer):
"""Return role."""
return account.role
- def get_domains(self, account) -> List[str]:
+ def get_domains(self, account) -> list[str]:
"""Return domains administered by this account."""
if account.role not in ["DomainAdmins", "Resellers"]:
return []
diff --git a/modoboa/admin/api/v2/serializers.py b/modoboa/admin/api/v2/serializers.py
index b11fbda5d..f33c19e5f 100644
--- a/modoboa/admin/api/v2/serializers.py
+++ b/modoboa/admin/api/v2/serializers.py
@@ -2,7 +2,6 @@
import ipaddress
import os
-from typing import List
from django.conf import settings
from django.core.exceptions import ValidationError
@@ -388,7 +387,7 @@ class AccountMeSerializer(v1_serializers.AccountSerializer):
class Meta(v1_serializers.AccountSerializer.Meta):
fields = v1_serializers.AccountSerializer.Meta.fields + ("permissions",)
- def get_permissions(self, user) -> List[str]:
+ def get_permissions(self, user) -> list[str]:
"""Return available permissions for given user."""
permissions = (
user.user_permissions.all() | Permission.objects.filter(group__user=user)
@@ -412,7 +411,7 @@ class AccountSerializer(v1_serializers.AccountSerializer):
return
self.fields["resources"] = serializers.SerializerMethodField()
- def get_resources(self, account) -> List[AccountResourceSerializer]:
+ def get_resources(self, account) -> list[AccountResourceSerializer]:
if account.role == "SimpleUsers":
return []
resources = []
diff --git a/modoboa/admin/models/base.py b/modoboa/admin/models/base.py
index 06b71029b..cd4d4ef5a 100644
--- a/modoboa/admin/models/base.py
+++ b/modoboa/admin/models/base.py
@@ -48,7 +48,7 @@ class AdminObject(models.Model):
def from_db(cls, db, field_names, values):
"""Store loaded values."""
instance = super().from_db(db, field_names, values)
- instance._loaded_values = dict(zip(field_names, values))
+ instance._loaded_values = dict(zip(field_names, values, strict=False))
return instance
@property
diff --git a/modoboa/autoreply/models.py b/modoboa/autoreply/models.py
index 8a77cd753..10070bc7f 100644
--- a/modoboa/autoreply/models.py
+++ b/modoboa/autoreply/models.py
@@ -56,7 +56,7 @@ class ARmessage(models.Model):
)
content = self.content % context
name = "autoreply"
- action = [("vacation", ":subject", self.subject, ":days", days, content)]
+ action = [("vacation", ":subject", self.subject, ":days", str(days), content)]
if not fset.getfilter(name):
fset.addfilter(name, condition, action)
else:
diff --git a/modoboa/contacts/importer/__init__.py b/modoboa/contacts/importer/__init__.py
index 76b391812..6064ed4b7 100644
--- a/modoboa/contacts/importer/__init__.py
+++ b/modoboa/contacts/importer/__init__.py
@@ -1,5 +1,4 @@
import csv
-from typing import Optional
from modoboa.contacts.importer.backends.outlook import OutlookBackend
@@ -29,7 +28,7 @@ def import_csv_file(
backend_name: str,
csv_filename: str,
delimiter: str,
- carddav_password: Optional[str] = None,
+ carddav_password: str | None = None,
):
with open(csv_filename) as fp:
backend, rows = get_import_backend(fp, delimiter, backend_name)
diff --git a/modoboa/core/extensions.py b/modoboa/core/extensions.py
index 55551fc49..34f0baa7c 100644
--- a/modoboa/core/extensions.py
+++ b/modoboa/core/extensions.py
@@ -1,7 +1,5 @@
"""Extension management."""
-from typing import Dict, Optional
-
from django.conf import settings
from django.urls import include
from django.urls import re_path
@@ -22,8 +20,8 @@ class ModoExtension:
description: str = ""
needs_media: bool = False
always_active: bool = False
- url: Optional[str] = None
- topredirection_url: Optional[str] = None
+ url: str | None = None
+ topredirection_url: str | None = None
def get_available_apps(self) -> list:
return []
@@ -59,7 +57,7 @@ class ExtensionsPool:
"""The extensions manager"""
def __init__(self) -> None:
- self.extensions: Dict[str, Dict] = {}
+ self.extensions: dict[str, dict] = {}
def register_extension(self, ext: ModoExtension, show: bool = True) -> None:
"""Register an extension.
@@ -69,7 +67,7 @@ class ExtensionsPool:
"""
self.extensions[ext.name] = {"cls": ext, "show": show}
- def get_extension(self, name: str) -> Optional[ModoExtension]:
+ def get_extension(self, name: str) -> ModoExtension | None:
"""Retrieve the current instance of an extension."""
if name not in self.extensions:
return None
diff --git a/modoboa/core/models.py b/modoboa/core/models.py
index 351fbb904..129e0f0d5 100644
--- a/modoboa/core/models.py
+++ b/modoboa/core/models.py
@@ -2,7 +2,6 @@
import re
from email.header import Header
-from typing import Optional
from django.core.serializers.json import DjangoJSONEncoder
from django.conf import settings
@@ -132,7 +131,7 @@ class User(AbstractUser):
raw_value = smart_bytes(raw_value)
return get_password_hasher(scheme.upper())().encrypt(raw_value)
- def set_password(self, raw_value: str, curvalue: Optional[str] = None) -> None:
+ def set_password(self, raw_value: str, curvalue: str | None = None) -> None:
"""Password update.
Update the current mailbox's password with the given clear
diff --git a/modoboa/imap_migration/__init__.py b/modoboa/imap_migration/__init__.py
index 6b3d80114..e944787c5 100644
--- a/modoboa/imap_migration/__init__.py
+++ b/modoboa/imap_migration/__init__.py
@@ -1,12 +1,3 @@
"""A plugin to migrate mailboxes using IMAP."""
-from pkg_resources import get_distribution, DistributionNotFound
-
-
-try:
- __version__ = get_distribution(__name__).version
-except DistributionNotFound:
- # package is not installed
- pass
-
default_app_config = "modoboa.imap_migration.apps.IMAPMigrationConfig"
diff --git a/modoboa/lib/exceptions.py b/modoboa/lib/exceptions.py
index 1ddfb8ecd..2de2ae340 100644
--- a/modoboa/lib/exceptions.py
+++ b/modoboa/lib/exceptions.py
@@ -4,8 +4,6 @@
"""
-from typing import Optional
-
from django.utils.translation import gettext as _
@@ -14,7 +12,7 @@ class ModoboaException(Exception):
Base class for Modoboa custom exceptions.
"""
- http_code: Optional[int] = None
+ http_code: int | None = None
def __init__(self, *args, **kwargs):
if "http_code" in kwargs:
diff --git a/modoboa/limits/tests/test_user_limits.py b/modoboa/limits/tests/test_user_limits.py
index 6d14df96c..ccd144059 100644
--- a/modoboa/limits/tests/test_user_limits.py
+++ b/modoboa/limits/tests/test_user_limits.py
@@ -1,7 +1,5 @@
"""Test cases for the limits extension."""
-from typing import Optional
-
from django.urls import reverse
from modoboa.admin.factories import populate_database
@@ -76,7 +74,7 @@ class ResourceTestCase(lib_tests.ModoAPITestCase):
self.assertEqual(response.status_code, status)
return response
- def _create_alias(self, email, rcpt: Optional[list] = None, status=201):
+ def _create_alias(self, email, rcpt: list | None = None, status=201):
if rcpt is None:
rcpt = ["user@test.com"]
values = {"address": email, "recipients": rcpt, "enabled": True}
diff --git a/modoboa/parameters/tools.py b/modoboa/parameters/tools.py
index 44ebed708..6ad51bef1 100644
--- a/modoboa/parameters/tools.py
+++ b/modoboa/parameters/tools.py
@@ -1,7 +1,6 @@
"""Parameters management."""
import copy
-from typing import Optional
from rest_framework.fields import empty
@@ -102,7 +101,7 @@ class Registry:
raise NotDefined(app)
return self._registry2[level][app]["serializer_class"]
- def get_structure(self, level: str, for_app: Optional[str] = None) -> list:
+ def get_structure(self, level: str, for_app: str | None = None) -> list:
"""Return fields structure."""
result = []
for app, fields in list(self._registry2[level].items()):
@@ -141,7 +140,7 @@ class Registry:
result.append(item)
return result
- def exists(self, level: str, app: str, parameter: Optional[str] = None) -> bool:
+ def exists(self, level: str, app: str, parameter: str | None = None) -> bool:
"""Check if parameter exists."""
parameters = self._registry2[level]
result = app in parameters
@@ -193,7 +192,7 @@ class Manager:
return self._parameters[app].get(parameter, default)
return default
- def get_values(self, app: Optional[str] = None):
+ def get_values(self, app: str | None = None):
"""Return all values for the given level/app."""
if app is None:
app = guess_extension_name()
@@ -225,7 +224,7 @@ class Manager:
)
self._parameters[app][parameter] = value
- def set_values(self, values: dict, app: Optional[str] = None) -> None:
+ def set_values(self, values: dict, app: str | None = None) -> None:
"""Set/update values for the given app."""
if app is None:
app = guess_extension_name()
diff --git a/modoboa/rspamd/admin.py b/modoboa/rspamd/admin.py
index 13be29d96..846f6b406 100644
--- a/modoboa/rspamd/admin.py
+++ b/modoboa/rspamd/admin.py
@@ -1,6 +1 @@
-# -*- coding: utf-8 -*-
-from __future__ import unicode_literals
-
-from django.contrib import admin
-
# Register your models here.
diff --git a/modoboa/sievefilters/__init__.py b/modoboa/sievefilters/__init__.py
index a48e9b877..82d26c218 100644
--- a/modoboa/sievefilters/__init__.py
+++ b/modoboa/sievefilters/__init__.py
@@ -1,12 +1,3 @@
"""The sieve filters editor of Modoboa."""
-from pkg_resources import get_distribution, DistributionNotFound
-
-
-try:
- __version__ = get_distribution(__name__).version
-except DistributionNotFound:
- # package is not installed
- pass
-
-default_app_config = "modoboa_sievefilters.apps.SieveFiltersConfig"
+default_app_config = "modoboa.sievefilters.apps.SieveFiltersConfig"
diff --git a/modoboa/sievefilters/api/v2/serializers.py b/modoboa/sievefilters/api/v2/serializers.py
index b101b2749..2575fef81 100644
--- a/modoboa/sievefilters/api/v2/serializers.py
+++ b/modoboa/sievefilters/api/v2/serializers.py
@@ -1,7 +1,5 @@
"""Sievefilters serializers."""
-from typing import List, Tuple
-
from sievelib import commands
from sievelib.factory import Filter
@@ -61,7 +59,7 @@ class FilterSerializer(serializers.Serializer):
actions = ActionSerializer(many=True)
@staticmethod
- def from_filters(filters: List[Filter]) -> "FilterSerializer":
+ def from_filters(filters: list[Filter]) -> "FilterSerializer":
result = []
for fobj in filters:
if fobj["name"] == "autoreply":
@@ -117,9 +115,9 @@ class FilterSerializer(serializers.Serializer):
result.append(item)
return FilterSerializer(result, many=True)
- def to_filter(self) -> Tuple[str, List, List]:
+ def to_filter(self) -> tuple[str, list, list]:
"""Convert serializer data to filter representation."""
- conditions: List[Tuple] = []
+ conditions: list[tuple] = []
actions = []
match_type = self.validated_data["match_type"]
if match_type == "all":
diff --git a/modoboa/sievefilters/api/v2/vloaders.py b/modoboa/sievefilters/api/v2/vloaders.py
index d8b9787b3..dffdb2aa4 100644
--- a/modoboa/sievefilters/api/v2/vloaders.py
+++ b/modoboa/sievefilters/api/v2/vloaders.py
@@ -1,6 +1,6 @@
"""Value loading functions."""
-from typing import List, Optional, TypedDict
+from typing import TypedDict
from modoboa.webmail.lib import imaputils
@@ -13,9 +13,9 @@ class UserMailbox(TypedDict):
def __build_folders_list(
- folders: list, user, imapc: imaputils.IMAPconnector, parentmb: Optional[str] = None
-) -> List[UserMailbox]:
- ret: List[UserMailbox] = []
+ folders: list, user, imapc: imaputils.IMAPconnector, parentmb: str | None = None
+) -> list[UserMailbox]:
+ ret: list[UserMailbox] = []
for fd in folders:
value = fd["path"] if "path" in fd else fd["name"]
if parentmb:
@@ -33,7 +33,7 @@ def __build_folders_list(
return ret
-def user_mailboxes(request) -> List[UserMailbox]:
+def user_mailboxes(request) -> list[UserMailbox]:
"""Retrieve list of available mailboxes for given user."""
with imaputils.get_imapconnector(request, with_namespaces=False) as mbc:
ret = mbc.getmboxes(request.user)
diff --git a/modoboa/sievefilters/lib.py b/modoboa/sievefilters/lib.py
index 3ee538cf8..fa12b776c 100644
--- a/modoboa/sievefilters/lib.py
+++ b/modoboa/sievefilters/lib.py
@@ -1,7 +1,5 @@
"""Internal tools."""
-from typing import Optional, Tuple, Union
-
from sievelib.factory import FiltersSet
from sievelib import managesieve
from sievelib.parser import Parser
@@ -27,7 +25,7 @@ class SieveClient:
msc: managesieve.Client
- def __init__(self, user: Optional[str] = None, password: Optional[str] = None):
+ def __init__(self, user: str | None = None, password: str | None = None):
if user and password:
try:
ret, msg = self.login(user, password)
@@ -36,9 +34,9 @@ class SieveClient:
if not ret:
raise SieveClientError(msg)
- def login(self, user: str, password: str) -> Tuple[bool, Union[str, None]]:
+ def login(self, user: str, password: str) -> tuple[bool, str | None]:
conf = dict(param_tools.get_global_parameters("sievefilters"))
- self.msc = managesieve.Client(conf["server"], conf["port"], debug=True)
+ self.msc = managesieve.Client(conf["server"], conf["port"], debug=False)
try:
ret = self.msc.connect(
user, password, starttls=conf["starttls"], authmech="OAUTHBEARER"
@@ -59,7 +57,7 @@ class SieveClient:
def listscripts(self):
return self.msc.listscripts()
- def getscript(self, name: str, format: str = "raw") -> Union[FiltersSet, str, None]:
+ def getscript(self, name: str, format: str = "raw") -> FiltersSet | str | None:
content = self.msc.getscript(name)
if content is None:
raise SieveClientError(self.msc.errmsg.decode())
diff --git a/modoboa/webmail/lib/attachments.py b/modoboa/webmail/lib/attachments.py
index 53ca33e93..391139762 100644
--- a/modoboa/webmail/lib/attachments.py
+++ b/modoboa/webmail/lib/attachments.py
@@ -3,7 +3,6 @@ from email.mime.base import MIMEBase
import json
import os
from tempfile import NamedTemporaryFile
-from typing import Optional
import uuid
import six
@@ -81,7 +80,7 @@ def save_attachment(request, session_uid: str, f) -> dict:
fp = NamedTemporaryFile(dir=get_storage_path(""), delete=False)
except Exception as e:
raise InternalError(str(e)) from None
- if isinstance(f, (six.binary_type, six.text_type)):
+ if isinstance(f, six.binary_type | six.text_type):
fp.write(smart_bytes(f))
else:
for chunk in f.chunks():
@@ -100,7 +99,7 @@ def save_attachment(request, session_uid: str, f) -> dict:
return attachment
-def remove_attachment(request, session_uid: str, name: str) -> Optional[str]:
+def remove_attachment(request, session_uid: str, name: str) -> str | None:
manager = ComposeSessionManager(request.user.username)
session = manager.get_content(session_uid)
for att in session["attachments"]:
diff --git a/modoboa/webmail/lib/imaputils.py b/modoboa/webmail/lib/imaputils.py
index 8b36a64b3..706e6ebc0 100644
--- a/modoboa/webmail/lib/imaputils.py
+++ b/modoboa/webmail/lib/imaputils.py
@@ -5,7 +5,6 @@ import imaplib
import re
import ssl
import time
-from typing import Optional
from django.conf import settings
from django.utils.translation import gettext as _
@@ -108,7 +107,7 @@ class BodyStructure:
self.attachments += [params]
def load_from_definition(
- self, definition: list, multisubtype: Optional[str] = None
+ self, definition: list, multisubtype: str | None = None
) -> None:
for mp in definition:
if isinstance(mp, list):
@@ -148,11 +147,11 @@ class IMAPconnector:
unseen_pattern = re.compile(r"[^\(]+\(UNSEEN (\d+)\)")
def __init__(self, user: str, password: str, with_namespaces: bool = True) -> None:
- self.__hdelimiter: Optional[str] = None
+ self.__hdelimiter: str | None = None
self.__ns_prefixes: dict = {}
self.quota_usage: int = -1
- self.quota_limit: Optional[int] = None
- self.quota_current: Optional[int] = None
+ self.quota_limit: int | None = None
+ self.quota_current: int | None = None
self.criterions: list = []
self.conf = dict(param_tools.get_global_parameters("webmail"))
self.address = self.conf["imap_server"]
@@ -170,7 +169,7 @@ class IMAPconnector:
def __exit__(self, *args):
self.logout()
- def _cmd(self, name: str, *args, **kwargs) -> Optional[list]:
+ def _cmd(self, name: str, *args, **kwargs) -> list | None:
"""IMAP command wrapper.
To simplify errors handling, this wrapper calls the
@@ -240,7 +239,8 @@ class IMAPconnector:
except (OSError, imaplib.IMAP4.error, ssl.SSLError) as error:
raise ImapError(_(f"Connection to IMAP server failed: {error}")) from None
- if not settings.WEBMAIL_DEV_MODE:
+ dev_mode = getattr(settings, "WEBMAIL_DEV_MODE", False)
+ if not dev_mode:
token = oauth2.build_oauthbearer_string(user, rawtoken)
data = self._cmd("AUTHENTICATE", b"OAUTHBEARER", token)
else:
@@ -600,7 +600,7 @@ class IMAPconnector:
self.select_mailbox(mbox, False)
self._cmd("EXPUNGE")
- def create_folder(self, name: str, parent: Optional[str] = None) -> bool:
+ def create_folder(self, name: str, parent: str | None = None) -> bool:
if parent is not None:
name = f"{parent}{self.hdelimiter}{name}"
typ, data = self.m.create(self._encode_mbox_name(name))
@@ -675,7 +675,7 @@ class IMAPconnector:
return attdef, data[int(uid)][f"BODY[{partnum}]"]
def fetch(
- self, start: int, stop: Optional[int] = None, mbox: Optional[str] = None
+ self, start: int, stop: int | None = None, mbox: str | None = None
) -> list:
"""Retrieve information about messages from the server.
@@ -748,7 +748,7 @@ class IMAPconnector:
return data[int(mailid)]
-def separate_mailbox(fullname: str, sep: str = ".") -> tuple[str, Optional[str]]:
+def separate_mailbox(fullname: str, sep: str = ".") -> tuple[str, str | None]:
"""Split a mailbox name.
If a separator is found in ``fullname``, this function returns the
diff --git a/modoboa/webmail/lib/sendmail.py b/modoboa/webmail/lib/sendmail.py
index a5b6b8cd9..bc178b38e 100644
--- a/modoboa/webmail/lib/sendmail.py
+++ b/modoboa/webmail/lib/sendmail.py
@@ -3,7 +3,6 @@ from email.mime.image import MIMEImage
import os
import pkg_resources
import smtplib
-from typing import Optional, Tuple
from urllib.parse import unquote, urlparse
import lxml
@@ -20,7 +19,7 @@ from modoboa.webmail.lib.utils import html2plaintext
from . import get_imapconnector
-def make_body_images_inline(body: str) -> Tuple[str, list]:
+def make_body_images_inline(body: str) -> tuple[str, list]:
"""Look for images inside the body and make them inline.
Before sending a message in HTML format, it is necessary to find
@@ -117,9 +116,7 @@ def create_message(user: core_models.User, attributes: dict, attachments: list):
return msg
-def send_mail(
- request, attributes: dict, attachments: list
-) -> Tuple[bool, Optional[str]]:
+def send_mail(request, attributes: dict, attachments: list) -> tuple[bool, str | None]:
"""
Send a new email.