Example #1
0
 def test_is_email_allowed_by_dmarc_with_subdomain_policy_quarantine(self):
     """Testing is_email_allowed_by_dmarc with subdomain and
     policy=quarantine
     """
     self.dmarc_txt_records['_dmarc.example.com'] = \
         'v=DMARC1; p=reject; sp=quarantine;'
     self.assertFalse(is_email_allowed_by_dmarc('*****@*****.**'))
Example #2
0
 def test_is_email_allowed_by_dmarc_with_subdomain_policy_quarantine(self):
     """Testing is_email_allowed_by_dmarc with subdomain and
     policy=quarantine
     """
     self.dmarc_txt_records['_dmarc.example.com'] = \
         'v=DMARC1; p=reject; sp=quarantine;'
     self.assertFalse(is_email_allowed_by_dmarc('*****@*****.**'))
Example #3
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_pct_0(self):
     """Testing is_email_allowed_by_dmarc with domain 0% match"""
     self.dmarc_txt_records['_dmarc.example.com'] = \
         'v=DMARC1; p=reject; pct=0;'
     self.assertTrue(is_email_allowed_by_dmarc('*****@*****.**'))
Example #4
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_reject(self):
     """Testing is_email_allowed_by_dmarc with domain policy=reject"""
     self.dmarc_txt_records['_dmarc.example.com'] = 'v=DMARC1; p=reject;'
     self.assertFalse(is_email_allowed_by_dmarc('*****@*****.**'))
Example #5
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_none(self):
     """Testing is_email_allowed_by_dmarc with domain policy=none"""
     self.dmarc_txt_records['_dmarc.example.com'] = 'v=DMARC1; p=none;'
     self.assertTrue(is_email_allowed_by_dmarc('*****@*****.**'))
Example #6
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_pct_0(self):
     """Testing is_email_allowed_by_dmarc with domain 0% match"""
     self.dmarc_txt_records['_dmarc.example.com'] = \
         'v=DMARC1; p=reject; pct=0;'
     self.assertTrue(is_email_allowed_by_dmarc('*****@*****.**'))
Example #7
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_reject(self):
     """Testing is_email_allowed_by_dmarc with domain policy=reject"""
     self.dmarc_txt_records['_dmarc.example.com'] = 'v=DMARC1; p=reject;'
     self.assertFalse(is_email_allowed_by_dmarc('*****@*****.**'))
Example #8
0
 def test_is_email_allowed_by_dmarc_with_domain_policy_none(self):
     """Testing is_email_allowed_by_dmarc with domain policy=none"""
     self.dmarc_txt_records['_dmarc.example.com'] = 'v=DMARC1; p=none;'
     self.assertTrue(is_email_allowed_by_dmarc('*****@*****.**'))
