mirror of
https://github.com/Kozea/Radicale.git
synced 2025-09-15 20:36:55 +00:00
- Add support for local SMTP
- Ignore venv in flake8
This commit is contained in:
parent
3228f046e2
commit
ce9b2cf5d2
5 changed files with 143 additions and 19 deletions
|
@ -1,16 +1,17 @@
|
|||
# This file is related to Radicale - CalDAV and CardDAV server
|
||||
# for email notifications
|
||||
# Copyright © 2025-2025 Nate Harris
|
||||
|
||||
import enum
|
||||
import re
|
||||
import smtplib
|
||||
import ssl
|
||||
from datetime import datetime, timedelta
|
||||
from email.encoders import encode_base64
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formatdate
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
import vobject
|
||||
|
||||
|
@ -28,6 +29,14 @@ PLUGIN_CONFIG_SCHEMA = {
|
|||
"value": "",
|
||||
"type": str
|
||||
},
|
||||
"smtp_security": {
|
||||
"value": "none",
|
||||
"type": str,
|
||||
},
|
||||
"smtp_ssl_verify_mode": {
|
||||
"value": "REQUIRED",
|
||||
"type": str,
|
||||
},
|
||||
"smtp_username": {
|
||||
"value": "",
|
||||
"type": str
|
||||
|
@ -82,6 +91,44 @@ MESSAGE_TEMPLATE_VARIABLES = [
|
|||
]
|
||||
|
||||
|
||||
class SMTP_SECURITY_TYPE_ENUM(enum.Enum):
|
||||
EMPTY = ""
|
||||
NONE = "none"
|
||||
STARTTLS = "starttls"
|
||||
TLS = "tls"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value):
|
||||
"""Convert a string to the corresponding enum value."""
|
||||
for member in cls:
|
||||
if member.value == value:
|
||||
return member
|
||||
raise ValueError(f"Invalid security type: {value}. Allowed values are: {[m.value for m in cls]}")
|
||||
|
||||
|
||||
class SMTP_SSL_VERIFY_MODE_ENUM(enum.Enum):
|
||||
EMPTY = ""
|
||||
NONE = "NONE"
|
||||
OPTIONAL = "OPTIONAL"
|
||||
REQUIRED = "REQUIRED"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, value):
|
||||
"""Convert a string to the corresponding enum value."""
|
||||
for member in cls:
|
||||
if member.value == value:
|
||||
return member
|
||||
raise ValueError(f"Invalid SSL verify mode: {value}. Allowed values are: {[m.value for m in cls]}")
|
||||
|
||||
|
||||
SMTP_SECURITY_TYPES: Sequence[str] = (SMTP_SECURITY_TYPE_ENUM.NONE.value,
|
||||
SMTP_SECURITY_TYPE_ENUM.STARTTLS.value,
|
||||
SMTP_SECURITY_TYPE_ENUM.TLS.value)
|
||||
SMTP_SSL_VERIFY_MODES: Sequence[str] = (SMTP_SSL_VERIFY_MODE_ENUM.NONE.value,
|
||||
SMTP_SSL_VERIFY_MODE_ENUM.OPTIONAL.value,
|
||||
SMTP_SSL_VERIFY_MODE_ENUM.REQUIRED.value)
|
||||
|
||||
|
||||
def ics_contents_contains_invited_event(contents: str):
|
||||
"""
|
||||
Check if the ICS contents contain an event (versus a VTODO or VJOURNAL).
|
||||
|
@ -543,6 +590,8 @@ class EmailConfig:
|
|||
def __init__(self,
|
||||
host: str,
|
||||
port: int,
|
||||
security: str,
|
||||
ssl_verify_mode: str,
|
||||
username: str,
|
||||
password: str,
|
||||
from_email: str,
|
||||
|
@ -551,6 +600,8 @@ class EmailConfig:
|
|||
removed_template: MessageTemplate):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.security = SMTP_SECURITY_TYPE_ENUM.from_string(value=security)
|
||||
self.ssl_verify_mode = SMTP_SSL_VERIFY_MODE_ENUM.from_string(value=ssl_verify_mode)
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.from_email = from_email
|
||||
|
@ -567,6 +618,9 @@ class EmailConfig:
|
|||
return f"EmailConfig(host={self.host}, port={self.port}, username={self.username}, " \
|
||||
f"from_email={self.from_email}, send_mass_emails={self.send_mass_emails})"
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def send_added_email(self, attendees: List[Attendee], event: EmailEvent) -> bool:
|
||||
"""
|
||||
Send a notification for added attendees.
|
||||
|
@ -644,6 +698,23 @@ class EmailConfig:
|
|||
|
||||
return not failure_encountered # Return True if all emails were sent successfully
|
||||
|
||||
def _build_context(self) -> ssl.SSLContext:
|
||||
"""
|
||||
Build the SSL context based on the configured security and SSL verify mode.
|
||||
:return: An SSLContext object configured for the SMTP connection.
|
||||
"""
|
||||
context = ssl.create_default_context()
|
||||
if self.ssl_verify_mode == SMTP_SSL_VERIFY_MODE_ENUM.REQUIRED:
|
||||
context.check_hostname = True
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
elif self.ssl_verify_mode == SMTP_SSL_VERIFY_MODE_ENUM.OPTIONAL:
|
||||
context.check_hostname = True
|
||||
context.verify_mode = ssl.CERT_OPTIONAL
|
||||
else:
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
return context
|
||||
|
||||
def _send_email(self,
|
||||
subject: str,
|
||||
body: str,
|
||||
|
@ -681,11 +752,25 @@ class EmailConfig:
|
|||
text = message.as_string()
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(host=self.host, port=self.port)
|
||||
server.ehlo() # Identify self to server
|
||||
server.starttls() # Start TLS connection
|
||||
server.ehlo() # Identify again after starting TLS
|
||||
server.login(user=self.username, password=self.password)
|
||||
if self.security == SMTP_SECURITY_TYPE_ENUM.EMPTY:
|
||||
logger.warning("SMTP security type is empty, raising ValueError.")
|
||||
raise ValueError("SMTP security type cannot be empty. Please specify a valid security type.")
|
||||
elif self.security == SMTP_SECURITY_TYPE_ENUM.NONE:
|
||||
server = smtplib.SMTP(host=self.host, port=self.port)
|
||||
elif self.security == SMTP_SECURITY_TYPE_ENUM.STARTTLS:
|
||||
context = self._build_context()
|
||||
server = smtplib.SMTP(host=self.host, port=self.port)
|
||||
server.ehlo() # Identify self to server
|
||||
server.starttls(context=context) # Start TLS connection
|
||||
server.ehlo() # Identify again after starting TLS
|
||||
elif self.security == SMTP_SECURITY_TYPE_ENUM.TLS:
|
||||
context = self._build_context()
|
||||
server = smtplib.SMTP_SSL(host=self.host, port=self.port, context=context)
|
||||
|
||||
if self.username and self.password:
|
||||
logger.debug("Logging in to SMTP server with username: %s", self.username)
|
||||
server.login(user=self.username, password=self.password)
|
||||
|
||||
errors: Dict[str, Tuple[int, bytes]] = server.sendmail(from_addr=self.from_email, to_addrs=to_addresses,
|
||||
msg=text)
|
||||
logger.debug("Email sent successfully to %s", to_addresses)
|
||||
|
@ -701,12 +786,6 @@ class EmailConfig:
|
|||
|
||||
return True
|
||||
|
||||
def __repr__(self):
|
||||
return f'EmailConfig(host={self.host}, port={self.port}, from_email={self.from_email})'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.from_email} ({self.host}:{self.port})'
|
||||
|
||||
|
||||
def _read_event(vobject_data: str) -> EmailEvent:
|
||||
"""
|
||||
|
@ -729,6 +808,8 @@ class Hook(BaseHook):
|
|||
self.email_config = EmailConfig(
|
||||
host=self.configuration.get("hook", "smtp_server"),
|
||||
port=self.configuration.get("hook", "smtp_port"),
|
||||
security=self.configuration.get("hook", "smtp_security"),
|
||||
ssl_verify_mode=self.configuration.get("hook", "smtp_ssl_verify_mode"),
|
||||
username=self.configuration.get("hook", "smtp_username"),
|
||||
password=self.configuration.get("hook", "smtp_password"),
|
||||
from_email=self.configuration.get("hook", "from_email"),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue