refactor(email): remove dead Graph API/Azure/MSAL code — SMTP only
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
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
EmailService now uses SMTP (OVH Zimbra) exclusively. Removed msal/requests imports, MSAL_AVAILABLE guard, _get_access_token(), Azure credential fields (tenant_id, client_id, client_secret, authority, scope, graph_endpoint), Graph API send_mail() body, and Azure-based init_email_service() logic. __init__ now takes only mail_from; init_email_service() checks SMTP_USER only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8c62571099
commit
73abb76c9e
175
email_service.py
175
email_service.py
@ -2,8 +2,7 @@
|
||||
Norda Biznes - Email Service
|
||||
=============================
|
||||
|
||||
Sends emails via Microsoft Graph API using Application permissions.
|
||||
Based on mtbtracker implementation.
|
||||
Sends emails via SMTP (OVH Zimbra).
|
||||
|
||||
Author: Norda Biznes Development Team
|
||||
Created: 2025-12-25
|
||||
@ -19,70 +18,18 @@ load_dotenv()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if msal is available
|
||||
try:
|
||||
import msal
|
||||
import requests
|
||||
MSAL_AVAILABLE = True
|
||||
except ImportError:
|
||||
MSAL_AVAILABLE = False
|
||||
logger.warning("msal package not installed. Email service will be disabled.")
|
||||
|
||||
|
||||
class EmailService:
|
||||
"""Service for sending emails via Microsoft Graph API"""
|
||||
"""Service for sending emails via SMTP"""
|
||||
|
||||
def __init__(self, tenant_id: str, client_id: str, client_secret: str, mail_from: str):
|
||||
def __init__(self, mail_from: str):
|
||||
"""
|
||||
Initialize Email Service
|
||||
|
||||
Args:
|
||||
tenant_id: Azure AD Tenant ID
|
||||
client_id: Application (client) ID
|
||||
client_secret: Client secret value
|
||||
mail_from: Default sender email address (e.g., noreply@inpi.pl)
|
||||
mail_from: Default sender email address (e.g., noreply@nordabiznes.pl)
|
||||
"""
|
||||
self.tenant_id = tenant_id
|
||||
self.client_id = client_id
|
||||
self.client_secret = client_secret
|
||||
self.mail_from = mail_from
|
||||
self.authority = f"https://login.microsoftonline.com/{tenant_id}"
|
||||
self.scope = ["https://graph.microsoft.com/.default"]
|
||||
self.graph_endpoint = "https://graph.microsoft.com/v1.0"
|
||||
|
||||
def _get_access_token(self) -> Optional[str]:
|
||||
"""
|
||||
Acquire access token using client credentials flow
|
||||
|
||||
Returns:
|
||||
Access token string or None if failed
|
||||
"""
|
||||
if not MSAL_AVAILABLE:
|
||||
logger.error("msal package not available")
|
||||
return None
|
||||
|
||||
try:
|
||||
app = msal.ConfidentialClientApplication(
|
||||
self.client_id,
|
||||
authority=self.authority,
|
||||
client_credential=self.client_secret,
|
||||
)
|
||||
|
||||
result = app.acquire_token_silent(self.scope, account=None)
|
||||
|
||||
if not result:
|
||||
logger.info("No token in cache, acquiring new token")
|
||||
result = app.acquire_token_for_client(scopes=self.scope)
|
||||
|
||||
if "access_token" in result:
|
||||
return result["access_token"]
|
||||
else:
|
||||
logger.error(f"Failed to acquire token: {result.get('error')}: {result.get('error_description')}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception during token acquisition: {e}", exc_info=True)
|
||||
return None
|
||||
|
||||
def send_mail(
|
||||
self,
|
||||
@ -94,7 +41,7 @@ class EmailService:
|
||||
bcc: Optional[List[str]] = None
|
||||
) -> bool:
|
||||
"""
|
||||
Send email via Microsoft Graph API
|
||||
Send email via SMTP
|
||||
|
||||
Args:
|
||||
to: List of recipient email addresses
|
||||
@ -107,90 +54,10 @@ class EmailService:
|
||||
Returns:
|
||||
True if sent successfully, False otherwise
|
||||
"""
|
||||
# Primary: use SMTP (OVH Zimbra) for all email delivery
|
||||
smtp_user = os.getenv('SMTP_USER', '')
|
||||
if smtp_user:
|
||||
return self._send_via_smtp(subject, body_html or body_text, to, bcc=bcc)
|
||||
|
||||
# Legacy fallback: Graph API (only if SMTP not configured and Azure creds present)
|
||||
if not MSAL_AVAILABLE:
|
||||
logger.error("msal package not available - cannot send email")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Get access token
|
||||
token = self._get_access_token()
|
||||
if not token:
|
||||
logger.error("Failed to acquire access token")
|
||||
return False
|
||||
|
||||
# Use default sender if not specified
|
||||
sender = from_address or self.mail_from
|
||||
|
||||
# Prepare recipients
|
||||
recipients = [{"emailAddress": {"address": email}} for email in to]
|
||||
|
||||
# Prepare message body
|
||||
if body_html:
|
||||
content_type = "HTML"
|
||||
content = body_html
|
||||
else:
|
||||
content_type = "Text"
|
||||
content = body_text
|
||||
|
||||
# Build email message
|
||||
# Display name for sender - shown in email clients
|
||||
sender_display_name = "Norda Biznes Partner"
|
||||
|
||||
email_msg = {
|
||||
"message": {
|
||||
"subject": subject,
|
||||
"body": {
|
||||
"contentType": content_type,
|
||||
"content": content
|
||||
},
|
||||
"toRecipients": recipients,
|
||||
"from": {
|
||||
"emailAddress": {
|
||||
"address": sender,
|
||||
"name": sender_display_name
|
||||
}
|
||||
}
|
||||
},
|
||||
"saveToSentItems": "false"
|
||||
}
|
||||
|
||||
# Add BCC recipients if provided
|
||||
if bcc:
|
||||
bcc_recipients = [{"emailAddress": {"address": email}} for email in bcc]
|
||||
email_msg["message"]["bccRecipients"] = bcc_recipients
|
||||
|
||||
# Try Graph API first
|
||||
url = f"{self.graph_endpoint}/users/{sender}/sendMail"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=email_msg, timeout=30)
|
||||
|
||||
if response.status_code == 202:
|
||||
logger.info(f"Email sent successfully via Graph API to {to}")
|
||||
return True
|
||||
else:
|
||||
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, bcc=bcc)
|
||||
|
||||
except Exception as e:
|
||||
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, bcc=bcc)
|
||||
return self._send_via_smtp(subject, body_html or body_text, to, bcc=bcc)
|
||||
|
||||
def _send_via_smtp(self, subject, body, to, sender_name=None, bcc=None):
|
||||
"""Fallback: send email via SMTP (OVH Zimbra)."""
|
||||
"""Send email via SMTP (OVH Zimbra)."""
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
@ -204,7 +71,7 @@ class EmailService:
|
||||
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")
|
||||
logger.error("SMTP failed: SMTP_USER or SMTP_PASSWORD not configured")
|
||||
return False
|
||||
|
||||
try:
|
||||
@ -230,7 +97,7 @@ class EmailService:
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"SMTP fallback failed: {e}", exc_info=True)
|
||||
logger.error(f"SMTP send failed: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
@ -243,29 +110,21 @@ def init_email_service():
|
||||
Initialize the global Email Service instance from environment variables
|
||||
|
||||
Required env vars:
|
||||
AZURE_TENANT_ID: Azure AD Tenant ID
|
||||
AZURE_CLIENT_ID: Application (client) ID
|
||||
AZURE_CLIENT_SECRET: Client secret value
|
||||
SMTP_USER: SMTP username
|
||||
SMTP_PASSWORD: SMTP password
|
||||
MAIL_FROM: Default sender email address
|
||||
"""
|
||||
global _email_service
|
||||
|
||||
tenant_id = os.getenv('AZURE_TENANT_ID')
|
||||
client_id = os.getenv('AZURE_CLIENT_ID')
|
||||
client_secret = os.getenv('AZURE_CLIENT_SECRET')
|
||||
mail_from = os.getenv('MAIL_FROM', 'noreply@inpi.pl')
|
||||
mail_from = os.getenv('MAIL_FROM', 'noreply@nordabiznes.pl')
|
||||
smtp_user = os.getenv('SMTP_USER')
|
||||
|
||||
if tenant_id and client_id and client_secret:
|
||||
_email_service = EmailService(tenant_id, client_id, client_secret, mail_from)
|
||||
logger.info(f"Email Service initialized with Graph API, sender: {mail_from}")
|
||||
return True
|
||||
elif os.getenv('SMTP_USER') and os.getenv('SMTP_PASSWORD'):
|
||||
# SMTP-only mode (no Azure) — create service with dummy Azure creds
|
||||
_email_service = EmailService('dummy', 'dummy', 'dummy', mail_from)
|
||||
logger.info(f"Email Service initialized with SMTP only, sender: {mail_from}")
|
||||
if smtp_user:
|
||||
_email_service = EmailService(mail_from)
|
||||
logger.info(f"Email Service initialized (SMTP), sender: {mail_from}")
|
||||
return True
|
||||
else:
|
||||
logger.warning("Email Service not configured - missing Azure or SMTP credentials")
|
||||
logger.warning("Email Service not configured - missing SMTP_USER")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user