Example #9
0
    def __init__(
        self,
        subject="",
        text_body="",
        html_body="",
        from_email=None,
        to=None,
        cc=None,
        bcc=None,
        sender=None,
        in_reply_to=None,
        headers=None,
        auto_generated=False,
        prevent_auto_responses=False,
        enable_smart_spoofing=None,
    ):
        """Create a new EmailMessage.

        Args:
            subject (unicode, optional):
                The subject of the message. Defaults to being blank (which
                MTAs might replace with "no subject".)

            text_body (unicode, optional):
                The body of the e-mail as plain text. Defaults to an empty
                string (allowing HTML-only e-mails to be sent).

            html_body (unicode, optional):
                The body of the e-mail as HTML. Defaults to an empty string
                (allowing text-only e-mails to be sent).

            from_email (unicode, optional):
                The from address for the e-mail. Defaults to
                :django:setting:`DEFAULT_FROM_EMAIL`.

            to (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive the e-mail. Defaults to an empty list of
                addresses (allowing using CC/BCC only).

            cc (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive a carbon copy of the e-mail, or ``None`` if
                there are no CC recipients.

            bcc (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive a blind carbon copy of the e-mail, or ``None``
                if there are not BCC recipients.

            sender (unicode, optional):
                The actual e-mail address sending this e-mail, for use in
                the :mailheader:`Sender` header. If this differs from
                ``from_email``, it will be left out of the header as per
                :rfc:`2822`.

                This will default to :django:setting:`DEFAULT_FROM_EMAIL`
                if unspecified.

            in_reply_to (unicode, optional):
                An optional message ID (which will be used as the value for the
                :mailheader:`In-Reply-To` and :mailheader:`References`
                headers). This will be generated if not provided and will be
                available as the :py:attr:`message_id` attribute after the
                e-mail has been sent.

            headers (django.utils.datastructures.MultiValueDict, optional):
                Extra headers to provide with the e-mail.

            auto_generated (bool, optional):
                If ``True``, the e-mail will contain headers that mark it as
                an auto-generated message (as per :rfc:`3834`) to avoid auto
                replies.

            prevent_auto_responses (bool, optional):
                If ``True``, the e-mail will contain headers to prevent auto
                replies for delivery reports, read receipts, out of office
                e-mails, and other auto-generated e-mails from Exchange.

            enable_smart_spoofing (bool, optional):
                Whether to enable smart spoofing of any e-mail addresses for
                the :mailheader:`From` header.

                This defaults to ``settings.EMAIL_ENABLE_SMART_SPOOFING``
                (which itself defaults to ``False``).
        """
        headers = headers or MultiValueDict()

        if isinstance(headers, dict) and not isinstance(headers, MultiValueDict):
            # Instantiating a MultiValueDict from a dict does not ensure that
            # values are lists, so we have to ensure that ourselves.
            headers = MultiValueDict(dict((key, [value]) for key, value in six.iteritems(headers)))

        if in_reply_to:
            headers["In-Reply-To"] = in_reply_to
            headers["References"] = in_reply_to

        headers["Reply-To"] = from_email

        if enable_smart_spoofing is None:
            enable_smart_spoofing = getattr(settings, "EMAIL_ENABLE_SMART_SPOOFING", False)

        # Figure out the From/Sender we'll be wanting to use.
        if not sender:
            sender = settings.DEFAULT_FROM_EMAIL

        if sender == from_email:
            # RFC 2822 section 3.6.2 states that we should only include Sender
            # if the two are not equal. We also know that we're not spoofing,
            # so e-mail sending should work fine here.
            sender = None
        elif enable_smart_spoofing:
            # We will be checking the DMARC record from the e-mail address
            # we'd be ideally sending on behalf of. If the record indicates
            # that the message has any likelihood of being quarantined or
            # rejected, we'll alter the From field to send using our Sender
            # address instead.
            parsed_from_name, parsed_from_email = parseaddr(from_email)
            parsed_sender_name, parsed_sender_email = parseaddr(sender)

            # The above will return ('', '') if the address couldn't be parsed,
            # so check for this.
            if not parsed_from_email:
                logging.warning("EmailMessage: Unable to parse From address " '"%s"', from_email)

            if not parsed_sender_email:
                logging.warning("EmailMessage: Unable to parse Sender address " '"%s"', sender)

            # We may not be allowed to send on behalf of this user.
            # We actually aren't going to check for this (it may be due
            # to SPF, which is too complex for us to want to check, or
            # it may be due to another ruleset somewhere). Instead, just
            # check if this e-mail could get lost due to the DMARC rules.
            if parsed_from_email != parsed_sender_email and not is_email_allowed_by_dmarc(parsed_from_email):
                # We can't spoof the e-mail address, so instead, we'll keep
                # the e-mail in Reply To and create a From address we own,
                # which will also indicate what service is sending on behalf
                # of the user.
                from_email = build_email_address_via_service(
                    full_name=parsed_from_name, email=parsed_from_email, sender_email=parsed_sender_email
                )

        if sender:
            headers["Sender"] = sender
            headers["X-Sender"] = sender

        if auto_generated:
            headers["Auto-Submitted"] = "auto-generated"

        if prevent_auto_responses:
            headers["X-Auto-Response-Suppress"] = "DR, RN, OOF, AutoReply"

        # We're always going to explicitly send with the DEFAULT_FROM_EMAIL,
        # but replace the From header with the e-mail address we decided on.
        # While this class and its parent classes don't really care about the
        # difference between these, Django's SMTP e-mail sending machinery
        # treats them differently, sending the value of EmailMessage.from_email
        # when communicating with the SMTP server.
        super(EmailMessage, self).__init__(
            subject=subject,
            body=text_body,
            from_email=settings.DEFAULT_FROM_EMAIL,
            to=to,
            cc=cc,
            bcc=bcc,
            headers={"From": from_email},
        )

        self.message_id = None

        # We don't want to use the regular extra_headers attribute because
        # it will be treated as a plain dict by Django. Instead, since we're
        # using a MultiValueDict, we store it in a separate attribute
        # attribute and handle adding our headers in the message method.
        self._headers = headers

        if html_body:
            self.attach_alternative(html_body, "text/html")
