浏览代码

Merge pull request #72 from Miksus/dev/custom_headers

ENH: Custom headers
Mikael Koli 3 年之前
父节点
当前提交
0e72aa08f3
共有 5 个文件被更改,包括 159 次插入16 次删除
  1. 51 1
      docs/tutorials/sending.rst
  2. 3 3
      docs/tutorials/testing.rst
  3. 1 0
      docs/versions.rst
  4. 40 12
      redmail/email/sender.py
  5. 64 0
      redmail/test/email/test_headers.py

+ 51 - 1
docs/tutorials/sending.rst

@@ -27,7 +27,26 @@ is configured. At minimum, sending an email requires:
     If you don't spesify the ``sender``, the sender is considered to 
     If you don't spesify the ``sender``, the sender is considered to 
     be ``email.sender``. If ``email.sender`` is also missing, the sender
     be ``email.sender``. If ``email.sender`` is also missing, the sender
     is then set to be ``email.username``. Ensure that any of these is a 
     is then set to be ``email.username``. Ensure that any of these is a 
-    valid email address. 
+    valid email address.
+
+    Similar flow of logic applies to most attributes. You can set defaults on the 
+    ``email`` instance which are used in case they are not passed via the 
+    method call ``email.send(...)``.
+
+    Here is an example:
+
+    .. code-block:: python
+
+        email = EmailSender(host='localhost', port=0)
+        email.subject = "email subject"
+        email.receivers = ["you@example.com"]
+        email.send(
+            sender="me@example.com",
+            subject="important email"
+        )
+
+    The above sends an email that has ``"important email"`` as the email subject
+    and the email is sent to address ``you@example.com``.
 
 
 .. note::
 .. note::
 
 
@@ -122,6 +141,37 @@ Alias is an alternative text that is displayed instead of
 the actual email addresses. The receivers can still get 
 the actual email addresses. The receivers can still get 
 the addresses though.
 the addresses though.
 
 
+.. _send-headers:
+
+Sending with Custom Headers
+---------------------------
+
+Sometimes you might want to override or add custom 
+email headers to your email. For example, you 
+might want to set a custom date for the email and 
+set it as important:
+
+.. code-block:: python
+
+    import datetime
+
+    email.send(
+        subject='email subject',
+        sender="The Sender <me@example.com>",
+        receivers=['you@example.com'],
+        headers={
+            "Importance": "high",
+            "Date": datetime.datetime(2021, 1, 31, 6, 56, 46)
+        }
+    )
+
+Read more about email headers from `IANA's website <https://www.iana.org/assignments/message-headers/message-headers.xhtml>`_.
+
+.. note::
+
+    Headers passed this way can override the other headers such 
+    as ``From``, ``To``, ``Cc``, ``Bcc``, ``Date`` and ``Message-ID``.
+
 .. _send-multi:
 .. _send-multi:
 
 
 Sending Multiple Emails
 Sending Multiple Emails

+ 3 - 3
docs/tutorials/testing.rst

@@ -44,7 +44,7 @@ in tests:
     Subject: Some news
     Subject: Some news
     To: you@example.com
     To: you@example.com
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
-    Date: Dec, 31 Jan 2021 06:56:46 -0000
+    Date: Sun, 31 Jan 2021 06:56:46 -0000
     Content-Type: text/plain; charset="utf-8"
     Content-Type: text/plain; charset="utf-8"
     Content-Transfer-Encoding: 7bit
     Content-Transfer-Encoding: 7bit
     MIME-Version: 1.0
     MIME-Version: 1.0
@@ -114,7 +114,7 @@ Then to use this mock:
     Subject: Some news
     Subject: Some news
     To: you@example.com
     To: you@example.com
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
-    Date: Dec, 31 Jan 2021 06:56:46 -0000
+    Date: Sun, 31 Jan 2021 06:56:46 -0000
     Content-Type: text/plain; charset="utf-8"
     Content-Type: text/plain; charset="utf-8"
     Content-Transfer-Encoding: 7bit
     Content-Transfer-Encoding: 7bit
     MIME-Version: 1.0
     MIME-Version: 1.0
@@ -163,7 +163,7 @@ Then to use this class:
     Subject: Some news
     Subject: Some news
     To: you@example.com
     To: you@example.com
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
     Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
-    Date: Dec, 31 Jan 2021 06:56:46 -0000
+    Date: Sun, 31 Jan 2021 06:56:46 -0000
     Content-Type: text/plain; charset="utf-8"
     Content-Type: text/plain; charset="utf-8"
     Content-Transfer-Encoding: 7bit
     Content-Transfer-Encoding: 7bit
     MIME-Version: 1.0
     MIME-Version: 1.0

+ 1 - 0
docs/versions.rst

@@ -6,6 +6,7 @@ Version history
 
 
 - ``0.5.0``
 - ``0.5.0``
 
 
