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