first commit
This commit is contained in:
commit
dc1245f164
22 changed files with 1666 additions and 0 deletions
13
notifications/__init__.py
Normal file
13
notifications/__init__.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"""Notifications package."""
|
||||
|
||||
from notifications.base import LoggingNotificationService, NotificationService
|
||||
from notifications.email import EmailNotificationService
|
||||
from notifications.sms import SmsNotificationService, create_sms_service
|
||||
|
||||
__all__ = [
|
||||
"NotificationService",
|
||||
"LoggingNotificationService",
|
||||
"SmsNotificationService",
|
||||
"EmailNotificationService",
|
||||
"create_sms_service",
|
||||
]
|
||||
32
notifications/base.py
Normal file
32
notifications/base.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
"""Notification service abstraction."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NotificationService(ABC):
|
||||
"""Abstract notification service."""
|
||||
|
||||
@abstractmethod
|
||||
def send_sms(self, message: str) -> bool:
|
||||
"""Send an SMS message."""
|
||||
|
||||
@abstractmethod
|
||||
def send_email(self, subject: str, body: str) -> bool:
|
||||
"""Send an email message."""
|
||||
|
||||
|
||||
class LoggingNotificationService(NotificationService):
|
||||
"""Default implementation that logs notifications instead of sending them."""
|
||||
|
||||
def send_sms(self, message: str) -> bool:
|
||||
logger.info("SMS WOULD BE SENT:\n%s", message)
|
||||
return True
|
||||
|
||||
def send_email(self, subject: str, body: str) -> bool:
|
||||
logger.info("EMAIL WOULD BE SENT:\nSubject: %s\n%s", subject, body)
|
||||
return True
|
||||
57
notifications/email.py
Normal file
57
notifications/email.py
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
"""Email notification provider."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from notifications.base import LoggingNotificationService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EmailNotificationService(LoggingNotificationService):
|
||||
"""
|
||||
Email notification service.
|
||||
|
||||
Uses SMTP credentials from environment variables when configured.
|
||||
Falls back to logging when SMTP is not set up.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.smtp_host = os.environ.get("SMTP_HOST", "")
|
||||
self.smtp_port = int(os.environ.get("SMTP_PORT", "587"))
|
||||
self.smtp_user = os.environ.get("SMTP_USER", "")
|
||||
self.smtp_password = os.environ.get("SMTP_PASSWORD", "")
|
||||
self.email_from = os.environ.get("EMAIL_FROM", "")
|
||||
self.email_to = os.environ.get("EMAIL_TO", "")
|
||||
self.use_tls = os.environ.get("SMTP_USE_TLS", "true").lower() == "true"
|
||||
|
||||
def _smtp_configured(self) -> bool:
|
||||
return all([self.smtp_host, self.smtp_user, self.smtp_password, self.email_from, self.email_to])
|
||||
|
||||
def send_email(self, subject: str, body: str) -> bool:
|
||||
if not self._smtp_configured():
|
||||
return super().send_email(subject, body)
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg["From"] = self.email_from
|
||||
msg["To"] = self.email_to
|
||||
msg["Subject"] = subject
|
||||
msg.attach(MIMEText(body, "plain"))
|
||||
|
||||
with smtplib.SMTP(self.smtp_host, self.smtp_port, timeout=30) as server:
|
||||
if self.use_tls:
|
||||
server.starttls()
|
||||
server.login(self.smtp_user, self.smtp_password)
|
||||
server.sendmail(self.email_from, [self.email_to], msg.as_string())
|
||||
|
||||
logger.info("Email sent to %s", self.email_to)
|
||||
return True
|
||||
except Exception as exc:
|
||||
logger.error("Failed to send email: %s", exc)
|
||||
return False
|
||||
41
notifications/sms.py
Normal file
41
notifications/sms.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
"""SMS notification provider stubs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from notifications.base import LoggingNotificationService, NotificationService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SmsNotificationService(LoggingNotificationService):
|
||||
"""
|
||||
SMS notification service.
|
||||
|
||||
Currently logs messages. Future providers (Twilio, Urmi, etc.) can be
|
||||
plugged in without changing the monitoring engine.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.provider = os.environ.get("SMS_PROVIDER", "logging")
|
||||
self.api_key = os.environ.get("SMS_API_KEY", "")
|
||||
self.from_number = os.environ.get("SMS_FROM_NUMBER", "")
|
||||
self.to_number = os.environ.get("SMS_TO_NUMBER", "")
|
||||
|
||||
def send_sms(self, message: str) -> bool:
|
||||
if self.provider == "logging" or not self.api_key:
|
||||
return super().send_sms(message)
|
||||
|
||||
# Placeholder for future provider integration
|
||||
logger.warning(
|
||||
"SMS provider '%s' is not yet implemented; logging instead",
|
||||
self.provider,
|
||||
)
|
||||
return super().send_sms(message)
|
||||
|
||||
|
||||
def create_sms_service() -> NotificationService:
|
||||
"""Factory for SMS-capable notification service."""
|
||||
return SmsNotificationService()
|
||||
Loading…
Add table
Add a link
Reference in a new issue