agentic.email/backend/app/services/mail_aliases.py
2026-10-05 04:53:45 +05:30

154 lines
4.4 KiB
Python

"""Email alias management."""
from __future__ import annotations
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.models import Account, MailAlias, MailGroup
from app.schemas import MailAliasCreate, MailAliasRead, MailGroupEmailCheck
def _alias_read(alias: MailAlias) -> MailAliasRead:
return MailAliasRead(
id=alias.id,
alias_address=alias.alias_address,
forwards_to=alias.forwards_to,
alias_type=alias.alias_type,
is_domain_alias=alias.is_domain_alias,
status=alias.status,
created_at=alias.created_at,
)
async def list_mail_aliases(
session: AsyncSession,
company_domain: str,
) -> tuple[list[MailAliasRead], dict[str, int]]:
result = await session.scalars(
select(MailAlias)
.where(MailAlias.company_domain == company_domain)
.order_by(MailAlias.created_at.desc())
)
aliases = list(result.all())
items = [_alias_read(alias) for alias in aliases]
counts = {
"all": len(items),
"domain": sum(1 for item in items if item.is_domain_alias),
}
return items, counts
async def check_alias_email_available(
session: AsyncSession,
company_domain: str,
email: str,
) -> MailGroupEmailCheck:
normalized = str(email).lower()
if not normalized.endswith(f"@{company_domain}"):
return MailGroupEmailCheck(
email=normalized,
available=False,
conflict_type="invalid_domain",
)
account = await session.scalar(
select(Account).where(func.lower(Account.email) == normalized)
)
if account:
return MailGroupEmailCheck(
email=normalized,
available=False,
conflict_type="user",
)
group = await session.scalar(
select(MailGroup).where(func.lower(MailGroup.email) == normalized)
)
if group:
return MailGroupEmailCheck(
email=normalized,
available=False,
conflict_type="group",
)
alias = await session.scalar(
select(MailAlias).where(
MailAlias.company_domain == company_domain,
func.lower(MailAlias.alias_address) == normalized,
)
)
if alias:
return MailGroupEmailCheck(
email=normalized,
available=False,
conflict_type="alias",
)
return MailGroupEmailCheck(email=normalized, available=True)
async def _destination_exists(
session: AsyncSession,
company_domain: str,
address: str,
) -> bool:
normalized = address.lower()
if not normalized.endswith(f"@{company_domain}"):
return False
account = await session.scalar(
select(Account).where(func.lower(Account.email) == normalized)
)
if account:
return True
group = await session.scalar(
select(MailGroup).where(func.lower(MailGroup.email) == normalized)
)
return group is not None
async def create_mail_alias(
session: AsyncSession,
*,
manager: Account,
payload: MailAliasCreate,
) -> MailAliasRead:
company_domain = manager.company_domain
if company_domain is None:
raise ValueError("Company domain is required.")
alias_address = str(payload.alias_address).lower()
forwards_to = str(payload.forwards_to).lower()
if not alias_address.endswith(f"@{company_domain}"):
raise ValueError(f"Alias address must use @{company_domain}.")
if not forwards_to.endswith(f"@{company_domain}"):
raise ValueError(f"Forwarding address must use @{company_domain}.")
if alias_address == forwards_to:
raise ValueError("Alias cannot forward to itself.")
availability = await check_alias_email_available(
session,
company_domain,
alias_address,
)
if not availability.available:
raise ValueError("That alias address is already in use.")
if not await _destination_exists(session, company_domain, forwards_to):
raise ValueError("Forwarding address must be an existing user or group.")
alias = MailAlias(
company_domain=company_domain,
alias_address=alias_address,
forwards_to=forwards_to,
alias_type=payload.alias_type,
is_domain_alias=payload.is_domain_alias,
status="active",
created_by_id=manager.id,
)
session.add(alias)
await session.flush()
return _alias_read(alias)