From 933e0621969f2a6bad0822ec3f3e7d631914ac75 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Mon, 30 Mar 2026 12:37:27 +0200 Subject: [PATCH] feat(email): SMTP fallback via OVH Zimbra + direct SMTP for @nordabiznes.pl Domain nordabiznes.pl removed from M365 (moved to OVH Zimbra). Graph API can no longer send as @nordabiznes.pl. - Added _send_via_smtp() method using SMTP_HOST/SMTP_USER/SMTP_PASSWORD - @nordabiznes.pl sender goes directly to SMTP (skips Graph API) - Other domains still try Graph API first, SMTP as fallback - .env updated with OVH Zimbra SMTP credentials Co-Authored-By: Claude Opus 4.6 (1M context) --- email_service.py | 64 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/email_service.py b/email_service.py index 7d326cc..28c2a72 100644 --- a/email_service.py +++ b/email_service.py @@ -107,6 +107,12 @@ class EmailService: Returns: True if sent successfully, False otherwise """ + # If sender is @nordabiznes.pl, go directly to SMTP (domain removed from M365) + sender = from_address or self.mail_from + if sender and '@nordabiznes.pl' in sender: + logger.info(f"Sender is @nordabiznes.pl — using SMTP directly (not Graph API)") + return self._send_via_smtp(subject, body_html or body_text, to, sender_name=sender_name, bcc=bcc) + if not MSAL_AVAILABLE: logger.error("msal package not available - cannot send email") return False @@ -159,7 +165,7 @@ class EmailService: bcc_recipients = [{"emailAddress": {"address": email}} for email in bcc] email_msg["message"]["bccRecipients"] = bcc_recipients - # Send email via Graph API + # Try Graph API first url = f"{self.graph_endpoint}/users/{sender}/sendMail" headers = { "Authorization": f"Bearer {token}", @@ -169,14 +175,62 @@ class EmailService: response = requests.post(url, headers=headers, json=email_msg, timeout=30) if response.status_code == 202: - logger.info(f"Email sent successfully to {to}") + logger.info(f"Email sent successfully via Graph API to {to}") return True else: - logger.error(f"Failed to send email. Status: {response.status_code}, Response: {response.text}") - return False + logger.error(f"Failed to send email via Graph API. Status: {response.status_code}, Response: {response.text}") + # Fallback to SMTP if Graph API fails (e.g. domain removed from M365) + logger.info("Attempting SMTP fallback...") + return self._send_via_smtp(subject, body, to, sender_name=sender_name, bcc=bcc) except Exception as e: - logger.error(f"Exception during email sending: {e}", exc_info=True) + logger.error(f"Exception during Graph email sending: {e}", exc_info=True) + # Fallback to SMTP + logger.info("Attempting SMTP fallback after exception...") + return self._send_via_smtp(subject, body, to, sender_name=sender_name, bcc=bcc) + + def _send_via_smtp(self, subject, body, to, sender_name=None, bcc=None): + """Fallback: send email via SMTP (OVH Zimbra).""" + import smtplib + from email.mime.text import MIMEText + from email.mime.multipart import MIMEMultipart + from email.utils import formataddr + + smtp_host = os.getenv('SMTP_HOST', 'ssl0.ovh.net') + smtp_port = int(os.getenv('SMTP_PORT', '587')) + smtp_user = os.getenv('SMTP_USER', '') + smtp_pass = os.getenv('SMTP_PASSWORD', '') + mail_from = os.getenv('MAIL_FROM', smtp_user) + mail_from_name = sender_name or os.getenv('MAIL_FROM_NAME', 'NordaBiznes Portal') + + if not smtp_user or not smtp_pass: + logger.error("SMTP fallback failed: SMTP_USER or SMTP_PASSWORD not configured") + return False + + try: + msg = MIMEMultipart('alternative') + msg['Subject'] = subject + msg['From'] = formataddr((mail_from_name, mail_from)) + msg['To'] = ', '.join(to) if isinstance(to, list) else to + if bcc: + msg['Bcc'] = ', '.join(bcc) if isinstance(bcc, list) else bcc + + msg.attach(MIMEText(body, 'html', 'utf-8')) + + all_recipients = list(to) if isinstance(to, list) else [to] + if bcc: + all_recipients.extend(bcc if isinstance(bcc, list) else [bcc]) + + with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server: + server.starttls() + server.login(smtp_user, smtp_pass) + server.sendmail(mail_from, all_recipients, msg.as_string()) + + logger.info(f"Email sent successfully via SMTP to {to}") + return True + + except Exception as e: + logger.error(f"SMTP fallback failed: {e}", exc_info=True) return False