2022-11-23 13:51:08 +01:00
|
|
|
import csv
|
|
|
|
from io import StringIO
|
2021-12-14 15:00:27 +01:00
|
|
|
import re
|
2024-10-08 12:48:53 +02:00
|
|
|
from dataclasses import dataclass
|
2022-04-28 14:43:24 +02:00
|
|
|
from typing import Optional, Tuple
|
2020-04-04 15:24:27 +02:00
|
|
|
|
2021-09-17 17:42:16 +02:00
|
|
|
from email_validator import validate_email, EmailNotValidError
|
2021-01-11 15:45:41 +01:00
|
|
|
from sqlalchemy.exc import IntegrityError, DataError
|
2022-11-23 13:51:08 +01:00
|
|
|
from flask import make_response
|
2020-05-07 22:27:27 +02:00
|
|
|
|
2024-10-14 12:45:00 +02:00
|
|
|
from app.alias_audit_log_utils import AliasAuditLogAction, emit_alias_audit_log
|
2022-04-28 14:43:24 +02:00
|
|
|
from app.config import (
|
|
|
|
BOUNCE_PREFIX_FOR_REPLY_PHASE,
|
|
|
|
BOUNCE_PREFIX,
|
|
|
|
BOUNCE_SUFFIX,
|
|
|
|
VERP_PREFIX,
|
|
|
|
)
|
2021-10-12 14:36:47 +02:00
|
|
|
from app.db import Session
|
2020-04-04 15:24:27 +02:00
|
|
|
from app.email_utils import (
|
|
|
|
get_email_domain_part,
|
|
|
|
send_cannot_create_directory_alias,
|
2020-10-15 16:21:31 +02:00
|
|
|
can_create_directory_for_address,
|
2020-12-07 10:55:13 +01:00
|
|
|
send_cannot_create_directory_alias_disabled,
|
2021-09-17 17:42:16 +02:00
|
|
|
get_email_local_part,
|
2022-04-28 14:43:24 +02:00
|
|
|
send_cannot_create_domain_alias,
|
2023-08-31 13:42:44 +02:00
|
|
|
send_email,
|
|
|
|
render,
|
2024-10-08 12:48:53 +02:00
|
|
|
sl_formataddr,
|
2020-04-04 15:24:27 +02:00
|
|
|
)
|
2020-05-23 19:45:26 +02:00
|
|
|
from app.errors import AliasInTrashError
|
2024-05-23 10:27:08 +02:00
|
|
|
from app.events.event_dispatcher import EventDispatcher
|
2024-06-07 15:36:18 +02:00
|
|
|
from app.events.generated.event_pb2 import (
|
|
|
|
AliasDeleted,
|
|
|
|
AliasStatusChanged,
|
|
|
|
EventContent,
|
2024-10-04 15:17:59 +02:00
|
|
|
AliasCreated,
|
2024-06-07 15:36:18 +02:00
|
|
|
)
|
2020-04-04 15:24:27 +02:00
|
|
|
from app.log import LOG
|
|
|
|
from app.models import (
|
|
|
|
Alias,
|
2024-07-08 16:39:18 +02:00
|
|
|
AliasDeleteReason,
|
2020-04-04 15:24:27 +02:00
|
|
|
CustomDomain,
|
|
|
|
Directory,
|
|
|
|
User,
|
|
|
|
DeletedAlias,
|
2020-05-23 11:49:34 +02:00
|
|
|
DomainDeletedAlias,
|
2020-06-05 22:30:32 +02:00
|
|
|
AliasMailbox,
|
2020-08-30 19:56:45 +02:00
|
|
|
Mailbox,
|
|
|
|
EmailLog,
|
|
|
|
Contact,
|
2022-04-28 14:43:24 +02:00
|
|
|
AutoCreateRule,
|
2023-08-31 13:42:44 +02:00
|
|
|
AliasUsedOn,
|
|
|
|
ClientUser,
|
2020-04-04 15:24:27 +02:00
|
|
|
)
|
2021-12-14 15:00:27 +01:00
|
|
|
from app.regex_utils import regex_match
|
2020-04-04 15:24:27 +02:00
|
|
|
|
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
def get_user_if_alias_would_auto_create(
|
|
|
|
address: str, notify_user: bool = False
|
|
|
|
) -> Optional[User]:
|
|
|
|
banned_prefix = f"{VERP_PREFIX}."
|
|
|
|
if address.startswith(banned_prefix):
|
|
|
|
LOG.w("alias %s can't start with %s", address, banned_prefix)
|
|
|
|
return None
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Prevent addresses with unicode characters (🤯) in them for now.
|
|
|
|
validate_email(address, check_deliverability=False, allow_smtputf8=False)
|
|
|
|
except EmailNotValidError:
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.i(f"Not creating alias for {address} because email is invalid")
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
|
2022-04-28 18:43:10 +02:00
|
|
|
domain_and_rule = check_if_alias_can_be_auto_created_for_custom_domain(
|
2022-04-28 14:43:24 +02:00
|
|
|
address, notify_user=notify_user
|
|
|
|
)
|
2023-06-01 17:33:58 +02:00
|
|
|
if DomainDeletedAlias.get_by(email=address):
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.i(
|
|
|
|
f"Not creating alias for {address} because it was previously deleted for this domain"
|
|
|
|
)
|
2023-06-01 17:33:58 +02:00
|
|
|
return None
|
2022-04-28 18:43:10 +02:00
|
|
|
if domain_and_rule:
|
|
|
|
return domain_and_rule[0].user
|
2022-04-28 14:43:24 +02:00
|
|
|
directory = check_if_alias_can_be_auto_created_for_a_directory(
|
|
|
|
address, notify_user=notify_user
|
|
|
|
)
|
|
|
|
if directory:
|
|
|
|
return directory.user
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def check_if_alias_can_be_auto_created_for_custom_domain(
|
|
|
|
address: str, notify_user: bool = True
|
|
|
|
) -> Optional[Tuple[CustomDomain, Optional[AutoCreateRule]]]:
|
|
|
|
"""
|
|
|
|
Check if this address would generate an auto created alias.
|
|
|
|
If that's the case return the domain that would create it and the rule that triggered it.
|
|
|
|
If there's no rule it's a catchall creation
|
|
|
|
"""
|
|
|
|
alias_domain = get_email_domain_part(address)
|
|
|
|
custom_domain: CustomDomain = CustomDomain.get_by(domain=alias_domain)
|
|
|
|
|
|
|
|
if not custom_domain:
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.i(
|
|
|
|
f"Cannot auto-create custom domain alias for {address} because there's no custom domain for {alias_domain}"
|
|
|
|
)
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
user: User = custom_domain.user
|
|
|
|
if user.disabled:
|
|
|
|
LOG.i("Disabled user %s can't create new alias via custom domain", user)
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not user.can_create_new_alias():
|
2022-08-09 10:01:55 +02:00
|
|
|
LOG.d(f"{user} can't create new custom-domain alias {address}")
|
2022-04-28 14:43:24 +02:00
|
|
|
if notify_user:
|
|
|
|
send_cannot_create_domain_alias(custom_domain.user, address, alias_domain)
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not custom_domain.catch_all:
|
|
|
|
if len(custom_domain.auto_create_rules) == 0:
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.i(
|
|
|
|
f"Cannot create alias {address} for domain {custom_domain} because it has no catch-all and no rules"
|
|
|
|
)
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
local = get_email_local_part(address)
|
|
|
|
|
|
|
|
for rule in custom_domain.auto_create_rules:
|
|
|
|
if regex_match(rule.regex, local):
|
|
|
|
LOG.d(
|
|
|
|
"%s passes %s on %s",
|
|
|
|
address,
|
|
|
|
rule.regex,
|
|
|
|
custom_domain,
|
|
|
|
)
|
|
|
|
return custom_domain, rule
|
|
|
|
else: # no rule passes
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.d(f"No rule matches auto-create {address} for domain {custom_domain}")
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
LOG.d("Create alias via catchall")
|
|
|
|
|
|
|
|
return custom_domain, None
|
|
|
|
|
|
|
|
|
|
|
|
def check_if_alias_can_be_auto_created_for_a_directory(
|
|
|
|
address: str, notify_user: bool = True
|
|
|
|
) -> Optional[Directory]:
|
|
|
|
"""
|
|
|
|
Try to create an alias with directory
|
2022-04-28 18:43:10 +02:00
|
|
|
If an alias would be created, return the dictionary that would trigger the creation. Otherwise, return None.
|
2022-04-28 14:43:24 +02:00
|
|
|
"""
|
|
|
|
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
|
|
|
|
if not can_create_directory_for_address(address):
|
|
|
|
return None
|
|
|
|
|
|
|
|
# alias contains one of the 3 special directory separator: "/", "+" or "#"
|
|
|
|
if "/" in address:
|
|
|
|
sep = "/"
|
|
|
|
elif "+" in address:
|
|
|
|
sep = "+"
|
|
|
|
elif "#" in address:
|
|
|
|
sep = "#"
|
|
|
|
else:
|
|
|
|
# if there's no directory separator in the alias, no way to auto-create it
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.info(f"Cannot auto-create {address} since it has no directory separator")
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
directory_name = address[: address.find(sep)]
|
|
|
|
LOG.d("directory_name %s", directory_name)
|
|
|
|
|
|
|
|
directory = Directory.get_by(name=directory_name)
|
|
|
|
if not directory:
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.info(
|
|
|
|
f"Cannot auto-create {address} because there is no directory for {directory_name}"
|
|
|
|
)
|
2022-04-28 14:43:24 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
user: User = directory.user
|
|
|
|
if user.disabled:
|
|
|
|
LOG.i("Disabled %s can't create new alias with directory", user)
|
|
|
|
return None
|
|
|
|
|
|
|
|
if not user.can_create_new_alias():
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.d(
|
|
|
|
f"{user} can't create new directory alias {address} because user cannot create aliases"
|
|
|
|
)
|
2022-04-28 14:43:24 +02:00
|
|
|
if notify_user:
|
|
|
|
send_cannot_create_directory_alias(user, address, directory_name)
|
|
|
|
return None
|
|
|
|
|
|
|
|
if directory.disabled:
|
2024-08-08 15:50:57 +02:00
|
|
|
LOG.d(
|
|
|
|
f"{user} can't create new directory alias {address} bcause directory is disabled"
|
|
|
|
)
|
2022-04-28 14:43:24 +02:00
|
|
|
if notify_user:
|
|
|
|
send_cannot_create_directory_alias_disabled(user, address, directory_name)
|
|
|
|
return None
|
|
|
|
|
|
|
|
return directory
|
|
|
|
|
|
|
|
|
2020-04-04 15:24:27 +02:00
|
|
|
def try_auto_create(address: str) -> Optional[Alias]:
|
2020-08-27 10:20:48 +02:00
|
|
|
"""Try to auto-create the alias using directory or catch-all domain"""
|
2022-01-06 11:12:26 +01:00
|
|
|
# VERP for reply phase is {BOUNCE_PREFIX_FOR_REPLY_PHASE}+{email_log.id}+@{alias_domain}
|
|
|
|
if address.startswith(f"{BOUNCE_PREFIX_FOR_REPLY_PHASE}+") and "+@" in address:
|
2021-09-08 11:29:55 +02:00
|
|
|
LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX_FOR_REPLY_PHASE)
|
2021-05-25 17:58:03 +02:00
|
|
|
return None
|
|
|
|
|
2022-01-06 11:12:26 +01:00
|
|
|
# VERP for forward phase is BOUNCE_PREFIX + email_log.id + BOUNCE_SUFFIX
|
|
|
|
if address.startswith(BOUNCE_PREFIX) and address.endswith(BOUNCE_SUFFIX):
|
2022-01-05 15:16:04 +01:00
|
|
|
LOG.e("alias %s can't start with %s", address, BOUNCE_PREFIX)
|
|
|
|
return None
|
|
|
|
|
2021-09-17 17:42:16 +02:00
|
|
|
try:
|
|
|
|
# NOT allow unicode for now
|
|
|
|
validate_email(address, check_deliverability=False, allow_smtputf8=False)
|
|
|
|
except EmailNotValidError:
|
|
|
|
return None
|
|
|
|
|
2021-09-22 09:44:35 +02:00
|
|
|
alias = try_auto_create_via_domain(address)
|
2020-04-04 15:24:27 +02:00
|
|
|
if not alias:
|
|
|
|
alias = try_auto_create_directory(address)
|
|
|
|
|
|
|
|
return alias
|
|
|
|
|
|
|
|
|
|
|
|
def try_auto_create_directory(address: str) -> Optional[Alias]:
|
|
|
|
"""
|
|
|
|
Try to create an alias with directory
|
|
|
|
"""
|
2022-04-28 14:43:24 +02:00
|
|
|
directory = check_if_alias_can_be_auto_created_for_a_directory(
|
|
|
|
address, notify_user=True
|
|
|
|
)
|
|
|
|
if not directory:
|
|
|
|
return None
|
2020-12-07 10:55:13 +01:00
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
try:
|
|
|
|
LOG.d("create alias %s for directory %s", address, directory)
|
2020-05-23 19:45:26 +02:00
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
mailboxes = directory.mailboxes
|
2020-06-05 22:30:32 +02:00
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
alias = Alias.create(
|
|
|
|
email=address,
|
|
|
|
user_id=directory.user_id,
|
|
|
|
directory_id=directory.id,
|
|
|
|
mailbox_id=mailboxes[0].id,
|
|
|
|
)
|
|
|
|
if not directory.user.disable_automatic_alias_note:
|
|
|
|
alias.note = f"Created by directory {directory.name}"
|
|
|
|
Session.flush()
|
|
|
|
for i in range(1, len(mailboxes)):
|
|
|
|
AliasMailbox.create(
|
|
|
|
alias_id=alias.id,
|
|
|
|
mailbox_id=mailboxes[i].id,
|
2020-05-23 19:45:26 +02:00
|
|
|
)
|
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
Session.commit()
|
|
|
|
return alias
|
|
|
|
except AliasInTrashError:
|
|
|
|
LOG.w(
|
|
|
|
"Alias %s was deleted before, cannot auto-create using directory %s, user %s",
|
|
|
|
address,
|
|
|
|
directory.name,
|
|
|
|
directory.user,
|
|
|
|
)
|
|
|
|
return None
|
|
|
|
except IntegrityError:
|
|
|
|
LOG.w("Alias %s already exists", address)
|
|
|
|
Session.rollback()
|
|
|
|
alias = Alias.get_by(email=address)
|
|
|
|
return alias
|
2020-04-04 15:24:27 +02:00
|
|
|
|
|
|
|
|
2021-09-22 09:44:35 +02:00
|
|
|
def try_auto_create_via_domain(address: str) -> Optional[Alias]:
|
|
|
|
"""Try to create an alias with catch-all or auto-create rules on custom domain"""
|
2022-04-28 14:43:24 +02:00
|
|
|
can_create = check_if_alias_can_be_auto_created_for_custom_domain(address)
|
|
|
|
if not can_create:
|
2020-04-04 15:24:27 +02:00
|
|
|
return None
|
2022-04-28 14:43:24 +02:00
|
|
|
custom_domain, rule = can_create
|
2021-09-22 09:43:48 +02:00
|
|
|
|
2022-04-28 14:43:24 +02:00
|
|
|
if rule:
|
|
|
|
alias_note = f"Created by rule {rule.order} with regex {rule.regex}"
|
|
|
|
mailboxes = rule.mailboxes
|
|
|
|
else:
|
|
|
|
alias_note = "Created by catchall option"
|
2021-09-20 18:29:36 +02:00
|
|
|
mailboxes = custom_domain.mailboxes
|
2020-04-04 15:24:27 +02:00
|
|
|
|
2022-01-13 09:33:32 +01:00
|
|
|
# a rule can have 0 mailboxes. Happened when a mailbox is deleted
|
|
|
|
if not mailboxes:
|
2022-04-28 14:43:24 +02:00
|
|
|
LOG.d(
|
|
|
|
"use %s default mailbox for %s %s",
|
|
|
|
custom_domain.user,
|
|
|
|
address,
|
|
|
|
custom_domain,
|
|
|
|
)
|
|
|
|
mailboxes = [custom_domain.user.default_mailbox]
|
2022-01-13 09:33:32 +01:00
|
|
|
|
2020-05-23 19:45:26 +02:00
|
|
|
try:
|
|
|
|
LOG.d("create alias %s for domain %s", address, custom_domain)
|
|
|
|
alias = Alias.create(
|
|
|
|
email=address,
|
|
|
|
user_id=custom_domain.user_id,
|
|
|
|
custom_domain_id=custom_domain.id,
|
|
|
|
automatic_creation=True,
|
2020-08-01 12:31:43 +02:00
|
|
|
mailbox_id=mailboxes[0].id,
|
2020-05-23 19:45:26 +02:00
|
|
|
)
|
2021-10-23 18:24:28 +02:00
|
|
|
if not custom_domain.user.disable_automatic_alias_note:
|
|
|
|
alias.note = alias_note
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.flush()
|
2020-08-01 12:31:43 +02:00
|
|
|
for i in range(1, len(mailboxes)):
|
|
|
|
AliasMailbox.create(
|
2020-08-27 10:20:48 +02:00
|
|
|
alias_id=alias.id,
|
|
|
|
mailbox_id=mailboxes[i].id,
|
2020-08-01 12:31:43 +02:00
|
|
|
)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2020-05-23 19:45:26 +02:00
|
|
|
return alias
|
|
|
|
except AliasInTrashError:
|
2021-09-08 11:29:55 +02:00
|
|
|
LOG.w(
|
2020-04-04 15:24:27 +02:00
|
|
|
"Alias %s was deleted before, cannot auto-create using domain catch-all %s, user %s",
|
|
|
|
address,
|
|
|
|
custom_domain,
|
2022-04-28 14:43:24 +02:00
|
|
|
custom_domain.user,
|
2020-04-04 15:24:27 +02:00
|
|
|
)
|
|
|
|
return None
|
2020-09-14 12:18:15 +02:00
|
|
|
except IntegrityError:
|
2021-09-08 11:29:55 +02:00
|
|
|
LOG.w("Alias %s already exists", address)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.rollback()
|
2020-09-14 12:18:15 +02:00
|
|
|
alias = Alias.get_by(email=address)
|
|
|
|
return alias
|
2021-01-11 15:45:41 +01:00
|
|
|
except DataError:
|
2021-09-08 11:29:55 +02:00
|
|
|
LOG.w("Cannot create alias %s", address)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.rollback()
|
2021-01-11 15:45:41 +01:00
|
|
|
return None
|
2020-09-14 12:18:15 +02:00
|
|
|
|
2020-04-04 15:24:27 +02:00
|
|
|
|
2024-07-08 16:39:18 +02:00
|
|
|
def delete_alias(
|
2024-08-23 13:32:32 +02:00
|
|
|
alias: Alias,
|
|
|
|
user: User,
|
|
|
|
reason: AliasDeleteReason = AliasDeleteReason.Unspecified,
|
|
|
|
commit: bool = False,
|
2024-07-08 16:39:18 +02:00
|
|
|
):
|
2020-08-14 12:02:33 +02:00
|
|
|
"""
|
|
|
|
Delete an alias and add it to either global or domain trash
|
|
|
|
Should be used instead of Alias.delete, DomainDeletedAlias.create, DeletedAlias.create
|
|
|
|
"""
|
2024-04-08 15:05:51 +02:00
|
|
|
LOG.i(f"User {user} has deleted alias {alias}")
|
|
|
|
# save deleted alias to either global or domain tra
|
2020-05-23 11:49:34 +02:00
|
|
|
if alias.custom_domain_id:
|
2020-08-23 20:24:46 +02:00
|
|
|
if not DomainDeletedAlias.get_by(
|
2020-08-23 20:17:50 +02:00
|
|
|
email=alias.email, domain_id=alias.custom_domain_id
|
|
|
|
):
|
2024-04-08 15:05:51 +02:00
|
|
|
domain_deleted_alias = DomainDeletedAlias(
|
|
|
|
user_id=user.id,
|
|
|
|
email=alias.email,
|
|
|
|
domain_id=alias.custom_domain_id,
|
2024-07-08 16:39:18 +02:00
|
|
|
reason=reason,
|
2020-05-23 11:49:34 +02:00
|
|
|
)
|
2024-04-08 15:05:51 +02:00
|
|
|
Session.add(domain_deleted_alias)
|
2022-01-06 15:29:37 +01:00
|
|
|
Session.commit()
|
2024-04-08 15:05:51 +02:00
|
|
|
LOG.i(
|
|
|
|
f"Moving {alias} to domain {alias.custom_domain_id} trash {domain_deleted_alias}"
|
|
|
|
)
|
2020-05-23 11:49:34 +02:00
|
|
|
else:
|
2020-08-23 20:17:50 +02:00
|
|
|
if not DeletedAlias.get_by(email=alias.email):
|
2024-07-08 16:39:18 +02:00
|
|
|
deleted_alias = DeletedAlias(email=alias.email, reason=reason)
|
2024-04-08 15:05:51 +02:00
|
|
|
Session.add(deleted_alias)
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.commit()
|
2024-04-08 15:05:51 +02:00
|
|
|
LOG.i(f"Moving {alias} to global trash {deleted_alias}")
|
2020-08-14 12:02:33 +02:00
|
|
|
|
2024-09-06 18:23:12 +02:00
|
|
|
alias_id = alias.id
|
|
|
|
alias_email = alias.email
|
2024-10-14 12:45:00 +02:00
|
|
|
|
|
|
|
emit_alias_audit_log(
|
|
|
|
alias, AliasAuditLogAction.DeleteAlias, "Alias deleted by user action"
|
|
|
|
)
|
2021-10-12 14:36:47 +02:00
|
|
|
Alias.filter(Alias.id == alias.id).delete()
|
|
|
|
Session.commit()
|
2020-08-30 19:56:45 +02:00
|
|
|
|
2024-05-23 10:27:08 +02:00
|
|
|
EventDispatcher.send_event(
|
2024-09-06 18:23:12 +02:00
|
|
|
user,
|
2024-09-13 14:25:38 +02:00
|
|
|
EventContent(alias_deleted=AliasDeleted(id=alias_id, email=alias_email)),
|
2024-05-23 10:27:08 +02:00
|
|
|
)
|
2024-08-23 13:32:32 +02:00
|
|
|
if commit:
|
|
|
|
Session.commit()
|
2024-05-23 10:27:08 +02:00
|
|
|
|
2020-08-30 19:56:45 +02:00
|
|
|
|
|
|
|
def aliases_for_mailbox(mailbox: Mailbox) -> [Alias]:
|
|
|
|
"""
|
|
|
|
get list of aliases for a given mailbox
|
|
|
|
"""
|
2021-10-12 14:36:47 +02:00
|
|
|
ret = set(Alias.filter(Alias.mailbox_id == mailbox.id).all())
|
2020-08-30 19:56:45 +02:00
|
|
|
|
|
|
|
for alias in (
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.query(Alias)
|
2020-08-30 19:56:45 +02:00
|
|
|
.join(AliasMailbox, Alias.id == AliasMailbox.alias_id)
|
|
|
|
.filter(AliasMailbox.mailbox_id == mailbox.id)
|
|
|
|
):
|
|
|
|
ret.add(alias)
|
|
|
|
|
|
|
|
return list(ret)
|
|
|
|
|
|
|
|
|
|
|
|
def nb_email_log_for_mailbox(mailbox: Mailbox):
|
|
|
|
aliases = aliases_for_mailbox(mailbox)
|
|
|
|
alias_ids = [alias.id for alias in aliases]
|
|
|
|
return (
|
2021-10-12 14:36:47 +02:00
|
|
|
Session.query(EmailLog)
|
2020-08-30 19:56:45 +02:00
|
|
|
.join(Contact, EmailLog.contact_id == Contact.id)
|
|
|
|
.filter(Contact.alias_id.in_(alias_ids))
|
|
|
|
.count()
|
|
|
|
)
|
2020-11-03 10:39:08 +01:00
|
|
|
|
|
|
|
|
2021-04-30 11:37:17 +02:00
|
|
|
# Only lowercase letters, numbers, dots (.), dashes (-) and underscores (_) are currently supported
|
|
|
|
_ALIAS_PREFIX_PATTERN = r"[0-9a-z-_.]{1,}"
|
2020-11-03 10:39:08 +01:00
|
|
|
|
|
|
|
|
|
|
|
def check_alias_prefix(alias_prefix) -> bool:
|
2020-11-18 10:38:35 +01:00
|
|
|
if len(alias_prefix) > 40:
|
|
|
|
return False
|
|
|
|
|
2020-11-03 10:39:08 +01:00
|
|
|
if re.fullmatch(_ALIAS_PREFIX_PATTERN, alias_prefix) is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2022-11-23 13:51:08 +01:00
|
|
|
|
|
|
|
|
|
|
|
def alias_export_csv(user, csv_direct_export=False):
|
|
|
|
"""
|
|
|
|
Get user aliases as importable CSV file
|
|
|
|
Output:
|
|
|
|
Importable CSV file
|
|
|
|
|
|
|
|
"""
|
|
|
|
data = [["alias", "note", "enabled", "mailboxes"]]
|
|
|
|
for alias in Alias.filter_by(user_id=user.id).all(): # type: Alias
|
|
|
|
# Always put the main mailbox first
|
|
|
|
# It is seen a primary while importing
|
|
|
|
alias_mailboxes = alias.mailboxes
|
|
|
|
alias_mailboxes.insert(
|
|
|
|
0, alias_mailboxes.pop(alias_mailboxes.index(alias.mailbox))
|
|
|
|
)
|
|
|
|
|
|
|
|
mailboxes = " ".join([mailbox.email for mailbox in alias_mailboxes])
|
|
|
|
data.append([alias.email, alias.note, alias.enabled, mailboxes])
|
|
|
|
|
|
|
|
si = StringIO()
|
|
|
|
cw = csv.writer(si)
|
|
|
|
cw.writerows(data)
|
|
|
|
if csv_direct_export:
|
|
|
|
return si.getvalue()
|
|
|
|
output = make_response(si.getvalue())
|
|
|
|
output.headers["Content-Disposition"] = "attachment; filename=aliases.csv"
|
|
|
|
output.headers["Content-type"] = "text/csv"
|
|
|
|
return output
|
2023-08-31 13:42:44 +02:00
|
|
|
|
|
|
|
|
2024-10-14 12:45:00 +02:00
|
|
|
def transfer_alias(alias: Alias, new_user: User, new_mailboxes: [Mailbox]):
|
2023-08-31 13:42:44 +02:00
|
|
|
# cannot transfer alias which is used for receiving newsletter
|
|
|
|
if User.get_by(newsletter_alias_id=alias.id):
|
|
|
|
raise Exception("Cannot transfer alias that's used to receive newsletter")
|
|
|
|
|
|
|
|
# update user_id
|
|
|
|
Session.query(Contact).filter(Contact.alias_id == alias.id).update(
|
|
|
|
{"user_id": new_user.id}
|
|
|
|
)
|
|
|
|
|
|
|
|
Session.query(AliasUsedOn).filter(AliasUsedOn.alias_id == alias.id).update(
|
|
|
|
{"user_id": new_user.id}
|
|
|
|
)
|
|
|
|
|
|
|
|
Session.query(ClientUser).filter(ClientUser.alias_id == alias.id).update(
|
|
|
|
{"user_id": new_user.id}
|
|
|
|
)
|
|
|
|
|
|
|
|
# remove existing mailboxes from the alias
|
|
|
|
Session.query(AliasMailbox).filter(AliasMailbox.alias_id == alias.id).delete()
|
|
|
|
|
|
|
|
# set mailboxes
|
|
|
|
alias.mailbox_id = new_mailboxes.pop().id
|
|
|
|
for mb in new_mailboxes:
|
|
|
|
AliasMailbox.create(alias_id=alias.id, mailbox_id=mb.id)
|
|
|
|
|
|
|
|
# alias has never been transferred before
|
|
|
|
if not alias.original_owner_id:
|
|
|
|
alias.original_owner_id = alias.user_id
|
|
|
|
|
|
|
|
# inform previous owner
|
|
|
|
old_user = alias.user
|
|
|
|
send_email(
|
|
|
|
old_user.email,
|
|
|
|
f"Alias {alias.email} has been received",
|
|
|
|
render(
|
|
|
|
"transactional/alias-transferred.txt",
|
2024-07-03 14:59:16 +02:00
|
|
|
user=old_user,
|
2023-08-31 13:42:44 +02:00
|
|
|
alias=alias,
|
|
|
|
),
|
|
|
|
render(
|
|
|
|
"transactional/alias-transferred.html",
|
2024-07-03 14:59:16 +02:00
|
|
|
user=old_user,
|
2023-08-31 13:42:44 +02:00
|
|
|
alias=alias,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
# now the alias belongs to the new user
|
|
|
|
alias.user_id = new_user.id
|
|
|
|
|
|
|
|
# set some fields back to default
|
|
|
|
alias.disable_pgp = False
|
|
|
|
alias.pinned = False
|
|
|
|
|
2024-10-14 12:45:00 +02:00
|
|
|
emit_alias_audit_log(
|
|
|
|
alias=alias,
|
|
|
|
action=AliasAuditLogAction.TransferredAlias,
|
|
|
|
message=f"Lost ownership of alias due to alias transfer confirmed. New owner is {new_user.id}",
|
|
|
|
user_id=old_user.id,
|
|
|
|
)
|
2024-10-04 15:17:59 +02:00
|
|
|
EventDispatcher.send_event(
|
|
|
|
old_user,
|
|
|
|
EventContent(
|
|
|
|
alias_deleted=AliasDeleted(
|
|
|
|
id=alias.id,
|
|
|
|
email=alias.email,
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
2024-10-14 12:45:00 +02:00
|
|
|
|
|
|
|
emit_alias_audit_log(
|
|
|
|
alias=alias,
|
|
|
|
action=AliasAuditLogAction.AcceptTransferAlias,
|
|
|
|
message=f"Accepted alias transfer from user {old_user.id}",
|
|
|
|
user_id=new_user.id,
|
|
|
|
)
|
2024-10-04 15:17:59 +02:00
|
|
|
EventDispatcher.send_event(
|
|
|
|
new_user,
|
|
|
|
EventContent(
|
|
|
|
alias_created=AliasCreated(
|
|
|
|
id=alias.id,
|
|
|
|
email=alias.email,
|
|
|
|
note=alias.note,
|
|
|
|
enabled=alias.enabled,
|
|
|
|
created_at=int(alias.created_at.timestamp),
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2023-08-31 13:42:44 +02:00
|
|
|
Session.commit()
|
2024-05-23 10:27:08 +02:00
|
|
|
|
|
|
|
|
2024-10-14 12:45:00 +02:00
|
|
|
def change_alias_status(
|
|
|
|
alias: Alias, enabled: bool, message: Optional[str] = None, commit: bool = False
|
|
|
|
):
|
2024-06-07 15:36:18 +02:00
|
|
|
LOG.i(f"Changing alias {alias} enabled to {enabled}")
|
2024-05-23 10:27:08 +02:00
|
|
|
alias.enabled = enabled
|
|
|
|
|
2024-06-07 15:36:18 +02:00
|
|
|
event = AliasStatusChanged(
|
2024-09-13 14:25:38 +02:00
|
|
|
id=alias.id,
|
|
|
|
email=alias.email,
|
|
|
|
enabled=enabled,
|
|
|
|
created_at=int(alias.created_at.timestamp),
|
2024-05-23 10:27:08 +02:00
|
|
|
)
|
|
|
|
EventDispatcher.send_event(alias.user, EventContent(alias_status_change=event))
|
2024-10-14 12:45:00 +02:00
|
|
|
audit_log_message = f"Set alias status to {enabled}"
|
|
|
|
if message is not None:
|
|
|
|
audit_log_message += f". {message}"
|
|
|
|
emit_alias_audit_log(
|
|
|
|
alias, AliasAuditLogAction.ChangeAliasStatus, audit_log_message
|
|
|
|
)
|
2024-05-23 10:27:08 +02:00
|
|
|
|
|
|
|
if commit:
|
|
|
|
Session.commit()
|
2024-10-08 12:48:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class AliasRecipientName:
|
|
|
|
name: str
|
|
|
|
message: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
def get_alias_recipient_name(alias: Alias) -> AliasRecipientName:
|
|
|
|
"""
|
|
|
|
Logic:
|
|
|
|
1. If alias has name, use it
|
|
|
|
2. If alias has custom domain, and custom domain has name, use it
|
|
|
|
3. Otherwise, use the alias email as the recipient
|
|
|
|
"""
|
|
|
|
if alias.name:
|
|
|
|
return AliasRecipientName(
|
|
|
|
name=sl_formataddr((alias.name, alias.email)),
|
|
|
|
message=f"Put alias name {alias.name} in from header",
|
|
|
|
)
|
|
|
|
elif alias.custom_domain:
|
|
|
|
if alias.custom_domain.name:
|
|
|
|
return AliasRecipientName(
|
|
|
|
name=sl_formataddr((alias.custom_domain.name, alias.email)),
|
|
|
|
message=f"Put domain default alias name {alias.custom_domain.name} in from header",
|
|
|
|
)
|
2024-10-09 10:46:48 +02:00
|
|
|
return AliasRecipientName(name=alias.email)
|