import email.policy import email.utils from email.message import EmailMessage import aiosmtplib import dkim from app.config import get_settings from app.models import MailDomain SMTP_POLICY = email.policy.SMTP.clone(max_line_length=998) def build_message( *, sender: str, recipients: list[str], cc: list[str], subject: str, body_text: str, body_html: str | None, reply_to: str | None = None, ) -> EmailMessage: message = EmailMessage(policy=SMTP_POLICY) message["From"] = sender message["To"] = ", ".join(recipients) if cc: message["Cc"] = ", ".join(cc) if reply_to: message["Reply-To"] = reply_to message["Subject"] = subject message["Date"] = email.utils.formatdate(localtime=True) message["Message-ID"] = email.utils.make_msgid(domain=sender.split("@")[-1]) message.set_content(body_text) if body_html: message.add_alternative(body_html, subtype="html") return message def sign_message(message: EmailMessage, domain: MailDomain) -> bytes: raw_message = message.as_bytes(policy=SMTP_POLICY) signed_headers = [ b"from", b"to", b"subject", b"date", b"message-id", b"mime-version", b"content-type", ] if message.get("Cc"): signed_headers.insert(3, b"cc") signature = dkim.sign( raw_message, domain.dkim_selector.encode(), domain.domain_name.encode(), domain.dkim_private_key.encode(), include_headers=signed_headers, canonicalize=(b"relaxed", b"relaxed"), ) if not signature.endswith(b"\r\n"): signature += b"\r\n" return signature + raw_message async def send_via_smtp( message_bytes: bytes, *, sender: str, recipients: list[str], ) -> None: settings = get_settings() await aiosmtplib.send( message_bytes, sender=sender, recipients=recipients, hostname=settings.smtp_host, port=settings.smtp_port, local_hostname=settings.smtp_client_hostname, username=settings.smtp_username or None, password=settings.smtp_password or None, start_tls=settings.smtp_use_tls, timeout=settings.smtp_timeout, ) async def deliver_email( *, sender: str, recipients: list[str], cc: list[str], bcc: list[str], subject: str, body_text: str, body_html: str | None, domain: MailDomain, reply_to: str | None = None, ) -> None: message = build_message( sender=sender, recipients=recipients, cc=cc, subject=subject, body_text=body_text, body_html=body_html, reply_to=reply_to, ) signed = sign_message(message, domain) await send_via_smtp( signed, sender=sender, recipients=[*recipients, *cc, *bcc], )