1+ #!/usr/bin/env python
2+ """
3+ Script to send a weekly digest email using SMTP (e.g., Office 365, Gmail).
4+ This is for testing SMTP-based delivery to a distribution list or group.
5+ """
6+
7+ import os
8+ import sys
9+ import asyncio
10+ import argparse
11+ import logging
12+ import smtplib
13+ from email .mime .text import MIMEText
14+ from typing import List
15+
16+ # Add the parent directory to sys.path to allow importing the app modules
17+ parent_dir = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' ))
18+ sys .path .insert (0 , parent_dir )
19+
20+ from src .services .weekly_digest import WeeklyDigestService
21+
22+ logging .basicConfig (
23+ level = logging .INFO ,
24+ format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' ,
25+ handlers = [logging .StreamHandler ()]
26+ )
27+ logger = logging .getLogger (__name__ )
28+
29+ async def generate_digest_html (days = None , status = None , limit = None ):
30+ digest_service = WeeklyDigestService ()
31+ signals_list = await digest_service .get_recent_signals (days = days , status = status , limit = limit )
32+ logger .info (f"Fetched { len (signals_list )} signals for digest." )
33+ html_content = digest_service .generate_email_html (signals_list )
34+ return html_content
35+
36+ def send_email_smtp (smtp_server , smtp_port , username , password , to_emails , subject , html_content ):
37+ msg = MIMEText (html_content , 'html' )
38+ msg ['Subject' ] = subject
39+ msg ['From' ] = username
40+ msg ['To' ] = ', ' .join (to_emails )
41+ with smtplib .SMTP (smtp_server , smtp_port ) as server :
42+ server .starttls ()
43+ server .login (username , password )
44+ server .sendmail (msg ['From' ], to_emails , msg .as_string ())
45+ logger .info (f"Email sent via SMTP to { to_emails } " )
46+
47+ def main ():
48+ parser = argparse .ArgumentParser (description = "Send weekly digest email via SMTP" )
49+ parser .add_argument ('--recipients' , nargs = '+' , required = True , help = "Email addresses to send the digest to (space-separated)" )
50+ parser .add_argument ('--days' , type = int , default = None , help = "Number of days to look back for signals (optional)" )
51+ parser .add_argument ('--status' , nargs = '+' , default = None , help = "Signal statuses to filter by (e.g. Draft Approved). Optional." )
52+ parser .add_argument ('--limit' , type = int , default = None , help = "Maximum number of signals to include (optional)" )
53+ parser .add_argument ('--smtp-server' , type = str , default = 'smtp.office365.com' , help = "SMTP server address" )
54+ parser .add_argument ('--smtp-port' , type = int , default = 587 , help = "SMTP server port" )
55+ parser .add_argument ('--smtp-user' , type = str , required = True , help = "SMTP username (your email)" )
56+ parser .add_argument ('--smtp-password' , type = str , required = True , help = "SMTP password (or app password)" )
57+ parser .add_argument ('--test' , action = 'store_true' , help = "Run in test mode (adds [TEST] to the subject line)" )
58+ args = parser .parse_args ()
59+
60+ subject = "UNDP Futures Weekly Digest"
61+ if args .test :
62+ subject = f"[TEST] { subject } "
63+
64+ # Validate email addresses
65+ for email in args .recipients :
66+ if "@" not in email :
67+ logger .error (f"Invalid email address: { email } " )
68+ sys .exit (1 )
69+
70+ # Map status strings to Status enum if provided
71+ status_enum = None
72+ if args .status :
73+ from src .services .weekly_digest import Status
74+ status_enum = [Status (s ) for s in args .status ]
75+
76+ # Generate digest HTML
77+ html_content = asyncio .run (generate_digest_html (days = args .days , status = status_enum , limit = args .limit ))
78+
79+ # Send email via SMTP
80+ send_email_smtp (
81+ smtp_server = args .smtp_server ,
82+ smtp_port = args .smtp_port ,
83+ username = args .smtp_user ,
84+ password = args .smtp_password ,
85+ to_emails = args .recipients ,
86+ subject = subject ,
87+ html_content = html_content
88+ )
89+
90+ if __name__ == "__main__" :
91+ main ()
0 commit comments