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
Każdy e-mail powiadomieniowy ma teraz:
(1) link w stopce "Wyłącz ten typ powiadomień jednym kliknięciem"
(2) nagłówki List-Unsubscribe + List-Unsubscribe-Post dla klientów
pocztowych (Gmail/Apple Mail pokażą natywny przycisk Unsubscribe)
Implementacja:
- utils/unsubscribe_tokens.py: signed token (itsdangerous, SECRET_KEY)
niosący user_id + notification_type, bez wygasania
- blueprints/unsubscribe: GET /unsubscribe?t=TOKEN → strona potwierdzenia,
POST /unsubscribe → faktyczne wyłączenie flagi notify_email_<type>
- email_service.send_email() dostał parametr notification_type. Jeśli
przekazany razem z user_id, footer + headery są doklejane
- Aktualizowane wywołania: message_notification (messages),
classified_question/answer (B2B Q&A), classified_expiry (skrypt cron)
Prefetch safety: GET pokazuje stronę z przyciskiem "Tak, wyłącz",
wyłączenie następuje po POST. RFC 8058 One-Click (POST bez formularza
z Content-Type application/x-www-form-urlencoded + body
"List-Unsubscribe=One-Click") obsługuje klientów pocztowych.
D.2/D.3 dorzucą kolejne notification_type (forum, broadcast, events).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
591 lines
24 KiB
Python
591 lines
24 KiB
Python
"""
|
|
Norda Biznes - Email Service
|
|
=============================
|
|
|
|
Sends emails via SMTP (OVH Zimbra).
|
|
|
|
Author: Maciej Pienczyn, InPi sp. z o.o.
|
|
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__)
|
|
|
|
|
|
class EmailService:
|
|
"""Service for sending emails via SMTP"""
|
|
|
|
def __init__(self, mail_from: str):
|
|
"""
|
|
Initialize Email Service
|
|
|
|
Args:
|
|
mail_from: Default sender email address (e.g., noreply@nordabiznes.pl)
|
|
"""
|
|
self.mail_from = mail_from
|
|
|
|
def send_mail(
|
|
self,
|
|
to: List[str],
|
|
subject: str,
|
|
body_text: str,
|
|
body_html: Optional[str] = None,
|
|
from_address: Optional[str] = None,
|
|
bcc: Optional[List[str]] = None,
|
|
extra_headers: Optional[dict] = None,
|
|
) -> bool:
|
|
return self._send_via_smtp(subject, body_html or body_text, to, bcc=bcc, extra_headers=extra_headers)
|
|
|
|
def _send_via_smtp(self, subject, body, to, sender_name=None, bcc=None, extra_headers=None):
|
|
"""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 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
|
|
if extra_headers:
|
|
for k, v in extra_headers.items():
|
|
msg[k] = v
|
|
|
|
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 send failed: {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:
|
|
SMTP_USER: SMTP username
|
|
SMTP_PASSWORD: SMTP password
|
|
MAIL_FROM: Default sender email address
|
|
"""
|
|
global _email_service
|
|
|
|
mail_from = os.getenv('MAIL_FROM', 'noreply@nordabiznes.pl')
|
|
smtp_user = os.getenv('SMTP_USER')
|
|
|
|
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 SMTP_USER")
|
|
return False
|
|
|
|
|
|
PUBLIC_BASE_URL = os.getenv('PUBLIC_BASE_URL', 'https://nordabiznes.pl').rstrip('/')
|
|
|
|
|
|
def _build_unsubscribe_footer(notification_type: str, user_id: int):
|
|
"""Return (footer_html, footer_text, unsubscribe_url) or (None, None, None)."""
|
|
try:
|
|
from utils.unsubscribe_tokens import generate_unsubscribe_token, NOTIFICATION_TYPE_TO_COLUMN
|
|
if notification_type not in NOTIFICATION_TYPE_TO_COLUMN:
|
|
return None, None, None
|
|
token = generate_unsubscribe_token(user_id, notification_type)
|
|
url = f"{PUBLIC_BASE_URL}/unsubscribe?t={token}"
|
|
footer_html = (
|
|
'<hr style="border:none;border-top:1px solid #e2e8f0;margin:28px 0 16px">'
|
|
'<p style="color:#94a3b8;font-size:12px;line-height:1.5;margin:0">'
|
|
f'Nie chcesz otrzymywać takich e-maili? '
|
|
f'<a href="{url}" style="color:#64748b;text-decoration:underline">'
|
|
f'Wyłącz ten typ powiadomień jednym kliknięciem</a>.'
|
|
f' Preferencje dla wszystkich typów: '
|
|
f'<a href="{PUBLIC_BASE_URL}/konto/prywatnosc" style="color:#64748b;text-decoration:underline">'
|
|
f'panel konta</a>.'
|
|
'</p>'
|
|
)
|
|
footer_text = (
|
|
"\n\n---\n"
|
|
f"Aby wyłączyć ten typ powiadomień e-mail, otwórz: {url}\n"
|
|
f"Pełne preferencje: {PUBLIC_BASE_URL}/konto/prywatnosc\n"
|
|
)
|
|
return footer_html, footer_text, url
|
|
except Exception as e:
|
|
logger.debug("unsubscribe footer build failed: %s", e)
|
|
return None, None, None
|
|
|
|
|
|
def send_email(
|
|
to: List[str],
|
|
subject: str,
|
|
body_text: str,
|
|
body_html: Optional[str] = None,
|
|
from_address: Optional[str] = None,
|
|
email_type: str = 'notification',
|
|
user_id: Optional[int] = None,
|
|
recipient_name: Optional[str] = None,
|
|
bcc: Optional[List[str]] = None,
|
|
notification_type: 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)
|
|
email_type: Type of email for logging (welcome, password_reset, notification)
|
|
user_id: User ID for logging (optional)
|
|
recipient_name: Recipient name for logging (optional)
|
|
bcc: List of BCC emails (optional, defaults to MAIL_BCC env var)
|
|
|
|
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]
|
|
|
|
# Default BCC from env var (empty by default — no admin copies)
|
|
if bcc is None:
|
|
default_bcc = os.getenv('MAIL_BCC', '')
|
|
if default_bcc:
|
|
bcc = [addr.strip() for addr in default_bcc.split(',') if addr.strip()]
|
|
# Don't BCC someone who is already a direct recipient
|
|
bcc = [addr for addr in bcc if addr not in to]
|
|
|
|
# Unsubscribe footer + RFC 8058 List-Unsubscribe headers (dla powiadomień user-facing)
|
|
extra_headers = None
|
|
if notification_type and user_id:
|
|
html_footer, text_footer, unsub_url = _build_unsubscribe_footer(notification_type, user_id)
|
|
if unsub_url:
|
|
if body_html:
|
|
# Dodaj przed zamykającym </body> lub po prostu doklejaj
|
|
if '</body>' in body_html.lower():
|
|
idx = body_html.lower().rfind('</body>')
|
|
body_html = body_html[:idx] + html_footer + body_html[idx:]
|
|
else:
|
|
body_html = body_html + html_footer
|
|
if body_text:
|
|
body_text = body_text + text_footer
|
|
extra_headers = {
|
|
'List-Unsubscribe': f'<{unsub_url}>',
|
|
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
|
|
}
|
|
|
|
result = _email_service.send_mail(to, subject, body_text, body_html, from_address,
|
|
bcc=bcc, extra_headers=extra_headers)
|
|
|
|
# Log email to database
|
|
_log_email(
|
|
email_type=email_type,
|
|
recipient_emails=to,
|
|
recipient_name=recipient_name,
|
|
subject=subject,
|
|
user_id=user_id,
|
|
sender_email=from_address or _email_service.mail_from if _email_service else None,
|
|
success=result
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def _log_email(
|
|
email_type: str,
|
|
recipient_emails: List[str],
|
|
subject: str,
|
|
success: bool,
|
|
recipient_name: Optional[str] = None,
|
|
user_id: Optional[int] = None,
|
|
sender_email: Optional[str] = None,
|
|
error_message: Optional[str] = None
|
|
) -> None:
|
|
"""
|
|
Log email to database for monitoring.
|
|
|
|
Args:
|
|
email_type: Type of email (welcome, password_reset, notification)
|
|
recipient_emails: List of recipient email addresses
|
|
subject: Email subject
|
|
success: Whether email was sent successfully
|
|
recipient_name: Recipient name (optional)
|
|
user_id: User ID (optional)
|
|
sender_email: Sender email address (optional)
|
|
error_message: Error message if failed (optional)
|
|
"""
|
|
try:
|
|
from database import SessionLocal, EmailLog
|
|
from datetime import datetime
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
for email in recipient_emails:
|
|
log_entry = EmailLog(
|
|
email_type=email_type,
|
|
recipient_email=email,
|
|
recipient_name=recipient_name,
|
|
subject=subject,
|
|
user_id=user_id,
|
|
sender_email=sender_email,
|
|
status='sent' if success else 'failed',
|
|
sent_at=datetime.utcnow() if success else None,
|
|
error_message=error_message if not success else None
|
|
)
|
|
db.add(log_entry)
|
|
db.commit()
|
|
logger.info(f"Email logged: {email_type} -> {recipient_emails} (status: {'sent' if success else 'failed'})")
|
|
except Exception as e:
|
|
db.rollback()
|
|
logger.error(f"Failed to log email to database: {e}")
|
|
finally:
|
|
db.close()
|
|
except ImportError:
|
|
# Database not available (e.g., during testing)
|
|
logger.warning("Could not log email - database module not available")
|
|
|
|
|
|
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 (v3 design)
|
|
# ============================================================
|
|
|
|
def _email_v3_wrap(title: str, subtitle: str, content_html: str) -> str:
|
|
"""Wrap email content in v3 branded shell (header + footer)."""
|
|
return f'''<!DOCTYPE html>
|
|
<html lang="pl">
|
|
<head><meta charset="UTF-8"></head>
|
|
<body style="margin:0; padding:0; background:#f1f5f9; font-family: 'Inter', Arial, sans-serif;">
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f1f5f9; padding: 32px 0;">
|
|
<tr><td align="center">
|
|
<table width="600" cellpadding="0" cellspacing="0" style="background:#ffffff; border-radius:12px; overflow:hidden; box-shadow: 0 4px 24px rgba(0,0,0,0.08);">
|
|
|
|
<tr><td style="background-color:#1e3a8a; padding: 36px 32px; text-align: center;">
|
|
<img src="https://nordabiznes.pl/static/img/logo-email.png" width="64" height="64" alt="NB" style="border-radius:50%; margin-bottom:16px; border: 2px solid rgba(255,255,255,0.3);">
|
|
<h1 style="margin:0; color:#ffffff; font-size:24px; font-weight:700;">{title}</h1>
|
|
<p style="margin:8px 0 0; color:#93c5fd; font-size:14px;">{subtitle}</p>
|
|
</td></tr>
|
|
|
|
<tr><td style="padding: 32px;">
|
|
{content_html}
|
|
</td></tr>
|
|
|
|
<tr><td style="background:#f8fafc; padding: 24px; text-align:center; border-top: 1px solid #e2e8f0;">
|
|
<p style="margin:0 0 4px; color:#1e293b; font-size:14px; font-weight:600;">Norda Biznes Partner</p>
|
|
<p style="margin:0 0 2px; color:#94a3b8; font-size:12px;">Stowarzyszenie Norda Biznes</p>
|
|
<p style="margin:0 0 12px; color:#94a3b8; font-size:12px;">ul. 12 Marca 238/5, 84-200 Wejherowo</p>
|
|
<p style="margin:0 0 12px; color:#94a3b8; font-size:13px;">
|
|
<a href="https://nordabiznes.pl" style="color:#2563eb; text-decoration:none;">nordabiznes.pl</a>
|
|
|
|
|
<a href="https://www.facebook.com/profile.php?id=100057396041901" style="color:#2563eb; text-decoration:none;">Facebook</a>
|
|
</p>
|
|
<p style="margin:0; color:#cbd5e1; font-size:11px;">To powiadomienie zostało wysłane automatycznie.</p>
|
|
</td></tr>
|
|
|
|
</table>
|
|
</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>'''
|
|
|
|
|
|
def build_message_notification_email(sender_name: str, subject: str, content_preview: str, message_url: str, settings_url: str) -> tuple:
|
|
"""Build branded email for message notification. Returns (html, text)."""
|
|
subject_html = f'<p style="margin:0 0 16px; color:#475569; font-size:15px;"><strong>Temat:</strong> {subject}</p>' if subject else ''
|
|
|
|
content_html = f'''
|
|
<p style="margin:0 0 20px; color:#1e293b; font-size:16px;">
|
|
<strong>{sender_name}</strong> wysłał(a) Ci wiadomość na portalu Norda Biznes.
|
|
</p>
|
|
|
|
{subject_html}
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-left:4px solid #3b82f6; border-radius:4px; margin-bottom:24px;">
|
|
<tr><td style="padding:16px;">
|
|
<p style="margin:0; color:#374151; font-size:14px;">{content_preview}</p>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
|
|
<tr><td align="center" style="padding:8px 0;">
|
|
<a href="{message_url}" style="display:inline-block; padding:14px 36px; background-color:#3b82f6; color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Przeczytaj wiadomość</a>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<p style="margin:0; color:#94a3b8; font-size:12px; text-align:center;">
|
|
Możesz wyłączyć powiadomienia e-mail w <a href="{settings_url}" style="color:#2563eb;">ustawieniach prywatności</a>.
|
|
</p>
|
|
'''
|
|
|
|
html = _email_v3_wrap('Nowa wiadomość', f'od {sender_name}', content_html)
|
|
|
|
text = f'{sender_name} wysłał(a) Ci wiadomość na portalu Norda Biznes. Odczytaj: {message_url}'
|
|
|
|
return html, text
|
|
|
|
|
|
def send_password_reset_email(email: str, reset_url: str, admin_initiated: bool = False) -> bool:
|
|
"""Send password reset email."""
|
|
subject = "Reset hasła - Norda Biznes Partner"
|
|
|
|
if admin_initiated:
|
|
intro_text = "Administrator portalu Norda Biznes Partner wygenerował dla Ciebie link do ustawienia hasła."
|
|
intro_html = "Administrator portalu wygenerował dla Ciebie link do ustawienia nowego hasła."
|
|
validity = "24 godziny"
|
|
else:
|
|
intro_text = "Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta Norda Biznes Partner."
|
|
intro_html = "Otrzymałeś ten email, ponieważ zażądano resetowania hasła dla Twojego konta."
|
|
validity = "1 godzinę"
|
|
|
|
body_text = f"""{intro_text}
|
|
|
|
Aby ustawić hasło, kliknij w poniższy link:
|
|
{reset_url}
|
|
|
|
Link będzie ważny przez {validity}.
|
|
|
|
Jeśli nie spodziewałeś się tego emaila, skontaktuj się z administratorem.
|
|
|
|
---
|
|
Norda Biznes Partner
|
|
https://nordabiznes.pl
|
|
"""
|
|
|
|
content = f'''
|
|
<p style="margin:0 0 20px; color:#1e293b; font-size:16px;">{intro_html}</p>
|
|
|
|
<p style="margin:0 0 24px; color:#475569; font-size:15px;">Kliknij poniższy przycisk, aby ustawić nowe hasło:</p>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
|
|
<tr><td align="center" style="padding: 8px 0;">
|
|
<a href="{reset_url}" style="display:inline-block; padding:16px 40px; background-color:#dc2626; color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Zresetuj hasło</a>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<!-- Warning -->
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d; margin-bottom:24px;">
|
|
<tr><td style="padding: 16px;">
|
|
<p style="margin:0 0 4px; color:#92400e; font-size:14px; font-weight:600;">Ważność linku: {validity}</p>
|
|
<p style="margin:0; color:#92400e; font-size:13px;">Po tym czasie konieczne będzie ponowne żądanie resetu hasła.</p>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<p style="margin:0 0 8px; color:#64748b; font-size:13px;">Jeśli przycisk nie działa, skopiuj i wklej ten link:</p>
|
|
<p style="margin:0 0 24px; color:#2563eb; font-size:13px; word-break:break-all;">{reset_url}</p>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fafc; border-radius:8px; border: 1px solid #e2e8f0;">
|
|
<tr><td style="padding: 16px;">
|
|
<p style="margin:0; color:#64748b; font-size:14px;">
|
|
<strong>Nie zażądałeś resetowania hasła?</strong><br>
|
|
Zignoruj ten email — Twoje hasło pozostanie bez zmian.
|
|
</p>
|
|
</td></tr>
|
|
</table>'''
|
|
|
|
body_html = _email_v3_wrap('Reset hasła', 'Norda Biznes Partner', content)
|
|
|
|
return send_email(
|
|
to=[email],
|
|
subject=subject,
|
|
body_text=body_text,
|
|
body_html=body_html,
|
|
email_type='password_reset'
|
|
)
|
|
|
|
|
|
def send_welcome_activation_email(email: str, name: str, reset_url: str) -> bool:
|
|
"""Send welcome activation email to users who never received credentials."""
|
|
subject = "Witamy w Norda Biznes Partner — Ustaw hasło do konta"
|
|
|
|
body_text = f"""Witaj {name}!
|
|
|
|
Twoje konto w portalu Norda Biznes Partner jest gotowe.
|
|
|
|
Aby ustawić hasło i zalogować się, kliknij w poniższy link:
|
|
{reset_url}
|
|
|
|
Link jest ważny przez 72 godziny. Jeśli wygaśnie, wejdź na nordabiznes.pl/login
|
|
i kliknij "Nie pamiętasz hasła?" — otrzymasz nowy link.
|
|
|
|
Co znajdziesz w portalu:
|
|
- Katalog firm członkowskich Izby
|
|
- Forum dyskusyjne dla członków
|
|
- Wiadomości prywatne i networking
|
|
- Asystent AI do wyszukiwania usług
|
|
|
|
Pozdrawiamy,
|
|
Zespół Norda Biznes Partner
|
|
https://nordabiznes.pl
|
|
"""
|
|
|
|
check = (
|
|
'<div style="width:28px; height:28px; background:#16a34a; border-radius:50%; '
|
|
'text-align:center; line-height:28px; display:inline-block; margin-right:10px;">'
|
|
'<span style="color:#fff; font-size:16px; font-weight:bold;">✓</span></div>'
|
|
)
|
|
|
|
content = f'''
|
|
<p style="margin:0 0 16px; color:#1e293b; font-size:16px;">Witaj <strong>{name}</strong>!</p>
|
|
<p style="margin:0 0 28px; color:#475569; font-size:15px; line-height:1.5;">Twoje konto w portalu <strong>Norda Biznes Partner</strong> jest gotowe. Kliknij poniższy przycisk, aby ustawić hasło i zalogować się po raz pierwszy.</p>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:28px;">
|
|
<tr><td align="center" style="padding: 8px 0;">
|
|
<a href="{reset_url}" style="display:inline-block; padding:16px 40px; background-color:#16a34a; color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Ustaw hasło i zaloguj się</a>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d; margin-bottom:28px;">
|
|
<tr><td style="padding: 14px 16px;">
|
|
<p style="margin:0; color:#92400e; font-size:13px;">Link jest ważny przez <strong>72 godziny</strong>. Jeśli wygaśnie, wejdź na <a href="https://nordabiznes.pl/login" style="color:#92400e; font-weight:600;">nordabiznes.pl/login</a> i kliknij <strong>„Nie pamiętasz hasła?"</strong> — otrzymasz nowy link.</p>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<p style="margin:0 0 14px; color:#1e293b; font-size:16px; font-weight:600;">Co znajdziesz w portalu:</p>
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fdf8; border-radius:10px; border: 1px solid #d1fae5; margin-bottom:28px;">
|
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Katalog firm członkowskich Izby</span>
|
|
</td></tr>
|
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Forum dyskusyjne dla członków</span>
|
|
</td></tr>
|
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Wiadomości prywatne i networking</span>
|
|
</td></tr>
|
|
<tr><td style="padding: 14px 16px;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Asystent AI do wyszukiwania usług</span>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<p style="margin:0 0 8px; color:#64748b; font-size:13px;">Jeśli przycisk nie działa, skopiuj i wklej ten link:</p>
|
|
<p style="margin:0; color:#2563eb; font-size:13px; word-break:break-all;">{reset_url}</p>'''
|
|
|
|
body_html = _email_v3_wrap('Witamy w portalu!', 'Norda Biznes Partner', content)
|
|
|
|
return send_email(
|
|
to=[email],
|
|
subject=subject,
|
|
body_text=body_text,
|
|
body_html=body_html,
|
|
email_type='welcome_activation',
|
|
recipient_name=name
|
|
)
|
|
|
|
|
|
def send_welcome_email(email: str, name: str, verification_url: str) -> bool:
|
|
"""Send welcome/verification email after registration."""
|
|
subject = "Witamy w Norda Biznes Partner — Potwierdź email"
|
|
|
|
body_text = f"""Witaj {name}!
|
|
|
|
Dziękujemy za rejestrację w Norda Biznes Partner.
|
|
|
|
Aby aktywować konto, kliknij: {verification_url}
|
|
Link ważny 24 godziny.
|
|
|
|
Pozdrawiamy,
|
|
Zespół Norda Biznes Partner
|
|
https://nordabiznes.pl
|
|
"""
|
|
|
|
check = (
|
|
'<div style="width:28px; height:28px; background:#16a34a; border-radius:50%; '
|
|
'text-align:center; line-height:28px; display:inline-block; margin-right:10px;">'
|
|
'<span style="color:#fff; font-size:16px; font-weight:bold;">✓</span></div>'
|
|
)
|
|
|
|
content = f'''
|
|
<p style="margin:0 0 16px; color:#1e293b; font-size:16px;">Witaj <strong>{name}</strong>!</p>
|
|
<p style="margin:0 0 28px; color:#475569; font-size:15px; line-height:1.5;">Dziękujemy za rejestrację w Norda Biznes Partner — platformie networkingu Regionalnej Izby Przedsiębiorców.</p>
|
|
|
|
<p style="margin:0 0 16px; color:#1e293b; font-size:15px;">Aby aktywować konto, potwierdź swój adres email:</p>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:28px;">
|
|
<tr><td align="center" style="padding: 8px 0;">
|
|
<a href="{verification_url}" style="display:inline-block; padding:16px 40px; background-color:#16a34a; color:#ffffff; text-decoration:none; border-radius:8px; font-size:15px; font-weight:600;">Potwierdź email</a>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<p style="margin:0 0 14px; color:#1e293b; font-size:16px; font-weight:600;">Co zyskujesz:</p>
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#f8fdf8; border-radius:10px; border: 1px solid #d1fae5; margin-bottom:28px;">
|
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Katalog firm członkowskich Izby</span>
|
|
</td></tr>
|
|
<tr><td style="padding: 14px 16px; border-bottom: 1px solid #e2e8f0;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Asystent AI do wyszukiwania usług</span>
|
|
</td></tr>
|
|
<tr><td style="padding: 14px 16px;">
|
|
{check}<span style="color:#1e293b; font-size:15px; font-weight:500;">Networking i kontakty biznesowe</span>
|
|
</td></tr>
|
|
</table>
|
|
|
|
<table width="100%" cellpadding="0" cellspacing="0" style="background:#fef3c7; border-radius:8px; border: 1px solid #fcd34d;">
|
|
<tr><td style="padding: 14px 16px;">
|
|
<p style="margin:0; color:#92400e; font-size:13px;">Link aktywacyjny jest ważny przez <strong>24 godziny</strong>.</p>
|
|
</td></tr>
|
|
</table>'''
|
|
|
|
body_html = _email_v3_wrap('Witamy!', 'Norda Biznes Partner', content)
|
|
|
|
return send_email(
|
|
to=[email],
|
|
subject=subject,
|
|
body_text=body_text,
|
|
body_html=body_html,
|
|
email_type='welcome',
|
|
recipient_name=name
|
|
)
|