ソースを参照

Merge pull request #72 from Miksus/dev/custom_headers

ENH: Custom headers
Mikael Koli 3 年 前
コミット
0e72aa08f3

+ 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 
     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 
-    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::
 
@@ -122,6 +141,37 @@ Alias is an alternative text that is displayed instead of
 the actual email addresses. The receivers can still get 
 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:
 
 Sending Multiple Emails

+ 3 - 3
docs/tutorials/testing.rst

@@ -44,7 +44,7 @@ in tests:
     Subject: Some news
     To: you@example.com
     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-Transfer-Encoding: 7bit
     MIME-Version: 1.0
@@ -114,7 +114,7 @@ Then to use this mock:
     Subject: Some news
     To: you@example.com
     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-Transfer-Encoding: 7bit
     MIME-Version: 1.0
@@ -163,7 +163,7 @@ Then to use this class:
     Subject: Some news
     To: you@example.com
     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-Transfer-Encoding: 7bit
     MIME-Version: 1.0

+ 1 - 0
docs/versions.rst

@@ -6,6 +6,7 @@ Version history
 
 - ``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, ``Date: ...``.
     - 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
         Jinja environment used for loading templates
         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
         Keyword arguments passed to ``cls_smtp``
         when connecting to the SMTP server.
@@ -171,6 +175,7 @@ class EmailSender:
         self.cc = None
         self.bcc = None
         self.subject = None
+        self.headers = None
 
         self.text = None
         self.html = None
@@ -191,6 +196,7 @@ class EmailSender:
              receivers:Union[List[str], str, None]=None,
              cc:Union[List[str], str, None]=None,
              bcc:Union[List[str], str, None]=None,
+             headers:Optional[Dict[str, str]]=None,
              html:Optional[str]=None,
              text:Optional[str]=None,
              html_template:Optional[str]=None,
@@ -219,6 +225,10 @@ class EmailSender:
             Blind Carbon Copy of the email.
             Additional recipients of the email that
             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 body of the email. This is processed
             by Jinja and may contain loops, parametrization
@@ -294,6 +304,7 @@ class EmailSender:
             receivers=receivers,
             cc=cc,
             bcc=bcc,
+            headers=headers,
             html=html,
             text=text,
             html_template=html_template,
@@ -320,6 +331,7 @@ class EmailSender:
                   body_tables:Optional[Dict[str, 'pd.DataFrame']]=None, 
                   body_params:Optional[Dict[str, Any]]=None,
                   attachments:Optional[Dict[str, Union[str, os.PathLike, 'pd.DataFrame', bytes]]]=None,
+                  headers:Optional[Dict[str, str]]=None,
                   use_jinja=None) -> EmailMessage:
         """Get the email message"""
 
@@ -329,6 +341,7 @@ class EmailSender:
         receivers = self.get_receivers(receivers)
         cc = self.get_cc(cc)
         bcc = self.get_bcc(bcc)
+        headers = self.get_headers(headers)
 
         html = html or self.html
         text = text or self.text
@@ -345,6 +358,7 @@ class EmailSender:
             receivers=receivers,
             cc=cc,
             bcc=bcc,
+            headers=headers 
         )
         has_text = text is not None or text_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"""
         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:
         """Get sender of the email"""
         return sender or self.sender or self.username
@@ -410,25 +428,35 @@ class EmailSender:
     def create_message_id(self) -> str:
         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["From"] = sender
-        msg["Subject"] = subject
-        
+
+        email_headers = {
+            "From": sender,
+            "Subject": subject,
+        }
+
         # To whoom the email goes
         if receivers:
-            msg["To"] = receivers
+            email_headers["To"] = receivers
         if cc:
-            msg['Cc'] = cc
+            email_headers['Cc'] = cc
         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
 
     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 re
 
+import pytest
+
 from redmail import EmailSender
 
 from convert import remove_email_content_id, prune_generated_headers
@@ -72,3 +74,65 @@ def test_cc_bcc():
     Date: <date>
 
     """)[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:]