136 lines
5.7 KiB
Python
136 lines
5.7 KiB
Python
from __future__ import annotations
|
|
|
|
import smtplib
|
|
from email.message import EmailMessage
|
|
from pathlib import Path
|
|
from typing import Iterable
|
|
|
|
from app.mailer.domain.campaign import MailCampaign, MailEntry, MailServerSettings, TransportSecurity
|
|
from app.mailer.domain.queue import MailQueue
|
|
from app.mailer.domain.recipients import Recipient, RecipientType
|
|
from app.mailer.services.attachment_matching import match_files
|
|
from app.mailer.services.zip_service import create_encrypted_zip
|
|
|
|
|
|
def _recipient_header(recipients: Iterable[Recipient], recipient_type: RecipientType) -> str:
|
|
return ", ".join(r.formatted() for r in recipients if r.type == recipient_type)
|
|
|
|
|
|
def _recipient_values(recipients: list[Recipient]) -> dict[str, str]:
|
|
def rows(recipient_type: RecipientType) -> list[Recipient]:
|
|
return [r for r in recipients if r.type == recipient_type]
|
|
|
|
def joined(recipient_type: RecipientType, mode: str) -> str:
|
|
selected = rows(recipient_type)
|
|
if mode == "address":
|
|
return ", ".join(r.address for r in selected)
|
|
if mode == "name":
|
|
return ", ".join(r.name or r.address for r in selected)
|
|
return ", ".join(r.formatted() for r in selected)
|
|
|
|
return {
|
|
"mm_recipients": joined(RecipientType.TO, "formatted"),
|
|
"mm_recipients_address": joined(RecipientType.TO, "address"),
|
|
"mm_recipients_name": joined(RecipientType.TO, "name"),
|
|
"mm_cc": joined(RecipientType.CC, "formatted"),
|
|
"mm_cc_address": joined(RecipientType.CC, "address"),
|
|
"mm_cc_name": joined(RecipientType.CC, "name"),
|
|
"mm_bcc": joined(RecipientType.BCC, "formatted"),
|
|
"mm_bcc_address": joined(RecipientType.BCC, "address"),
|
|
"mm_bcc_name": joined(RecipientType.BCC, "name"),
|
|
}
|
|
|
|
|
|
def _message_attachment_paths(campaign: MailCampaign, entry: MailEntry) -> list[Path]:
|
|
paths: list[Path] = []
|
|
if entry.combine_attachments:
|
|
for config in campaign.global_attachment_configs:
|
|
paths.extend(match_files(campaign.base_attachment_path, config))
|
|
if campaign.individual_attachments:
|
|
for config in entry.attachment_configs:
|
|
paths.extend(match_files(campaign.base_attachment_path, config))
|
|
return paths
|
|
|
|
|
|
def get_not_sent_files(campaign: MailCampaign) -> set[Path]:
|
|
"""Return files matching campaign attachment configs that are not referenced by any built entry."""
|
|
all_files: set[Path] = set()
|
|
used_files: set[Path] = set()
|
|
for config in campaign.global_attachment_configs:
|
|
all_files.update(match_files(campaign.base_attachment_path, config))
|
|
for entry in campaign.mail_entries:
|
|
files = set(_message_attachment_paths(campaign, entry))
|
|
used_files.update(files)
|
|
all_files.update(files)
|
|
return all_files - used_files
|
|
|
|
|
|
def build_message(campaign: MailCampaign, entry: MailEntry, *, zip_attachments: bool = True) -> EmailMessage | None:
|
|
recipients = campaign.all_recipients_for(entry)
|
|
attachments = _message_attachment_paths(campaign, entry)
|
|
if not attachments and not campaign.send_without_attachments:
|
|
return None
|
|
|
|
values = {}
|
|
values.update(_recipient_values(recipients))
|
|
values.update(campaign.field_contents.as_value_map("global"))
|
|
values.update(entry.field_contents.as_value_map("local"))
|
|
|
|
message = EmailMessage()
|
|
sender = entry.from_recipient if campaign.individual_from and entry.from_recipient else campaign.global_from
|
|
if sender:
|
|
message["From"] = sender.formatted()
|
|
to_header = _recipient_header(recipients, RecipientType.TO)
|
|
cc_header = _recipient_header(recipients, RecipientType.CC)
|
|
if to_header:
|
|
message["To"] = to_header
|
|
if cc_header:
|
|
message["Cc"] = cc_header
|
|
message["Subject"] = campaign.subject_template.apply_values(values)
|
|
message.set_content(campaign.mail_template.apply_values(values))
|
|
|
|
attachment_paths = attachments
|
|
|
|
if attachments and zip_attachments:
|
|
number = entry.get_field_content_from_name("number").as_string() if "number" in entry.field_contents.field_map else "attachments"
|
|
password = entry.get_field_content_from_name("password").as_string() if "password" in entry.field_contents.field_map else ""
|
|
zip_path = campaign.base_attachment_path / f"{number}.zip"
|
|
attachment_paths = [create_encrypted_zip(zip_path, attachments, password)]
|
|
|
|
for attachment_path in attachment_paths:
|
|
data = attachment_path.read_bytes()
|
|
message.add_attachment(data, maintype="application", subtype="octet-stream", filename=attachment_path.name)
|
|
return message
|
|
|
|
|
|
def build_mail_queue(campaign: MailCampaign, *, zip_attachments: bool = True) -> MailQueue:
|
|
queue = MailQueue()
|
|
for entry in campaign.mail_entries:
|
|
if not entry.is_active:
|
|
continue
|
|
message = build_message(campaign, entry, zip_attachments=zip_attachments)
|
|
if message is not None:
|
|
queue.add_mail(message)
|
|
return queue
|
|
|
|
|
|
def send_mail_queue(settings: MailServerSettings, queue: MailQueue) -> MailQueue:
|
|
retry_queue = MailQueue()
|
|
if settings.transport_security == TransportSecurity.TLS:
|
|
smtp = smtplib.SMTP_SSL(settings.server, settings.resolved_port(), timeout=60)
|
|
else:
|
|
smtp = smtplib.SMTP(settings.server, settings.resolved_port(), timeout=60)
|
|
try:
|
|
if settings.transport_security == TransportSecurity.STARTTLS:
|
|
smtp.starttls()
|
|
if settings.username:
|
|
smtp.login(settings.username, settings.password)
|
|
for message in queue:
|
|
try:
|
|
smtp.send_message(message)
|
|
except Exception:
|
|
retry_queue.add_mail(message)
|
|
finally:
|
|
smtp.quit()
|
|
return retry_queue
|