feat(email): SMTP fallback via OVH Zimbra + direct SMTP for @nordabiznes.pl
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

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) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-30 12:37:27 +02:00
parent cb1409bf09
commit 933e062196

View File

@ -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