from email.utils import parseaddr
from django.core.exceptions import ValidationError
from django.core.validators import validate_email
from django.db import models
from django.template import Context
from django.utils.translation import gettext_lazy as _
from encrypted_fields.fields import EncryptedCharField
from caps.models import Owned
from ox.utils.models import Named, Timestamped, ChildOwned
from ox.apps.content.models import RichTextField
from ox.apps.files.models import File
__all__ = ("MailAccount", "BaseMail", "Mail", "validate_email_list")
def validate_email_list(value):
if value:
emails = [e.strip() for e in value.split(",") if e.strip()]
for raw in emails:
name, email = parseaddr(raw)
if not email:
raise ValidationError(_(f"Invalid email format: '{raw}'"))
try:
validate_email(email)
except ValidationError:
raise ValidationError(_(f"Invalid email address: '{email}'"))
[docs]
class MailAccount(Named, Owned):
"""Configure an email account used to send mails."""
[docs]
class Encryption(models.IntegerChoices):
NONE = 0x00, _("None")
TLS = 0x01, "TLS"
SSL = 0x02, "SSL"
mail_header = RichTextField(
_("Mail Header"),
help_text=_("Header displayed at the top of all messages."),
blank=True,
null=True,
)
mail_signature = RichTextField(
_("Signature"), help_text=_("The signature displayed at the end of all messages."), blank=True, null=True
)
mail_subscription_footer = RichTextField(
_("Subscription Message"),
help_text=_("Message displayed on mails for contacts of subscription lists."),
blank=True,
null=True,
)
# SMTP Configuration
smtp_host = models.CharField(_("Host"), max_length=255)
smtp_port = models.PositiveIntegerField(_("Port"), default=587)
smtp_username = EncryptedCharField(_("Username"), max_length=255)
smtp_password = EncryptedCharField(_("Password"), max_length=128)
smtp_encryption = models.PositiveSmallIntegerField(
_("Encryption (SMTP)"), choices=Encryption, default=Encryption.SSL
)
# IMAP Configuration (optional)
# imap_host = models.CharField(_("Host (IMAP)"), max_length=255, blank=True, null=True)
# imap_port = models.PositiveIntegerField(_("Port (IMAP)"), blank=True, null=True)
# imap_username = EncryptedCharField(_("Username (IMAP)"), max_length=255, blank=True, null=True)
# imap_password = EncryptedCharField(_("Password (IMAP)"), max_length=128, blank=True, null=True)
# imap_ssl = models.BooleanField(_("Use SSL (IMAP)"), default=True, null=True, blank=True)
# imap_folder = models.CharField(_("Folder (IMAP)"), max_length=255, default="INBOX")
class Meta:
verbose_name = _("Email Account")
verbose_name_plural = _("Email Accounts")
[docs]
class BaseMail(Timestamped, ChildOwned):
"""
Base class for outgoing emails. Later is it planned for incoming too.
This is an abstract model as it shall be subclassed.
"""
[docs]
class State(models.IntegerChoices):
DRAFT = 0x00, _("Draft")
SENDING = 0x01, _("Sending")
SENT = 0x02, _("Sent")
ERROR = 0x03, _("Error")
account = models.ForeignKey(MailAccount, models.CASCADE, verbose_name=_("Account"))
state = models.PositiveSmallIntegerField(_("State"), choices=State.choices, default=State.DRAFT)
context = models.JSONField(_("Context"), default=dict)
subject = models.TextField(_("Subject"), default="")
content = RichTextField(_("Message"), default="")
attachments = models.ManyToManyField(File, related_name="+", verbose_name=_("Attached files"))
# From ChildOwned
parent_attr = "account"
class Meta:
abstract = True
verbose_name = _("Mail")
verbose_name_plural = _("Mails")
# TODO: validate owner from template
[docs]
def get_recipients(self) -> list[tuple[str, Context]]:
"""Return the recipients of the mail."""
raise NotImplementedError("This method must be implemented by subclass.")
[docs]
def get_content(self):
"""Return raw content."""
return self.content
[docs]
def get_subject(self):
"""Return raw subject."""
return self.subject
[docs]
def get_context(self, **context) -> Context:
"""Return mail context."""
return Context({**self.context, **context})
[docs]
class Mail(BaseMail):
"""A simple mail sending to a list of recipients as string."""
recipients = models.CharField(
_("Recipients"),
max_length=512,
validators=[validate_email_list],
help_text=_("A list of recipients, separated by a comma."),
)
""" Recipients, as a list of email strings. """
# Ensure to be detected by vue-i18n util
State = BaseMail.State
class Meta:
verbose_name = _("Mail")
verbose_name_plural = _("Mails")
[docs]
def get_recipients(self):
"""
Return recipients with context filled with ``name`` and ``email``.
"""
emails = [e.strip() for e in self.recipients.split(",") if e.strip()]
recipients = []
for raw in emails:
name, email = parseaddr(raw)
recipients.append((email, {"name": name or email.split("@")[0], "email": email}))
return recipients