Coverage for src/couchers/email/smtp.py: 89%
47 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-29 01:26 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-12-29 01:26 +0000
1import smtplib
2from email.headerregistry import Address
3from email.message import EmailMessage
4from email.utils import make_msgid
5from pathlib import Path
7from couchers.config import config
8from couchers.crypto import EMAIL_SOURCE_DATA_KEY_NAME, random_hex, simple_hash_signature
9from couchers.models import Email
11template_base = Path(Path(__file__).parent / ".." / ".." / ".." / "templates" / "v2")
14def make_cid(sender_email):
15 cid = make_msgid(domain=Address(addr_spec=sender_email).domain)
16 without_tag = cid[1:-1]
17 return cid, without_tag
20def send_smtp_email(sender_name, sender_email, recipient, subject, plain, html, list_unsubscribe_header, source_data):
21 """
22 Sends out the email through SMTP, settings from config.
24 Returns a models.Email object that can be straight away added to the database.
25 """
26 message_id = random_hex()
27 msg = EmailMessage()
28 msg["Subject"] = subject
29 msg["From"] = Address(sender_name, addr_spec=sender_email)
30 msg["To"] = Address(addr_spec=recipient)
31 msg["X-Couchers-ID"] = message_id
33 if list_unsubscribe_header:
34 msg["List-Unsubscribe"] = list_unsubscribe_header
36 if source_data:
37 msg["X-Couchers-Source-Data"] = source_data
38 msg["X-Couchers-Source-Sig"] = simple_hash_signature(source_data, EMAIL_SOURCE_DATA_KEY_NAME)
40 msg.set_content(plain)
42 if html:
43 # for any png files in attachment_imgs/, goes through and replaces instances of the filename with attachment
44 used_attachments = []
45 for attachment in (template_base / "attachment_imgs").glob("*.png"):
46 attachment_html_path = str(attachment.relative_to(template_base))
47 if attachment_html_path not in html:
48 continue
49 # it's used in this template, so attach and replace it
50 data = attachment.read_bytes()
51 cid, wcid = make_cid(sender_email)
52 html = html.replace(attachment_html_path, f"cid:{wcid}")
53 used_attachments.append((cid, "image", "png", data))
55 msg.add_alternative(html, subtype="html")
57 for cid, mime_type, mime_subtype, data in used_attachments:
58 msg.get_payload()[1].add_related(data, mime_type, mime_subtype, cid=cid)
60 with smtplib.SMTP(config["SMTP_HOST"], config["SMTP_PORT"]) as server:
61 server.ehlo()
62 if not config["DEV"]:
63 server.starttls()
64 # stmplib docs recommend calling ehlo() before and after starttls()
65 server.ehlo()
66 server.login(config["SMTP_USERNAME"], config["SMTP_PASSWORD"])
67 server.sendmail(sender_email, recipient, msg.as_string())
69 return Email(
70 id=message_id,
71 sender_name=sender_name,
72 sender_email=sender_email,
73 recipient=recipient,
74 subject=subject,
75 plain=plain,
76 html=html,
77 list_unsubscribe_header=list_unsubscribe_header,
78 source_data=source_data,
79 )