+    - Add: Option to set custom email headers.
     - Add: New header, ``Message-ID: ...``. Sending emails via Gmail may fail without it as of 2022. 
     - Add: New header, ``Message-ID: ...``. Sending emails via Gmail may fail without it as of 2022. 
     - Add: New header, ``Date: ...``.
     - Add: New header, ``Date: ...``.
     - Fix: Capitalized email headers including ``From``, ``To`` and ``Subject``.
     - Fix: Capitalized email headers including ``From``, ``To`` and ``Subject``.

+ 40 - 12
redmail/email/sender.py

@@ -107,6 +107,10 @@ class EmailSender:
     templates_text_table : jinja2.Environment
     templates_text_table : jinja2.Environment
         Jinja environment used for loading templates
         Jinja environment used for loading templates
         for table styling for text bodies.
         for table styling for text bodies.
+    headers : dict
+        Additional email headers. Will also override
+        the other generated email headers such as
+        ``From:``, ``To`` and ``Date:``.
     kws_smtp : dict
     kws_smtp : dict
         Keyword arguments passed to ``cls_smtp``
         Keyword arguments passed to ``cls_smtp``
         when connecting to the SMTP server.
         when connecting to the SMTP server.
@@ -171,6 +175,7 @@ class EmailSender:
         self.cc = None
         self.cc = None
         self.bcc = None
         self.bcc = None
         self.subject = None
         self.subject = None
+        self.headers = None
 
 
         self.text = None
         self.text = None
         self.html = None
         self.html = None
@@ -191,6 +196,7 @@ class EmailSender:
              receivers:Union[List[str], str, None]=None,
              receivers:Union[List[str], str, None]=None,
              cc:Union[List[str], str, None]=None,
              cc:Union[List[str], str, None]=None,
              bcc:Union[List[str], str, None]=None,
              bcc:Union[List[str], str, None]=None,
+             headers:Optional[Dict[str, str]]=None,
              html:Optional[str]=None,
              html:Optional[str]=None,
              text:Optional[str]=None,
              text:Optional[str]=None,
              html_template:Optional[str]=None,
              html_template:Optional[str]=None,
@@ -219,6 +225,10 @@ class EmailSender:
             Blind Carbon Copy of the email.
             Blind Carbon Copy of the email.
             Additional recipients of the email that
             Additional recipients of the email that
             don't see who else got the email.
             don't see who else got the email.
+        headers : dict, optional
+            Additional email headers. Will also override
+            the other generated email headers such as
+            ``From:``, ``To`` and ``Date:``.
         html : str, optional
         html : str, optional
             HTML body of the email. This is processed
             HTML body of the email. This is processed
             by Jinja and may contain loops, parametrization
             by Jinja and may contain loops, parametrization
@@ -294,6 +304,7 @@ class EmailSender:
             receivers=receivers,
             receivers=receivers,
             cc=cc,
             cc=cc,
             bcc=bcc,
             bcc=bcc,
+            headers=headers,
             html=html,
             html=html,
             text=text,
             text=text,
             html_template=html_template,
             html_template=html_template,
@@ -320,6 +331,7 @@ class EmailSender:
                   body_tables:Optional[Dict[str, 'pd.DataFrame']]=None, 
                   body_tables:Optional[Dict[str, 'pd.DataFrame']]=None, 
                   body_params:Optional[Dict[str, Any]]=None,
                   body_params:Optional[Dict[str, Any]]=None,
                   attachments:Optional[Dict[str, Union[str, os.PathLike, 'pd.DataFrame', bytes]]]=None,
                   attachments:Optional[Dict[str, Union[str, os.PathLike, 'pd.DataFrame', bytes]]]=None,
+                  headers:Optional[Dict[str, str]]=None,
                   use_jinja=None) -> EmailMessage:
                   use_jinja=None) -> EmailMessage:
         """Get the email message"""
         """Get the email message"""
 
 
@@ -329,6 +341,7 @@ class EmailSender:
         receivers = self.get_receivers(receivers)
         receivers = self.get_receivers(receivers)
         cc = self.get_cc(cc)
         cc = self.get_cc(cc)
         bcc = self.get_bcc(bcc)
         bcc = self.get_bcc(bcc)
+        headers = self.get_headers(headers)
 
 
         html = html or self.html
         html = html or self.html
         text = text or self.text
         text = text or self.text
@@ -345,6 +358,7 @@ class EmailSender:
             receivers=receivers,
             receivers=receivers,
             cc=cc,
             cc=cc,
             bcc=bcc,
             bcc=bcc,
+            headers=headers 
         )
         )
         has_text = text is not None or text_template is not None
         has_text = text is not None or text_template is not None
         has_html = html is not None or html_template is not None
         has_html = html is not None or html_template is not None
@@ -403,6 +417,10 @@ class EmailSender:
         """Get blind carbon copy (bcc) of the email"""
         """Get blind carbon copy (bcc) of the email"""
         return bcc or self.bcc
         return bcc or self.bcc
 
 