Example #10
0
    def __init__(self,
                 subject='',
                 text_body='',
                 html_body='',
                 from_email=None,
                 to=None,
                 cc=None,
                 bcc=None,
                 sender=None,
                 in_reply_to=None,
                 headers=None,
                 auto_generated=False,
                 prevent_auto_responses=False,
                 enable_smart_spoofing=None):
        """Create a new EmailMessage.

        Args:
            subject (unicode, optional):
                The subject of the message. Defaults to being blank (which
                MTAs might replace with "no subject".)

            text_body (unicode, optional):
                The body of the e-mail as plain text. Defaults to an empty
                string (allowing HTML-only e-mails to be sent).

            html_body (unicode, optional):
                The body of the e-mail as HTML. Defaults to an empty string
                (allowing text-only e-mails to be sent).

            from_email (unicode, optional):
                The from address for the e-mail. Defaults to
                :django:setting:`DEFAULT_FROM_EMAIL`.

            to (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive the e-mail. Defaults to an empty list of
                addresses (allowing using CC/BCC only).

            cc (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive a carbon copy of the e-mail, or ``None`` if
                there are no CC recipients.

            bcc (list, optional):
                A list of e-mail addresses as :py:class:`unicode` objects that
                are to receive a blind carbon copy of the e-mail, or ``None``
                if there are not BCC recipients.

            sender (unicode, optional):
                The actual e-mail address sending this e-mail, for use in
                the :mailheader:`Sender` header. If this differs from
                ``from_email``, it will be left out of the header as per
                :rfc:`2822`.

                This will default to :django:setting:`DEFAULT_FROM_EMAIL`
                if unspecified.

            in_reply_to (unicode, optional):
                An optional message ID (which will be used as the value for the
                :mailheader:`In-Reply-To` and :mailheader:`References`
                headers). This will be generated if not provided and will be
                available as the :py:attr:`message_id` attribute after the
                e-mail has been sent.

            headers (django.utils.datastructures.MultiValueDict, optional):
                Extra headers to provide with the e-mail.

            auto_generated (bool, optional):
                If ``True``, the e-mail will contain headers that mark it as
                an auto-generated message (as per :rfc:`3834`) to avoid auto
                replies.

            prevent_auto_responses (bool, optional):
                If ``True``, the e-mail will contain headers to prevent auto
                replies for delivery reports, read receipts, out of office
                e-mails, and other auto-generated e-mails from Exchange.

            enable_smart_spoofing (bool, optional):
                Whether to enable smart spoofing of any e-mail addresses for
                the :mailheader:`From` header.

                This defaults to ``settings.EMAIL_ENABLE_SMART_SPOOFING``
                (which itself defaults to ``False``).
        """
        headers = headers or MultiValueDict()

        if (isinstance(headers, dict)
                and not isinstance(headers, MultiValueDict)):
            # Instantiating a MultiValueDict from a dict does not ensure that
            # values are lists, so we have to ensure that ourselves.
            headers = MultiValueDict(
                dict((key, [value]) for key, value in six.iteritems(headers)))

        if in_reply_to:
            headers['In-Reply-To'] = in_reply_to
            headers['References'] = in_reply_to

        headers['Reply-To'] = from_email

        if enable_smart_spoofing is None:
            enable_smart_spoofing = \
                getattr(settings, 'EMAIL_ENABLE_SMART_SPOOFING', False)

        # Figure out the From/Sender we'll be wanting to use.
        if not sender:
            sender = settings.DEFAULT_FROM_EMAIL

        if sender == from_email:
            # RFC 2822 section 3.6.2 states that we should only include Sender
            # if the two are not equal. We also know that we're not spoofing,
            # so e-mail sending should work fine here.
            sender = None
        elif enable_smart_spoofing:
            # We will be checking the DMARC record from the e-mail address
            # we'd be ideally sending on behalf of. If the record indicates
            # that the message has any likelihood of being quarantined or
            # rejected, we'll alter the From field to send using our Sender
            # address instead.
            parsed_from_name, parsed_from_email = parseaddr(from_email)
            parsed_sender_name, parsed_sender_email = parseaddr(sender)

            # The above will return ('', '') if the address couldn't be parsed,
            # so check for this.
            if not parsed_from_email:
                logger.warning(
                    'EmailMessage: Unable to parse From address '
                    '"%s"', from_email)

            if not parsed_sender_email:
                logger.warning(
                    'EmailMessage: Unable to parse Sender address '
                    '"%s"', sender)

            # We may not be allowed to send on behalf of this user.
            # We actually aren't going to check for this (it may be due
            # to SPF, which is too complex for us to want to check, or
            # it may be due to another ruleset somewhere). Instead, just
            # check if this e-mail could get lost due to the DMARC rules.
            if (parsed_from_email != parsed_sender_email
                    and not is_email_allowed_by_dmarc(parsed_from_email)):
                # We can't spoof the e-mail address, so instead, we'll keep
                # the e-mail in Reply To and create a From address we own,
                # which will also indicate what service is sending on behalf
                # of the user.
                from_email = build_email_address_via_service(
                    full_name=parsed_from_name,
                    email=parsed_from_email,
                    sender_email=parsed_sender_email)

        if sender:
            headers['Sender'] = sender
            headers['X-Sender'] = sender

        if auto_generated:
            headers['Auto-Submitted'] = 'auto-generated'

        if prevent_auto_responses:
            headers['X-Auto-Response-Suppress'] = 'DR, RN, OOF, AutoReply'

        # We're always going to explicitly send with the DEFAULT_FROM_EMAIL,
        # but replace the From header with the e-mail address we decided on.
        # While this class and its parent classes don't really care about the
        # difference between these, Django's SMTP e-mail sending machinery
        # treats them differently, sending the value of EmailMessage.from_email
        # when communicating with the SMTP server.
        super(EmailMessage,
              self).__init__(subject=subject,
                             body=text_body,
                             from_email=settings.DEFAULT_FROM_EMAIL,
                             to=to,
                             cc=cc,
                             bcc=bcc,
                             headers={
                                 'From': from_email,
                             })

        self.message_id = None

        # We don't want to use the regular extra_headers attribute because
        # it will be treated as a plain dict by Django. Instead, since we're
        # using a MultiValueDict, we store it in a separate attribute
        # attribute and handle adding our headers in the message method.
        self._headers = headers

        if html_body:
            self.attach_alternative(html_body, 'text/html')