import email.policy import email.utils from email.message import EmailMessage, Message from email.mime.base import MIMEBase from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText import aiosmtplib import dkim from app.config import get_settings from app.models import MailDomain from app.services.mime_body_extractor import ExtractedMimePart SMTP_POLICY = email.policy.SMTP.clone(max_line_length=998) def _mime_attachment(part: ExtractedMimePart) -> MIMEBase: maintype, _, subtype = part.content_type.partition("/") if not subtype: maintype, subtype = "application", "octet-stream" mime = MIMEBase(maintype, subtype, policy=SMTP_POLICY) mime.set_payload(part.payload) email.utils.encode_base64(mime) mime.add_header("Content-Disposition", part.disposition, filename=part.filename) if part.content_id: mime.add_header("Content-ID", f"<{part.content_id}>") return mime 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, in_reply_to: str | None = None, references: list[str] | None = None, message_id: str | None = None, inline_parts: list[ExtractedMimePart] | None = None, file_attachments: list[ExtractedMimePart] | None = None, ) -> Message: inline_parts = inline_parts or [] file_attachments = file_attachments or [] if inline_parts or file_attachments: root = MIMEMultipart("mixed", policy=SMTP_POLICY) root["From"] = sender root["To"] = ", ".join(recipients) if cc: root["Cc"] = ", ".join(cc) if reply_to: root["Reply-To"] = reply_to root["Subject"] = subject root["Date"] = email.utils.formatdate(localtime=True) generated_id = message_id or email.utils.make_msgid(domain=sender.split("@")[-1]) root["Message-ID"] = generated_id if in_reply_to: root["In-Reply-To"] = in_reply_to if references: root["References"] = " ".join(references) alternative = MIMEMultipart("alternative", policy=SMTP_POLICY) alternative.attach(MIMEText(body_text, "plain", "utf-8", policy=SMTP_POLICY)) if body_html: if inline_parts: related = MIMEMultipart("related", policy=SMTP_POLICY) related.attach(MIMEText(body_html, "html", "utf-8", policy=SMTP_POLICY)) for part in inline_parts: related.attach(_mime_attachment(part)) alternative.attach(related) else: alternative.attach(MIMEText(body_html, "html", "utf-8", policy=SMTP_POLICY)) root.attach(alternative) for attachment in file_attachments: root.attach(_mime_attachment(attachment)) return root 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) generated_id = message_id or email.utils.make_msgid(domain=sender.split("@")[-1]) message["Message-ID"] = generated_id if in_reply_to: message["In-Reply-To"] = in_reply_to if references: message["References"] = " ".join(references) message.set_content(body_text) if body_html: message.add_alternative(body_html, subtype="html") return message def sign_message(message: Message, 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, in_reply_to: str | None = None, references: list[str] | None = None, message_id: str | None = None, inline_parts: list[ExtractedMimePart] | None = None, file_attachments: list[ExtractedMimePart] | None = None, ) -> str: message = build_message( sender=sender, recipients=recipients, cc=cc, subject=subject, body_text=body_text, body_html=body_html, reply_to=reply_to, in_reply_to=in_reply_to, references=references, message_id=message_id, inline_parts=inline_parts, file_attachments=file_attachments, ) signed = sign_message(message, domain) await send_via_smtp( signed, sender=sender, recipients=[*recipients, *cc, *bcc], ) return str(message["Message-ID"])