Some checks failed
CI/CD Pipeline / test (push) Failing after 4m9s
- Added email_service.py for sending emails with SMTP configuration. - Introduced email_templates.py to render job alert email subjects and bodies. - Enhanced scraper.py to extract contact information from job listings. - Updated settings.js to handle negative keyword input validation. - Created email.html and email_templates.html for managing email subscriptions and templates in the admin interface. - Modified base.html to include links for email alerts and templates. - Expanded user settings.html to allow management of negative keywords. - Updated utils.py to include functions for retrieving negative keywords and email settings. - Enhanced job filtering logic to exclude jobs containing negative keywords.
131 lines
3.9 KiB
Python
131 lines
3.9 KiB
Python
"""Email sending utilities for the jobs scraper."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from email.message import EmailMessage
|
|
from typing import Iterable, Sequence
|
|
import smtplib
|
|
|
|
from web.utils import get_email_settings
|
|
|
|
|
|
class EmailConfigurationError(RuntimeError):
|
|
"""Raised when email settings are missing or invalid."""
|
|
|
|
|
|
class EmailDeliveryError(RuntimeError):
|
|
"""Raised when an email fails to send."""
|
|
|
|
|
|
def _normalize_addresses(addresses: Sequence[str] | str | None) -> list[str]:
|
|
if not addresses:
|
|
return []
|
|
if isinstance(addresses, str):
|
|
items = [addresses]
|
|
else:
|
|
items = list(addresses)
|
|
cleaned: list[str] = []
|
|
seen: set[str] = set()
|
|
for raw in items:
|
|
if not isinstance(raw, str):
|
|
continue
|
|
addr = raw.strip()
|
|
if not addr:
|
|
continue
|
|
lower = addr.lower()
|
|
if lower in seen:
|
|
continue
|
|
seen.add(lower)
|
|
cleaned.append(addr)
|
|
return cleaned
|
|
|
|
|
|
def _ensure_recipients(*recipient_groups: Iterable[str]) -> list[str]:
|
|
merged: list[str] = []
|
|
seen: set[str] = set()
|
|
for group in recipient_groups:
|
|
for addr in group:
|
|
lower = addr.lower()
|
|
if lower in seen:
|
|
continue
|
|
seen.add(lower)
|
|
merged.append(addr)
|
|
if not merged:
|
|
raise EmailConfigurationError(
|
|
"At least one recipient address is required")
|
|
return merged
|
|
|
|
|
|
def send_email(
|
|
*,
|
|
subject: str,
|
|
body: str,
|
|
to: Sequence[str] | str,
|
|
cc: Sequence[str] | str | None = None,
|
|
bcc: Sequence[str] | str | None = None,
|
|
reply_to: Sequence[str] | str | None = None,
|
|
settings: dict | None = None,
|
|
) -> bool:
|
|
"""Send an email using configured SMTP settings.
|
|
|
|
Returns True when a message is sent, False when email is disabled.
|
|
Raises EmailConfigurationError for invalid config and EmailDeliveryError for SMTP failures.
|
|
"""
|
|
|
|
config = settings or get_email_settings()
|
|
if not config.get("enabled"):
|
|
return False
|
|
|
|
smtp_cfg = config.get("smtp", {})
|
|
host = (smtp_cfg.get("host") or "").strip()
|
|
if not host:
|
|
raise EmailConfigurationError("SMTP host is not configured")
|
|
|
|
port = int(smtp_cfg.get("port", 587) or 587)
|
|
timeout = int(smtp_cfg.get("timeout", 30) or 30)
|
|
use_ssl = bool(smtp_cfg.get("use_ssl", False))
|
|
use_tls = bool(smtp_cfg.get("use_tls", True))
|
|
|
|
from_address = (config.get("from_address")
|
|
or smtp_cfg.get("username") or "").strip()
|
|
if not from_address:
|
|
raise EmailConfigurationError("From address is not configured")
|
|
|
|
to_list = _normalize_addresses(to)
|
|
cc_list = _normalize_addresses(cc)
|
|
bcc_list = _normalize_addresses(bcc)
|
|
reply_to_list = _normalize_addresses(reply_to)
|
|
all_recipients = _ensure_recipients(to_list, cc_list, bcc_list)
|
|
|
|
message = EmailMessage()
|
|
message["Subject"] = subject
|
|
message["From"] = from_address
|
|
message["To"] = ", ".join(to_list)
|
|
if cc_list:
|
|
message["Cc"] = ", ".join(cc_list)
|
|
if reply_to_list:
|
|
message["Reply-To"] = ", ".join(reply_to_list)
|
|
message.set_content(body)
|
|
|
|
username = (smtp_cfg.get("username") or "").strip()
|
|
password = smtp_cfg.get("password") or ""
|
|
|
|
client_cls = smtplib.SMTP_SSL if use_ssl else smtplib.SMTP
|
|
|
|
try:
|
|
with client_cls(host=host, port=port, timeout=timeout) as client:
|
|
client.ehlo()
|
|
if use_tls and not use_ssl:
|
|
client.starttls()
|
|
client.ehlo()
|
|
if username:
|
|
client.login(username, password)
|
|
client.send_message(message, from_addr=from_address,
|
|
to_addrs=all_recipients)
|
|
except EmailConfigurationError:
|
|
raise
|
|
except Exception as exc: # pragma: no cover - network errors depend on env
|
|
raise EmailDeliveryError(str(exc)) from exc
|
|
|
|
return True
|