+    def get_headers(self, headers:Union[Dict[str, str], None]):
+        """Get additional headers"""
+        return headers or self.headers
+
     def get_sender(self, sender:Union[str, None]) -> str:
     def get_sender(self, sender:Union[str, None]) -> str:
         """Get sender of the email"""
         """Get sender of the email"""
         return sender or self.sender or self.username
         return sender or self.sender or self.username
@@ -410,25 +428,35 @@ class EmailSender:
     def create_message_id(self) -> str:
     def create_message_id(self) -> str:
         return make_msgid(domain=self.domain)
         return make_msgid(domain=self.domain)
 
 
-    def _create_body(self, subject, sender, receivers=None, cc=None, bcc=None) -> EmailMessage:
+    def _create_body(self, subject, sender, receivers=None, cc=None, bcc=None, headers=None) -> EmailMessage:
         msg = EmailMessage()
         msg = EmailMessage()
-        msg["From"] = sender
-        msg["Subject"] = subject
-        
+
+        email_headers = {
+            "From": sender,
+            "Subject": subject,
+        }
+
         # To whoom the email goes
         # To whoom the email goes
         if receivers:
         if receivers:
-            msg["To"] = receivers
+            email_headers["To"] = receivers
         if cc:
         if cc:
-            msg['Cc'] = cc
+            email_headers['Cc'] = cc
         if bcc:
         if bcc:
-            msg['Bcc'] = bcc
+            email_headers['Bcc'] = bcc
+
+        email_headers.update({
+            # Message-IDs could be produced by the first mail server
+            # or the program sending the email (as we are doing now).
+            # Apparently Gmail might require it as of 2022
+            "Message-ID": self.create_message_id(),
 
 
-        # Message-IDs could be produced by the first mail server
-        # or the program sending the email (as we are doing now).
-        # Apparently Gmail might require it as of 2022
-        msg['Message-ID'] = self.create_message_id()
+            "Date": formatdate(),
+        })
 
 
-        msg['Date'] = formatdate()
+        if headers:
+            email_headers.update(headers)
+        for key, val in email_headers.items():
+            msg[key] = val
         return msg
         return msg
 
 
     def _set_content_type(self, msg:EmailMessage, has_text, has_html, has_attachments):
     def _set_content_type(self, msg:EmailMessage, has_text, has_html, has_attachments):

+ 64 - 0
redmail/test/email/test_headers.py

@@ -4,6 +4,8 @@ from textwrap import dedent
 import sys
 import sys
 import re
 import re
 
 
+import pytest
+
 from redmail import EmailSender
 from redmail import EmailSender
 
 
 from convert import remove_email_content_id, prune_generated_headers
 from convert import remove_email_content_id, prune_generated_headers
@@ -72,3 +74,65 @@ def test_cc_bcc():
     Date: <date>
     Date: <date>
 
 
     """)[1:]
     """)[1:]
+
+@pytest.mark.parametrize("how", ["instance", "email"])
+def test_custom_headers(how):
+    email = EmailSender(host=None, port=1234)
+    headers = {"Importance": "high"}
+
+    if IS_PY37:
+        # Python <=3.7 has problems with domain names with UTF-8
+        # This is mostly problem with CI.
+        # We simulate realistic domain name
+        domain = "REDMAIL-1234.mail.com"
+        email.domain = domain
+
+    if how == "email":
+        msg = email.get_message(
+            sender="me@example.com", 
+            subject="Some email", 
+            headers=headers
+        )
+    elif how == "instance":
+        email.headers = headers
+        msg = email.get_message(
+            sender="me@example.com", 
+            subject="Some email",
+        )
+    msg = prune_generated_headers(str(msg))
+    assert remove_email_content_id(msg) == dedent("""
+    From: me@example.com
+    Subject: Some email
+    Message-ID: <<message_id>>
+    Date: <date>
+    Importance: high
+
+    """)[1:]
+
+@pytest.mark.parametrize("how", ["instance", "email"])
+def test_custom_headers_override(how):
+    email = EmailSender(host=None, port=1234)
+    headers = {
+        "Date": datetime.datetime(2021, 1, 31, 6, 56, 46, tzinfo=datetime.timezone.utc),
+        "Message-ID": "<167294165062.31860.1664530310632362057@LAPTOP-1234GML0>"
+    }
+
+    if how == "email":
+        msg = email.get_message(
+            sender="me@example.com", 
+            subject="Some email",
+            headers=headers
+        )
+    elif how == "instance":
+        email.headers = headers
+        msg = email.get_message(
+            sender="me@example.com",
+            subject="Some email",
+        )
+    assert str(msg) == dedent("""
+    From: me@example.com
+    Subject: Some email
+    Message-ID: <167294165062.31860.1664530310632362057@LAPTOP-1234GML0>
+    Date: Sun, 31 Jan 2021 06:56:46 +0000
+
+    """)[1:]