You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
10 KiB
Python
276 lines
10 KiB
Python
import logging
|
|
import os
|
|
|
|
from contrib.models import CommonModel
|
|
from contrib.rules import has_global_perm
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.validators import RegexValidator
|
|
from django.db import models
|
|
from django.utils.translation import gettext
|
|
from django.utils.translation import gettext_lazy as _
|
|
from utils import RestrictedFileField
|
|
|
|
from .mailutils import addr_with_name
|
|
from .mailutils import get_content
|
|
from .mailutils import NOT_SENT
|
|
from .mailutils import PARTLY_SENT
|
|
from .mailutils import send
|
|
from .mailutils import SENT
|
|
from .rules import is_creator
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
alphanumeric = RegexValidator(
|
|
r"^[0-9a-zA-Z._-]*$", _("Only alphanumeric characters, ., - and _ are allowed")
|
|
)
|
|
|
|
|
|
class EmailAddress(models.Model):
|
|
"""Represents an email address, that is forwarded to specific members"""
|
|
|
|
name = models.CharField(_("name"), max_length=50, validators=[alphanumeric], unique=True)
|
|
to_members = models.ManyToManyField(
|
|
"members.Member", verbose_name=_("Forward to participants"), blank=True
|
|
)
|
|
to_groups = models.ManyToManyField(
|
|
"members.Group", verbose_name=_("Forward to group"), blank=True
|
|
)
|
|
internal_only = models.BooleanField(
|
|
verbose_name=_("Restrict to internal email addresses"),
|
|
help_text=_(
|
|
"Only allow forwarding to this e-mail address from one of the following domains: %(domains)s."
|
|
)
|
|
% {"domains": ", ".join(settings.ALLOWED_EMAIL_DOMAINS_FOR_INVITE_AS_USER)},
|
|
default=False,
|
|
)
|
|
allowed_senders = models.ManyToManyField(
|
|
"members.Group",
|
|
verbose_name=_("Allowed sender"),
|
|
help_text=_(
|
|
"Only forward e-mails of members of selected groups. Leave empty to allow all senders."
|
|
),
|
|
blank=True,
|
|
related_name="allowed_sender_on_emailaddresses",
|
|
)
|
|
|
|
@property
|
|
def email(self):
|
|
return "{}@{}".format(self.name, settings.DOMAIN)
|
|
|
|
@property
|
|
def forwards(self):
|
|
mails = {member.email for member in self.to_members.all()}
|
|
mails.update(
|
|
[member.email for group in self.to_groups.all() for member in group.member_set.all()]
|
|
)
|
|
return mails
|
|
|
|
def __str__(self):
|
|
return self.email
|
|
|
|
class Meta:
|
|
verbose_name = _("email address")
|
|
verbose_name_plural = _("email addresses")
|
|
|
|
|
|
class EmailAddressForm(forms.ModelForm):
|
|
class Meta:
|
|
model = EmailAddress
|
|
exclude = []
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
group = self.cleaned_data.get("to_groups")
|
|
members = self.cleaned_data.get("to_members")
|
|
if not group and not members:
|
|
raise ValidationError(
|
|
_("Either a group or at least one member is required as forward recipient.")
|
|
)
|
|
|
|
|
|
# Create your models here.
|
|
class Message(CommonModel):
|
|
"""Represents a message that can be sent to some members"""
|
|
|
|
subject = models.CharField(_("subject"), max_length=50)
|
|
content = models.TextField(_("content"))
|
|
to_groups = models.ManyToManyField("members.Group", verbose_name=_("to group"), blank=True)
|
|
to_freizeit = models.ForeignKey(
|
|
"members.Freizeit",
|
|
verbose_name=_("to freizeit"),
|
|
on_delete=models.CASCADE,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
to_notelist = models.ForeignKey(
|
|
"members.MemberNoteList",
|
|
verbose_name=_("to notes list"),
|
|
on_delete=models.CASCADE,
|
|
blank=True,
|
|
null=True,
|
|
)
|
|
to_members = models.ManyToManyField("members.Member", verbose_name=_("to member"), blank=True)
|
|
reply_to = models.ManyToManyField(
|
|
"members.Member",
|
|
verbose_name=_("reply to participant"),
|
|
blank=True,
|
|
related_name="reply_to",
|
|
)
|
|
reply_to_email_address = models.ManyToManyField(
|
|
"mailer.EmailAddress",
|
|
verbose_name=_("reply to custom email address"),
|
|
blank=True,
|
|
related_name="reply_to_email_addr",
|
|
)
|
|
sent = models.BooleanField(_("sent"), default=False)
|
|
created_by = models.ForeignKey(
|
|
"members.Member",
|
|
verbose_name=_("Created by"),
|
|
blank=True,
|
|
null=True,
|
|
on_delete=models.SET_NULL,
|
|
related_name="created_messages",
|
|
)
|
|
|
|
def __str__(self):
|
|
return self.subject
|
|
|
|
def get_recipients(self):
|
|
recipients = [g.name for g in self.to_groups.all()]
|
|
if self.to_freizeit is not None:
|
|
recipients.append(self.to_freizeit.name)
|
|
if self.to_notelist is not None:
|
|
recipients.append(self.to_notelist.title)
|
|
if 3 > self.to_members.count() > 0:
|
|
recipients.extend([m.name for m in self.to_members.all()])
|
|
elif self.to_members.count() > 2:
|
|
recipients.append(gettext("Some other members"))
|
|
return ", ".join(recipients)
|
|
|
|
get_recipients.short_description = _("recipients")
|
|
|
|
def submit(self, sender=None):
|
|
"""Sends the mail to the specified group of members"""
|
|
# recipients
|
|
members = set()
|
|
# get all the members of the selected groups
|
|
groups = [gr.member_set.all() for gr in self.to_groups.all()]
|
|
members.update([m for gr in groups for m in gr])
|
|
# get all the individually picked members
|
|
members.update(self.to_members.all())
|
|
# get all the members of the selected freizeit
|
|
if self.to_freizeit is not None:
|
|
members.update([mol.member for mol in self.to_freizeit.membersonlist.all()])
|
|
members.update(self.to_freizeit.jugendleiter.all())
|
|
# get all the members of the selected notes list
|
|
if self.to_notelist is not None:
|
|
members.update([mol.member for mol in self.to_notelist.membersonlist.all()])
|
|
filtered = [m for m in members if m.gets_newsletter]
|
|
logger.info(f"sending mail to {filtered}")
|
|
|
|
attach = [a.f.path for a in Attachment.objects.filter(msg__id=self.pk) if a.f.name]
|
|
|
|
emails = [member.email for member in filtered]
|
|
emails.extend([member.alternative_email for member in filtered if member.alternative_email])
|
|
# remove any underscores from subject to prevent Arne from using
|
|
# terrible looking underscores in subjects
|
|
self.subject = self.subject.replace("_", " ")
|
|
# generate message id
|
|
message_id = "<{pk}@{domain}>".format(pk=self.pk, domain=settings.DOMAIN)
|
|
# reply to addresses
|
|
reply_to = [jl.association_email for jl in self.reply_to.all()]
|
|
reply_to.extend([ml.email for ml in self.reply_to_email_address.all()])
|
|
# set correct from address
|
|
# if the sender is none or if sending from association emails has been
|
|
# disabled, use the default sending mail
|
|
if sender is None:
|
|
from_addr = addr_with_name(settings.DEFAULT_SENDING_MAIL, settings.DEFAULT_SENDING_NAME)
|
|
elif sender and settings.SEND_FROM_ASSOCIATION_EMAIL:
|
|
from_addr = addr_with_name(sender.association_email, sender.name)
|
|
else:
|
|
from_addr = addr_with_name(settings.DEFAULT_SENDING_MAIL, sender.name)
|
|
# if sending from the association email has been disabled,
|
|
# a sender was supplied and the reply to is empty, add the sender's
|
|
# DAV360 email as reply to
|
|
if (
|
|
sender
|
|
and not settings.SEND_FROM_ASSOCIATION_EMAIL
|
|
and sender.has_internal_email()
|
|
and reply_to == []
|
|
):
|
|
reply_to.append(addr_with_name(sender.email, sender.name))
|
|
try:
|
|
success = send(
|
|
self.subject,
|
|
get_content(self.content, registration_complete=True),
|
|
from_addr,
|
|
emails,
|
|
message_id=message_id,
|
|
attachments=attach,
|
|
reply_to=reply_to,
|
|
)
|
|
if success == SENT or success == PARTLY_SENT:
|
|
self.sent = True
|
|
for a in Attachment.objects.filter(msg__id=self.pk):
|
|
if a.f.name:
|
|
os.remove(a.f.path)
|
|
a.delete()
|
|
success = SENT
|
|
except Exception as e:
|
|
logger.error(f"Caught exception while sending email: {e}")
|
|
success = NOT_SENT
|
|
finally:
|
|
self.save()
|
|
return success
|
|
|
|
class Meta(CommonModel.Meta):
|
|
verbose_name = _("message")
|
|
verbose_name_plural = _("messages")
|
|
permissions = (("submit_mails", _("Can submit mails")),)
|
|
rules_permissions = {
|
|
"view_obj": is_creator | has_global_perm("mailer.view_global_message"),
|
|
"change_obj": is_creator | has_global_perm("mailer.change_global_message"),
|
|
"delete_obj": is_creator | has_global_perm("mailer.delete_global_message"),
|
|
}
|
|
|
|
|
|
class MessageForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Message
|
|
exclude = []
|
|
|
|
def clean(self):
|
|
group = self.cleaned_data.get("to_groups")
|
|
freizeit = self.cleaned_data.get("to_freizeit")
|
|
notelist = self.cleaned_data.get("to_notelist")
|
|
members = self.cleaned_data.get("to_members")
|
|
if not group and freizeit is None and not members and notelist is None:
|
|
raise ValidationError(
|
|
_("Either a group, a memberlist or at least one member is required as recipient")
|
|
)
|
|
|
|
|
|
class Attachment(CommonModel):
|
|
"""Represents an attachment to an email"""
|
|
|
|
msg = models.ForeignKey(Message, on_delete=models.CASCADE)
|
|
# file (not naming it file because of builtin)
|
|
f = RestrictedFileField(_("file"), upload_to="attachments", max_upload_size=10)
|
|
|
|
def __str__(self):
|
|
return os.path.basename(self.f.name) if self.f.name else str(_("Empty"))
|
|
|
|
class Meta:
|
|
verbose_name = _("attachment")
|
|
verbose_name_plural = _("attachments")
|
|
rules_permissions = {
|
|
"add_obj": is_creator | has_global_perm("mailer.view_global_message"),
|
|
"view_obj": is_creator | has_global_perm("mailer.view_global_message"),
|
|
"change_obj": is_creator | has_global_perm("mailer.change_global_message"),
|
|
"delete_obj": is_creator | has_global_perm("mailer.delete_global_message"),
|
|
}
|