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

1import smtplib 

2from email.headerregistry import Address 

3from email.message import EmailMessage 

4from email.utils import make_msgid 

5from pathlib import Path 

6 

7from couchers.config import config 

8from couchers.crypto import EMAIL_SOURCE_DATA_KEY_NAME, random_hex, simple_hash_signature 

9from couchers.models import Email 

10 

11template_base = Path(Path(__file__).parent / ".." / ".." / ".." / "templates" / "v2") 

12 

13 

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 

18 

19 

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. 

23 

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 

32 

33 if list_unsubscribe_header: 

34 msg["List-Unsubscribe"] = list_unsubscribe_header 

35 

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) 

39 

40 msg.set_content(plain) 

41 

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)) 

54 

55 msg.add_alternative(html, subtype="html") 

56 

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) 

59 

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()) 

68 

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 )