فهرست منبع

fix: add Message-ID header

Mikael Koli 3 سال پیش
والد
کامیت
a9526a0344

+ 12 - 0
redmail/email/sender.py

@@ -1,6 +1,7 @@
 
 from copy import copy
 from email.message import EmailMessage
+from email.utils import make_msgid
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 import warnings
 
@@ -391,6 +392,12 @@ class EmailSender:
         """Get sender of the email"""
         return sender or self.sender or self.username
 
+    def create_message_id(self, sender:str) -> str:
+        domain = None
+        if sender is not None and '@' in sender:
+            domain = sender.split("@")[1]
+        return make_msgid(domain=domain)
+
     def _create_body(self, subject, sender, receivers=None, cc=None, bcc=None) -> EmailMessage:
         msg = EmailMessage()
         msg["from"] = sender
@@ -403,6 +410,11 @@ class EmailSender:
             msg['cc'] = cc
         if bcc:
             msg['bcc'] = bcc
+
+        # 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(sender)
         return msg
 
     def _set_content_type(self, msg:EmailMessage, has_text, has_html, has_attachments):

+ 26 - 9
redmail/test/email/test_body.py

@@ -7,7 +7,7 @@ from convert import remove_extra_lines, payloads_to_dict
 from getpass import getpass, getuser
 from platform import node
 
-from convert import remove_email_extra, remove_email_content_id
+from convert import remove_email_extra, remove_email_content_id, remove_email_message_id
 
 import platform
 PYTHON_VERSION = platform.sys.version_info
@@ -22,10 +22,12 @@ def test_text_message():
         subject="Some news",
         text="Hi, nice to meet you.",
     )
+    msg = remove_email_message_id(str(msg))
     assert str(msg) == dedent("""
     from: me@example.com
     subject: Some news
     to: you@example.com
+    Message-ID: <<message_id>>
     Content-Type: text/plain; charset="utf-8"
     Content-Transfer-Encoding: 7bit
     MIME-Version: 1.0
@@ -43,11 +45,12 @@ def test_html_message():
         subject="Some news",
         html="<h3>Hi,</h3><p>Nice to meet you</p>",
     )
-
+    msg = remove_email_message_id(str(msg))
     assert remove_email_content_id(str(msg)) == dedent("""
     from: me@example.com
     subject: Some news
     to: you@example.com
+    Message-ID: <<message_id>>
     Content-Type: multipart/mixed; boundary="===============<ID>=="
 
     --===============<ID>==
@@ -77,11 +80,12 @@ def test_text_and_html_message():
         html="<h3>Hi,</h3><p>nice to meet you.</p>",
         text="Hi, nice to meet you.",
     )
-
+    msg = remove_email_message_id(str(msg))
     assert remove_email_content_id(str(msg)) == dedent("""
     from: me@example.com
     subject: Some news
     to: you@example.com
+    Message-ID: <<message_id>>
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="===============<ID>=="
 
@@ -189,6 +193,7 @@ def test_without_jinja(use_jinja_obj, use_jinja):
     from: me@example.com
     subject: Some news
     to: you@example.com
+    Message-ID: <<message_id>>
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="===============<ID>=="
 
@@ -217,7 +222,8 @@ def test_without_jinja(use_jinja_obj, use_jinja):
     """)[1:]
     if IS_PY36:
         expected = expected.replace('sender.full_n=\n', 'sender.full_n')
-    assert remove_email_content_id(str(msg)) == expected
+    msg = remove_email_message_id(str(msg))
+    assert remove_email_content_id(msg) == expected
 
 
 def test_with_error():
@@ -254,24 +260,30 @@ def test_set_defaults():
     email.receivers = ['you@gmail.com', 'they@gmail.com']
     email.subject = "Some email"
     msg = email.get_message(text="Hi, an email")
+    headers = {
+        key: val if key not in ('Message-ID',) else '<ID>'
+        for key, val in msg.items()
+    }
     assert {
         'from': 'me@gmail.com', 
         'to': 'you@gmail.com, they@gmail.com', 
         'subject': 'Some email', 
         'Content-Type': 'text/plain; charset="utf-8"', 
         'Content-Transfer-Encoding': '7bit', 
-        'MIME-Version': '1.0'
-    } == dict(msg.items())
+        'MIME-Version': '1.0',
+        'Message-ID': '<ID>',
+    } == headers
 
 def test_cc_bcc():
     email = EmailSender(host=None, port=1234)
     msg = email.get_message(sender="me@example.com", subject="Some email", cc=['you@example.com'], bcc=['he@example.com', 'she@example.com'])
