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('*****@*****.**'))
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('*****@*****.**'))
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('*****@*****.**'))
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('*****@*****.**'))
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")
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')