nordabiz/email_service.py
2026-01-01 14:01:49 +01:00

421 lines
13 KiB
Python

"""
Norda Biznes - Email Service
=============================
Sends emails via Microsoft Graph API using Application permissions.
Based on mtbtracker implementation.
Author: Norda Biznes Development Team
Created: 2025-12-25
"""
import os
import logging
from typing import Optional, List
from dotenv import load_dotenv
# Load .env file for environment variables
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"""
def __init__(self, tenant_id: str, client_id: str, client_secret: str, 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)
"""
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,
to: List[str],
subject: str,
body_text: str,
body_html: Optional[str] = None,
from_address: Optional[str] = None
) -> bool:
"""
Send email via Microsoft Graph API
Args:
to: List of recipient email addresses
subject: Email subject
body_text: Plain text email body
body_html: HTML email body (optional)
from_address: Sender email (optional, defaults to configured mail_from)
Returns:
True if sent successfully, False otherwise
"""
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
email_msg = {
"message": {
"subject": subject,
"body": {
"contentType": content_type,
"content": content
},
"toRecipients": recipients,
"from": {
"emailAddress": {
"address": sender
}
}
},
"saveToSentItems": "false"
}
# Send email via Graph API
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 to {to}")
return True
else:
logger.error(f"Failed to send email. Status: {response.status_code}, Response: {response.text}")
return False
except Exception as e:
logger.error(f"Exception during email sending: {e}", exc_info=True)
return False
# Global service instance
_email_service: Optional[EmailService] = None
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
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')
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 sender: {mail_from}")
return True
else:
logger.warning("Email Service not configured - missing Azure credentials")
return False
def send_email(
to: List[str],
subject: str,
body_text: str,
body_html: Optional[str] = None,
from_address: Optional[str] = None
) -> bool:
"""
Send email using the global Email Service instance
Args:
to: List of recipient email addresses (can be single email as list)
subject: Email subject
body_text: Plain text email body
body_html: HTML email body (optional)
from_address: Sender email (optional)
Returns:
True if sent successfully, False otherwise
"""
if not _email_service:
logger.error("Email Service not initialized. Call init_email_service() first.")
return False
# Convert single email to list if needed
if isinstance(to, str):
to = [to]
return _email_service.send_mail(to, subject, body_text, body_html, from_address)
def is_configured() -> bool:
"""
Check if Email Service is configured and ready
Returns:
True if service is initialized, False otherwise
"""
return _email_service is not None
# Auto-initialize on module load
init_email_service()
# ============================================================
# EMAIL TEMPLATES
# ============================================================
def send_password_reset_email(email: str, reset_url: str) -> bool:
"""
Send password reset email
Args:
email: Recipient email address
reset_url: Password reset URL with token
Returns:
True if sent successfully, False otherwise
"""
subject = "Reset hasla - Norda Biznes Hub"
body_text = f"""Otrzymales ten email, poniewaz zazadano resetowania hasla dla Twojego konta Norda Biznes Hub.
Aby zresetowac haslo, kliknij w ponizszy link:
{reset_url}
Link bedzie wazny przez 1 godzine.
Jesli nie zazadales resetowania hasla, zignoruj ten email.
---
Norda Biznes Hub - Katalog Firm Czlonkowskich
https://nordabiznes.pl
"""
body_html = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }}
.header {{ background: linear-gradient(135deg, #2563eb, #1e40af); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; padding: 14px 32px; background: #2563eb; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }}
.button:hover {{ background: #1e40af; }}
.footer {{ text-align: center; padding: 20px; color: #64748b; font-size: 0.9em; }}
.warning {{ background: #fef3c7; border-left: 4px solid #f59e0b; padding: 15px; margin: 20px 0; border-radius: 0 8px 8px 0; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Reset hasla</h1>
</div>
<div class="content">
<p>Otrzymales ten email, poniewaz zazadano resetowania hasla dla Twojego konta Norda Biznes Hub.</p>
<p>Aby zresetowac haslo, kliknij w ponizszy przycisk:</p>
<center>
<a href="{reset_url}" class="button">Zresetuj haslo</a>
</center>
<div class="warning">
<strong>Waznosc linku:</strong> 1 godzina
</div>
<p>Jesli przycisk nie dziala, skopiuj i wklej ponizszy link do przegladarki:</p>
<p style="word-break: break-all; color: #2563eb;">{reset_url}</p>
<p style="margin-top: 30px; color: #64748b; font-size: 0.9em;">
<strong>Nie zazadales resetowania hasla?</strong><br>
Zignoruj ten email. Twoje haslo pozostanie bez zmian.
</p>
</div>
<div class="footer">
<p><strong>Norda Biznes Hub</strong> - Katalog Firm Czlonkowskich</p>
<p><a href="https://nordabiznes.pl">nordabiznes.pl</a></p>
</div>
</div>
</body>
</html>
"""
return send_email([email], subject, body_text, body_html)
def send_welcome_email(email: str, name: str, verification_url: str) -> bool:
"""
Send welcome/verification email after registration
Args:
email: Recipient email address
name: User's name
verification_url: Email verification URL with token
Returns:
True if sent successfully, False otherwise
"""
subject = "Witamy w Norda Biznes Hub - Potwierdz email"
body_text = f"""Witaj {name}!
Dziekujemy za rejestracje w Norda Biznes Hub - platformie sieciowej dla czlonkow stowarzyszenia Norda Biznes.
Aby aktywowac swoje konto, potwierdz adres email klikajac w ponizszy link:
{verification_url}
Link bedzie wazny przez 24 godziny.
Po potwierdzeniu email bedziesz mogl:
- Przegladac katalog 80 firm czlonkowskich
- Korzystac z asystenta AI do wyszukiwania uslug
- Nawiazywac kontakty biznesowe
Pozdrawiamy,
Zespol Norda Biznes Hub
---
Norda Biznes Hub - Katalog Firm Czlonkowskich
https://nordabiznes.pl
"""
body_html = f"""
<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: 'Inter', Arial, sans-serif; line-height: 1.6; color: #1e293b; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; background: #f8fafc; }}
.header {{ background: linear-gradient(135deg, #2563eb, #1e40af); color: white; padding: 30px; text-align: center; border-radius: 8px 8px 0 0; }}
.content {{ background: white; padding: 30px; border-radius: 0 0 8px 8px; }}
.button {{ display: inline-block; padding: 14px 32px; background: #10b981; color: white; text-decoration: none; border-radius: 8px; margin: 20px 0; font-weight: 600; }}
.footer {{ text-align: center; padding: 20px; color: #64748b; font-size: 0.9em; }}
.features {{ background: #f0fdf4; padding: 20px; border-radius: 8px; margin: 20px 0; }}
.features ul {{ margin: 10px 0; padding-left: 20px; }}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Witamy w Norda Biznes Hub!</h1>
</div>
<div class="content">
<p>Witaj <strong>{name}</strong>!</p>
<p>Dziekujemy za rejestracje w Norda Biznes Hub - platformie sieciowej dla czlonkow stowarzyszenia Norda Biznes.</p>
<p>Aby aktywowac swoje konto, potwierdz adres email:</p>
<center>
<a href="{verification_url}" class="button">Potwierdz email</a>
</center>
<div class="features">
<strong>Po potwierdzeniu email bedziesz mogl:</strong>
<ul>
<li>Przegladac katalog 80 firm czlonkowskich</li>
<li>Korzystac z asystenta AI do wyszukiwania uslug</li>
<li>Nawiazywac kontakty biznesowe</li>
</ul>
</div>
<p style="color: #64748b; font-size: 0.9em;">
Link bedzie wazny przez 24 godziny.
</p>
</div>
<div class="footer">
<p><strong>Norda Biznes Hub</strong> - Katalog Firm Czlonkowskich</p>
<p><a href="https://nordabiznes.pl">nordabiznes.pl</a></p>
</div>
</div>
</body>
</html>
"""
return send_email([email], subject, body_text, body_html)