__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"
import time
import re
import json
import inspect
from html2text import HTML2Text
from django.core.mail import EmailMultiAlternatives
from django.contrib.auth import get_user_model
from django.conf import settings
from django.template import loader
from scipost.models import Contributor
from common.utils import get_current_domain
[docs]class MailUtilsMixin:
"""
This mixin takes care of inserting the default data into the Utils or Form.
DEPRECATED
"""
instance = None
mail_data = {}
mail_template = ""
html_message = ""
message = ""
original_recipient = ""
mail_sent = False
delayed_processing = False
def __init__(self, *args, **kwargs):
"""Init an instance for a specific mail_code.
Arguments:
-- mail_code (str)
-- subject (str)
-- to (str): Email address or relation on the `instance`. Separated by comma.
-- bcc_to (str, optional): Email address or relation on the `instance`. Separated by comma.
-- instance: Instance of central object in email.
-- from (str, optional): Plain email address.
-- from_name (str, optional): Display name for from address.
"""
self.pre_validation(*args, **kwargs)
super().__init__(*args)
[docs] def pre_validation(self, *args, **kwargs):
"""Validate the incoming data to initiate a specific mail."""
self.mail_code = kwargs.pop("mail_code")
self.instance = kwargs.pop("instance", None)
kwargs["object"] = self.instance # Similar template nomenclature as Django.
self.mail_data = {
"subject": kwargs.pop("subject", ""),
"to_address": kwargs.pop("to", ""),
"bcc_to": kwargs.pop("bcc", ""),
"from_address_name": kwargs.pop("from_name", "SciPost"),
"from_address": kwargs.pop(
"from", "no-reply@%s" % get_current_domain()
),
}
# Gather meta data
json_location = "%s/templates/email/%s.json" % (
settings.BASE_DIR,
self.mail_code,
)
try:
self.mail_data.update(json.loads(open(json_location).read()))
except OSError:
if not self.mail_data["subject"]:
raise NotImplementedError(
(
"You did not create a valid .html and .json file "
"for mail_code: %s" % self.mail_code
)
)
# Save central object/instance if not already
self.instance = self.get_object(**kwargs)
# Digest the templates
if not self.delayed_processing:
mail_template = loader.get_template("email/%s.html" % self.mail_code)
if self.instance and self.mail_data.get("context_object"):
kwargs[self.mail_data["context_object"]] = self.instance
self.mail_template = mail_template.render(kwargs) # Damn slow.
# Gather Recipients data
try:
self.original_recipient = self._validate_single_entry(
self.mail_data.get("to_address")
)[0]
except IndexError:
self.original_recipient = ""
self.subject = self.mail_data["subject"]
[docs] def get_object(self, **kwargs):
if self.instance:
return self.instance
if self.mail_data.get("context_object"):
return kwargs.get(self.mail_data["context_object"], None)
def _validate_single_entry(self, entry):
"""
entry -- raw email string or path or properties leading to email mail field
Returns a list of email addresses found.
"""
if entry and self.instance:
if re.match("[^@]+@[^@]+\.[^@]+", entry):
# Email string
return [entry]
else:
mail_to = self.instance
for attr in entry.split("."):
try:
mail_to = getattr(mail_to, attr)
if inspect.ismethod(mail_to):
mail_to = mail_to()
except AttributeError:
# Invalid property/mail
return []
if not isinstance(mail_to, list):
return [mail_to]
else:
return mail_to
elif re.match("[^@]+@[^@]+\.[^@]+", entry):
return [entry]
else:
return []
[docs] def validate_bcc_list(self):
"""
bcc_to in the .json file may contain multiple raw email addreses or property paths to
an email field. The different entries need to be comma separated.
"""
# Get recipients list. Try to send through BCC to prevent privacy issues!
self.bcc_list = []
if self.mail_data.get("bcc_to"):
for bcc_entry in self.mail_data["bcc_to"].split(","):
self.bcc_list += self._validate_single_entry(bcc_entry)
[docs] def validate_recipients(self):
# Check the send list
if isinstance(self.original_recipient, list):
recipients = self.original_recipient
elif not isinstance(self.original_recipient, str):
try:
recipients = list(self.original_recipient)
except TypeError:
recipients = [self.original_recipient]
else:
recipients = [self.original_recipient]
recipients = list(recipients)
# Check if email needs to be taken from an instance
_recipients = []
for recipient in recipients:
if isinstance(recipient, Contributor):
_recipients.append(recipient.user.email)
elif isinstance(recipient, get_user_model()):
_recipients.append(recipient.email)
elif isinstance(recipient, str):
_recipients.append(recipient)
self.recipients = _recipients
[docs] def validate_message(self):
if not self.html_message:
self.html_message = self.mail_template
handler = HTML2Text()
self.message = handler.handle(self.html_message)
[docs] def validate(self):
"""Execute different validation methods.
Only to be used when the default data is used, eg. not in the EmailTemplateForm.
"""
self.validate_message()
self.validate_bcc_list()
self.validate_recipients()
self.save_mail_data()
[docs] def save_mail_data(self):
"""Save mail validated mail data; update default values of mail data."""
self.mail_data.update(
{
"subject": self.subject,
"message": self.message,
"html_message": self.html_message,
"recipients": self.recipients,
"bcc_list": self.bcc_list,
}
)
[docs] def set_alternative_sender(self, from_name, from_address):
"""TODO: REMOVE; DEPRECATED
Set an alternative from address/name from the default values received from the json
config file. The arguments only take raw string data, no methods/properties!
"""
self.mail_data["from_address_name"] = from_name
self.mail_data["from_address"] = from_address
[docs] def send(self):
"""Send the mail assuming `mail_data` is validated and complete."""
if self.mail_sent:
# Prevent double sending when using a Django form.
return
email = EmailMultiAlternatives(
self.mail_data["subject"],
self.mail_data["message"],
"%s <%s>"
% (self.mail_data["from_address_name"], self.mail_data["from_address"]),
self.mail_data["recipients"],
bcc=self.mail_data["bcc_list"],
reply_to=[self.mail_data["from_address"]],
headers={
"delayed_processing": self.delayed_processing,
"content_object": self.get_object(),
"mail_code": self.mail_code,
},
)
# Send html version if available
if "html_message" in self.mail_data:
email.attach_alternative(self.mail_data["html_message"], "text/html")
email.send(fail_silently=False)
self.mail_sent = True
if self.instance and hasattr(self.instance, "mail_sent"):
self.instance.mail_sent()