Source code for mails.core

__copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
__license__ = "AGPL v3"

from html2text import HTML2Text
import json
import re
import inspect

from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.db import models
from django.template.loader import get_template

from .exceptions import ConfigurationError


[docs]class MailEngine: """ This engine processes the configuration and template files to be saved into the database in the MailLog table. """ _required_parameters = ['recipient_list', 'subject'] _possible_parameters = ['recipient_list', 'subject', 'from_email', 'from_name', 'bcc'] _email_fields = ['recipient_list', 'from_email', 'bcc'] _processed_template = False _mail_sent = False def __init__(self, mail_code, subject='', recipient_list=[], bcc=[], from_email='', from_name='', **kwargs): """ Start engine with specific mail_code. Any other keyword argument that is passed will be used as a variable in the mail template. @Arguments -- mail_code (str) @Keyword arguments The following arguments overwrite the default values, set in the configuration files: -- subject (str, optional) -- recipient_list (str, optional): List of email addresses or db-relations. -- bcc (str, optional): List of email addresses or db-relations. -- from_email (str, optional): Plain email address. -- from_name (str, optional): Display name for from address. """ self.mail_code = mail_code self.extra_config = { 'bcc': bcc, 'subject': subject, 'from_name': from_name, 'from_email': from_email, 'recipient_list': recipient_list, } self.template_variables = kwargs def __repr__(self): return '<%(cls)s code="%(code)s", validated=%(validated)s sent=%(sent)s>' % { 'cls': self.__class__.__name__, 'code': self.mail_code, 'validated': hasattr(self, 'mail_data'), 'sent': self._mail_sent, }
[docs] def validate(self, render_template=False): """Check if MailEngine is valid and ready for sending.""" self._read_configuration_file() self._detect_and_save_object() self._check_template_exists() self._validate_configuration() self._validate_email_fields() if render_template: self.render_template()
[docs] def render_only(self): """Render template. To be used in mail backend only.""" if not hasattr(self, 'mail_data'): self.mail_data = {} self._check_template_exists() self.render_template() return self.mail_data['message'], self.mail_data.get('html_message', '')
[docs] def render_template(self, html_message=None): """ Render the template associated with the mail_code. If html_message is given, use this as a template instead. """ if html_message: self.mail_data['html_message'] = html_message else: self.mail_data['html_message'] = self._template.render(self.template_variables) # Damn slow. # Transform to non-HTML version. handler = HTML2Text() self.mail_data['message'] = handler.handle(self.mail_data['html_message']) self._processed_template = True
[docs] def send_mail(self): """Send the mail.""" if self._mail_sent: # Prevent double sending when using a Django form. return elif not hasattr(self, 'mail_data'): raise ValueError( "The mail: %s could not be sent because the data didn't validate." % self.mail_code) email = EmailMultiAlternatives( self.mail_data['subject'], self.mail_data.get('message', ''), '%s <%s>' % ( self.mail_data.get('from_name', 'SciPost'), self.mail_data.get('from_email', 'noreply@scipost.org')), self.mail_data['recipient_list'], bcc=self.mail_data['bcc'], reply_to=[ self.mail_data.get('from_email', 'noreply@scipost.org') ], headers={ 'delayed_processing': not self._processed_template, 'context': self.template_variables, '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 'object' in self.template_variables and hasattr(self.template_variables['object'], 'mail_sent'): self.template_variables['object'].mail_sent()
def _detect_and_save_object(self): """ Detect if less than or equal to one object exists and save it, else raise exception. Stick to Django's convention of saving it as a central `object` variable. """ object = None context_object_name = None if 'object' in self.template_variables: object = self.template_variables['object'] context_object_name = object._meta.model_name elif 'instance' in self.template_variables: object = self.template_variables['instance'] context_object_name = object._meta.model_name else: for key, var in self.template_variables.items(): if isinstance(var, models.Model): if object: raise ValueError('Multiple db instances are given. Please specify which object to use.') else: object = var if object: self.template_variables['object'] = object if context_object_name and context_object_name not in self.template_variables: self.template_variables[context_object_name] = object def _read_configuration_file(self): """Retrieve default configuration for specific mail_code.""" json_location = '%s/templates/email/%s.json' % (settings.BASE_DIR, self.mail_code) try: self.mail_data = json.loads(open(json_location).read()) except OSError: raise ImportError('No configuration file found. Mail code: %s' % self.mail_code) # Check if configuration file is valid. if 'subject' not in self.mail_data: raise ConfigurationError('key "subject" is missing.') if 'recipient_list' not in self.mail_data: raise ConfigurationError('key "recipient_list" is missing.') # Overwrite mail data if parameters are given. for key, val in self.extra_config.items(): if val or key not in self.mail_data: self.mail_data[key] = val def _check_template_exists(self): """Save template or raise TemplateDoesNotExist.""" self._template = get_template('email/%s.html' % self.mail_code) def _validate_configuration(self): """Check if all required data is given via either configuration or extra parameters.""" # Check data is complete if not all(key in self.mail_data for key in self._required_parameters): txt = 'Not all required parameters are given in the configuration file or on instantiation.' txt += ' Check required parameters: {}'.format(self._required_parameters) raise ConfigurationError(txt) # Check if data is overcomplete/ if not all(key in self._possible_parameters for key in self.mail_data.keys()): txt = 'Configuration file may only contain the following parameters: {}.'.format( self._possible_parameters) raise ConfigurationError(txt) # Check all configuration value types for email_key in ['subject', 'from_email', 'from_name']: if email_key in self.mail_data and self.mail_data[email_key]: if not isinstance(self.mail_data[email_key], str): raise ConfigurationError('"%(key)s" argument must be a string' % { 'key': email_key, }) for email_key in ['recipient_list', 'bcc']: if email_key in self.mail_data and self.mail_data[email_key]: if not isinstance(self.mail_data[email_key], list): raise ConfigurationError('"%(key)s" argument must be a list' % { 'key': email_key, }) def _validate_email_fields(self): """Validate all email addresses in the mail config.""" for email_key in self._email_fields: if email_key in self.mail_data: if isinstance(self.mail_data[email_key], list): for i, email in enumerate(self.mail_data[email_key]): self.mail_data[email_key][i] = self._validate_email_addresses(email) else: self.mail_data[email_key] = self._validate_email_addresses(self.mail_data[email_key]) def _validate_email_addresses(self, entry): """Return email address given raw email or database relation given in `entry`.""" if re.match("[^@]+@[^@]+\.[^@]+", entry): # Email string return entry elif self.template_variables['object']: mail_to = self.template_variables['object'] 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 raise KeyError('The property (%s) does not exist.' % entry) return mail_to raise KeyError('Neither an email adress nor db instance is given.')