-
-    assert remove_email_content_id(str(msg)) == dedent("""
+    msg = remove_email_message_id(str(msg))
+    assert remove_email_content_id(msg) == dedent("""
     from: me@example.com
     subject: Some email
     cc: you@example.com
     bcc: he@example.com, she@example.com
+    Message-ID: <<message_id>>
 
     """)[1:]
 
@@ -296,7 +308,12 @@ def test_no_table_templates():
         text="An example",
         html="<h1>An example</h1>"
     )
-    assert dict(msg.items()) == {
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
+    assert headers == {
         'from': 'me@gmail.com', 
         'subject': 'Some news', 
         'to': 'you@gmail.com', 

+ 6 - 3
redmail/test/email/test_cookbook.py

@@ -3,7 +3,7 @@ from typing import Union
 from textwrap import dedent
 from redmail import EmailSender
 
-from convert import remove_email_content_id
+from convert import remove_email_content_id, remove_email_message_id
 
 def test_distributions():
     class DistrSender(EmailSender):
@@ -40,10 +40,13 @@ def test_distributions():
         cc="group2",
         subject="Some email",
     )
-    assert remove_email_content_id(str(msg)) == dedent("""
+    msg = remove_email_message_id(str(msg))
+    msg = remove_email_content_id(str(msg))
+    assert msg == dedent("""
     from: me@example.com
     subject: Some email
     to: me@example.com, you@example.com
     cc: he@example.com, she@example.com
-
+    Message-ID: <<message_id>>
+    
     """)[1:]

+ 15 - 3
redmail/test/email/test_inline_media.py

@@ -62,7 +62,11 @@ def test_with_image_file(get_image_obj, dummy_png):
     compare_image_mime(mime_image, mime_html, orig_image=dummy_bytes)
 
     # Test receivers etc.
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     assert {
         'from': 'me@gmail.com', 
         'subject': 'Some news', 
@@ -110,7 +114,11 @@ def test_with_image_dict_jpeg():
     compare_image_mime(mime_image, mime_html, orig_image=img_bytes, type_="image/jpg")
 
     # Test receivers etc.
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     assert {
         'from': 'me@gmail.com', 
         'subject': 'Some news', 
@@ -148,7 +156,11 @@ def test_with_image_obj(get_image_obj):
     compare_image_mime(mime_image, mime_html, orig_image=image_bytes)
 
     # Test receivers etc.
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     assert {
         'from': 'me@gmail.com', 
         'subject': 'Some news', 

+ 3 - 1
redmail/test/email/test_template.py

@@ -4,7 +4,7 @@ from redmail import EmailSender
 
 import pytest
 
-from convert import remove_email_extra, remove_email_content_id
+from convert import remove_email_extra, remove_email_content_id, remove_email_message_id
 from getpass import getpass, getuser
 from platform import node
 
@@ -66,11 +66,13 @@ def test_jinja_env(tmpdir):
         html="<h1>A param: {{ my_param }}</h1>"
     )
     content = str(msg)
+    content = remove_email_message_id(content)
     content = remove_email_content_id(content)
     assert content == dedent("""
     from: me@example.com
     subject: Some news
     to: you@example.com
+    Message-ID: <<message_id>>
     MIME-Version: 1.0
     Content-Type: multipart/mixed; boundary="===============<ID>=="
 

+ 3 - 0
redmail/test/helpers/convert.py

@@ -13,6 +13,9 @@ def remove_email_extra(s:str):
 def remove_email_content_id(s:str, repl="<ID>"):
     return re.sub(r"(?<================)[0-9]+(?===)", repl, s)
 
+def remove_email_message_id(s:str, repl="<message_id>"):
+    return re.sub(r"(?<=Message-ID: <).+?(?=>)", repl, s)
+
 def payloads_to_dict(*parts):
     data = {}
     for part in parts:

+ 5 - 1
redmail/test/log/test_handler.py

@@ -167,7 +167,11 @@ def test_emit(logger, kwargs, exp_headers, exp_payload):
     
     assert len(msgs) == 1
     msg = msgs[0]
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     payload = msg.get_payload()
 
     assert headers == exp_headers

+ 15 - 3
redmail/test/log/test_handler_multi.py

@@ -177,7 +177,11 @@ def test_emit(logger, kwargs, exp_headers, exp_payload):
 
     assert len(msgs) == 1
     msg = msgs[0]
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     payload = msg.get_payload()
 
     assert headers == exp_headers
@@ -206,7 +210,11 @@ def test_flush_multiple(logger):
 
     assert len(msgs) == 1
     msg = msgs[0]
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     text = msg.get_payload()
 
     assert headers == {
@@ -238,7 +246,11 @@ def test_flush_none():
 
     assert len(msgs) == 1
     msg = msgs[0]
-    headers = dict(msg.items())
+    headers = {
+        key: val
+        for key, val in msg.items()
+        if key not in ('Message-ID',)
+    }
     text = msg.get_payload()
 
     assert headers == {