Source code for emailpal.sendable_email

import abc
import re
from typing import Dict, TypeVar, Generic, Dict, Any, cast  # NOQA
from django.utils.safestring import SafeString
from django.core.mail import EmailMultiAlternatives, EmailMessage
from django.template.loader import render_to_string
from django.utils.html import strip_tags

T = TypeVar('T')


def collapse_and_strip_tags(text: str) -> str:
    '''
    Strips HTML tags and collapases newlines in the given string.

    Example:

      >>> collapse_and_strip_tags('\\n\\n<p>hi james</p>\\n\\n\\n')
      '\\nhi james\\n'
    '''

    return re.sub(r'\n+', '\n', strip_tags(text))


[docs]class SendableEmail(Generic[T], metaclass=abc.ABCMeta): ''' This abstract base class represents a template-based email that can be sent in HTML and plaintext formats. When generating the email, the template is actually rendered *twice*: once as HTML, and again as plain text. As explained in :ref:`"There are people who can't read HTML email?"`, this allows both formats to share most of their content, yet also deviate where necessary. So, aside from the context your code provides, the following context variables are provided when rendering your template: * ``is_html_email`` is ``True`` if (and only if) the template is being used to render the email's HTML representation. * ``is_plaintext_email`` is ``True`` if (and only if) the template is being used to render the email's plaintext representation. Note that when rendering the email as plaintext, HTML tags are automatically stripped from the generated content. ''' @property @abc.abstractmethod def example_ctx(self) -> T: ''' An example context with which the email can be rendered. ''' pass # pragma: no cover @property @abc.abstractmethod def subject(self) -> str: ''' The subject line of the email. This is processed by :py:meth:`str.format` and passed the same context that is passed to templates when rendering the email, so you can include context variables via brace notation, e.g. ``"Hello {full_name}!"``. ''' pass # pragma: no cover @property @abc.abstractmethod def template_name(self) -> str: ''' The path to the template used to render the email, e.g. ``"my_app/my_email.html"``. ''' pass # pragma: no cover def _cast_to_dict(self, ctx: T) -> Dict[str, Any]: if not isinstance(ctx, dict): raise ValueError('context must be a dict subclass') return cast(Dict[str, Any], ctx) def render_body_as_plaintext(self, ctx: T) -> str: plaintext_ctx = self._cast_to_dict(ctx).copy() plaintext_ctx['is_html_email'] = False plaintext_ctx['is_plaintext_email'] = True return collapse_and_strip_tags( render_to_string(self.template_name, plaintext_ctx) ) def render_body_as_html(self, ctx: T) -> SafeString: html_ctx = self._cast_to_dict(ctx).copy() html_ctx['is_html_email'] = True html_ctx['is_plaintext_email'] = False body = render_to_string(self.template_name, html_ctx) # TODO: This is a workaround for # https://github.com/18F/calc/issues/1409, need to figure # out the exact reason behind it. body = body.encode('ascii', 'xmlcharrefreplace').decode('ascii') return SafeString(body) def render_subject(self, ctx: T) -> str: return self.subject.format(**self._cast_to_dict(ctx))
[docs] def create_message(self, ctx: T, from_email=None, to=None, bcc=None, connection=None, attachments=None, headers=None, alternatives=None, cc=None, reply_to=None) -> EmailMessage: ''' Creates and returns a :py:class:`django.core.mail.EmailMessage` which contains the plaintext and HTML versions of the email, using the context specified by ``ctx``. Aside from ``ctx``, arguments to this method are the same as those for :py:class:`~django.core.mail.EmailMessage`. ''' msg = EmailMultiAlternatives( subject=self.render_subject(ctx), body=self.render_body_as_plaintext(ctx), from_email=from_email, to=to, bcc=bcc, connection=connection, attachments=attachments, headers=headers, alternatives=alternatives, cc=cc, reply_to=reply_to, ) msg.attach_alternative(self.render_body_as_html(ctx), 'text/html') return msg