diff --git a/Python-Version/config/config.yaml b/Python-Version/config/config.yaml
index 4009b2f..030bb01 100644
--- a/Python-Version/config/config.yaml
+++ b/Python-Version/config/config.yaml
@@ -80,11 +80,15 @@ retry:
notifications:
enabled: true
smtp:
- server: ${SMTP_SERVER}
- port: ${SMTP_PORT}
- user: ${SMTP_USER}
- password: ${SMTP_PASSWORD}
- sender_email: ${SENDER_EMAIL}
+ server: ${SMTP_SERVER:-}
+ port: ${SMTP_PORT:-587}
+ user: ${SMTP_USER:-}
+ password: ${SMTP_PASSWORD:-}
+ sender_email: ${SENDER_EMAIL:-}
+ mailgun:
+ api_key: ${MAILGUN_API_KEY:-}
+ domain: ${MAILGUN_DOMAIN:-}
+ sender_email: ${MAILGUN_SENDER_EMAIL:-}
recipients:
success:
- ${REPORT_EMAILS}
diff --git a/Python-Version/scripts/shared/notifier.py b/Python-Version/scripts/shared/notifier.py
index cd0ae37..f48bb0a 100644
--- a/Python-Version/scripts/shared/notifier.py
+++ b/Python-Version/scripts/shared/notifier.py
@@ -18,7 +18,7 @@ class Notifier:
self.config = config
self.enabled = config['notifications']['enabled']
- # SMTP configuration (preferred method)
+ # SMTP configuration
smtp_config = config['notifications'].get('smtp', {})
self.smtp_server = smtp_config.get('server')
self.smtp_port = smtp_config.get('port', 587)
@@ -26,6 +26,12 @@ class Notifier:
self.smtp_password = smtp_config.get('password')
self.sender_email = smtp_config.get('sender_email')
+ # Mailgun API configuration (preferred over SMTP when configured)
+ mailgun_config = config['notifications'].get('mailgun', {})
+ self.mailgun_api_key = mailgun_config.get('api_key')
+ self.mailgun_domain = mailgun_config.get('domain')
+ self.mailgun_sender = mailgun_config.get('sender_email') or self.sender_email
+
self.recipients = config['notifications']['recipients']
self.webhook_config = config.get('webhooks', {})
@@ -43,8 +49,8 @@ class Notifier:
logger.info("Notifications disabled, skipping email")
return
- if not self.smtp_server or not self.smtp_user:
- logger.warning("SMTP not configured, skipping email")
+ if not self.mailgun_api_key and (not self.smtp_server or not self.smtp_user):
+ logger.warning("Neither Mailgun API nor SMTP configured, skipping email")
return
try:
@@ -111,7 +117,7 @@ class Notifier:
"""
},
'a2_to_a3_batch_complete': {
- 'subject': "A2→A3 Batch Upload Complete - {{ successful_count }}/{{ total_files }} Successful",
+ 'subject': "A2→A3 Batch Upload Complete - {successful_count}/{total_files} Successful",
'html': """
@@ -978,59 +984,95 @@ class Notifier:
html_content = jinja_template.render(data)
subject = template['subject'].format(**data)
- # 2. Create MIME message
- if attachments:
- # Use MIMEMultipart for attachments
- message = MIMEMultipart()
- message['From'] = self.sender_email
- message['To'] = ", ".join(recipients) if isinstance(recipients, list) else recipients
- message['Subject'] = subject
-
- # Attach HTML body
- message.attach(MIMEText(html_content, "html"))
-
- # Attach files
- from email.mime.base import MIMEBase
- from email import encoders
- import os
-
- for file_path in attachments:
- try:
- if os.path.exists(file_path):
- with open(file_path, "rb") as attachment:
- part = MIMEBase("application", "octet-stream")
- part.set_payload(attachment.read())
-
- encoders.encode_base64(part)
- filename = os.path.basename(file_path)
- part.add_header(
- "Content-Disposition",
- f"attachment; filename= {filename}",
- )
- message.attach(part)
- logger.info("Attached file: {}".format(filename))
- else:
- logger.warning("Attachment not found: {}".format(file_path))
- except Exception as e:
- logger.error("Failed to attach file {}: {}".format(file_path, str(e)))
- else:
- # Use standard MIMEText for simple emails
- message = MIMEText(html_content, "html")
- message['From'] = self.sender_email
- message['To'] = ", ".join(recipients) if isinstance(recipients, list) else recipients
- message['Subject'] = subject
+ # 2. Send via Mailgun API or SMTP
+ recipient_list = recipients if isinstance(recipients, list) else [recipients]
- # 3. Send via SMTP
- with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
- server.starttls()
- server.login(self.smtp_user, self.smtp_password)
- server.send_message(message)
+ if self.mailgun_api_key and self.mailgun_domain:
+ self._send_via_mailgun_api(recipient_list, subject, html_content, attachments)
+ else:
+ self._send_via_smtp(recipient_list, subject, html_content, attachments)
logger.info("Email sent to {} (Template: {})".format(recipients, template_name))
except Exception as e:
logger.error("Failed to send email: {}".format(str(e)))
+ def _send_via_mailgun_api(self, recipient_list, subject, html_content, attachments=None):
+ """Send email via Mailgun REST API"""
+ import os
+ url = "https://api.mailgun.net/v3/{}/messages".format(self.mailgun_domain)
+
+ data = {
+ "from": self.mailgun_sender,
+ "to": recipient_list,
+ "subject": subject,
+ "html": html_content,
+ }
+
+ files = []
+ try:
+ if attachments:
+ for file_path in attachments:
+ if os.path.exists(file_path):
+ files.append(("attachment", (os.path.basename(file_path), open(file_path, "rb"))))
+ logger.info("Attaching file: {}".format(os.path.basename(file_path)))
+ else:
+ logger.warning("Attachment not found: {}".format(file_path))
+
+ response = requests.post(
+ url,
+ auth=("api", self.mailgun_api_key),
+ data=data,
+ files=files if files else None,
+ )
+ response.raise_for_status()
+ logger.info("Mailgun API response: {}".format(response.json()))
+ finally:
+ for _, file_tuple in files:
+ file_tuple[1].close()
+
+ def _send_via_smtp(self, recipient_list, subject, html_content, attachments=None):
+ """Send email via SMTP"""
+ import os
+ from email.mime.base import MIMEBase
+ from email import encoders
+
+ if attachments:
+ message = MIMEMultipart()
+ message['From'] = self.sender_email
+ message['To'] = ", ".join(recipient_list)
+ message['Subject'] = subject
+ message.attach(MIMEText(html_content, "html"))
+
+ for file_path in attachments:
+ try:
+ if os.path.exists(file_path):
+ with open(file_path, "rb") as attachment:
+ part = MIMEBase("application", "octet-stream")
+ part.set_payload(attachment.read())
+ encoders.encode_base64(part)
+ filename = os.path.basename(file_path)
+ part.add_header(
+ "Content-Disposition",
+ "attachment; filename= {}".format(filename),
+ )
+ message.attach(part)
+ logger.info("Attached file: {}".format(filename))
+ else:
+ logger.warning("Attachment not found: {}".format(file_path))
+ except Exception as e:
+ logger.error("Failed to attach file {}: {}".format(file_path, str(e)))
+ else:
+ message = MIMEText(html_content, "html")
+ message['From'] = self.sender_email
+ message['To'] = ", ".join(recipient_list)
+ message['Subject'] = subject
+
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
+ server.starttls()
+ server.login(self.smtp_user, self.smtp_password)
+ server.send_message(message)
+
def send_webhook(self, url, payload):
"""
url: Webhook URL