Add Mailgun API support for PROD email notifications
Mailgun API is used when MAILGUN_API_KEY and MAILGUN_DOMAIN are set, with SMTP as fallback for PPR. Also fixes A2→A3 batch subject line that was rendering Jinja2 syntax literally instead of substituting values. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
96b33fa084
commit
dc779724fc
2 changed files with 101 additions and 55 deletions
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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': """
|
||||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||||
<div style="background-color: {% if failed_count == 0 %}#28a745{% else %}#ff9800{% endif %}; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue