From d04712a35dfd69177cfc2f37850703221065ce07 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Wed, 15 Dec 2021 00:35:44 +0200 Subject: [PATCH 01/14] Add support for dkim --- .gitignore | 1 + yagmail/dkim.py | 32 ++++++++++++++++++++++++++++++++ yagmail/message.py | 6 ++++++ yagmail/sender.py | 2 ++ 4 files changed, 41 insertions(+) create mode 100644 yagmail/dkim.py diff --git a/.gitignore b/.gitignore index 087b075..edede3a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ docs/_static/* .coverage .coverage.* .coveralls.yml +.idea diff --git a/yagmail/dkim.py b/yagmail/dkim.py new file mode 100644 index 0000000..489cd28 --- /dev/null +++ b/yagmail/dkim.py @@ -0,0 +1,32 @@ +from typing import NamedTuple + +import dkim + + +class DKIM(NamedTuple): + domain: str + private_key: str + include_headers: list + selector: str + + +class DKIMHandler: + + def __init__(self, dkim: DKIM): + self.dkim = dkim + + def add_dkim_sig_to_message(self, msg) -> None: + # Based on example from: + # https://github.com/russellballestrini/russell.ballestrini.net/blob/master/content/ + # 2018-06-04-quickstart-to-dkim-sign-email-with-python.rst + sig = dkim.sign( + message=msg.as_bytes(), + selector=self.dkim.selector, + domain=self.dkim.domain, + privkey=self.dkim.private_key, + include_headers=self.dkim.include_headers, + ) + # add the dkim signature to the email message headers. + # decode the signature back to string_type because later on + # the call to msg.as_string() performs it's own bytes encoding... + msg["DKIM-Signature"] = sig[len("DKIM-Signature: "):].decode() diff --git a/yagmail/message.py b/yagmail/message.py index 7b77d0a..c4232a9 100644 --- a/yagmail/message.py +++ b/yagmail/message.py @@ -10,6 +10,7 @@ from email.mime.text import MIMEText from email.utils import formatdate +from yagmail.dkim import DKIMHandler from yagmail.headers import add_message_id from yagmail.headers import add_recipients_headers from yagmail.headers import add_subject @@ -49,6 +50,7 @@ def prepare_message( prettify_html=True, message_id=None, group_messages=True, + dkim=None, ): # check if closed!!!!!! XXX """ Prepare a MIME message """ @@ -146,6 +148,10 @@ def prepare_message( msg_related.get_payload()[0] = MIMEText(htmlstr, "html", _charset=encoding) msg_alternative.attach(MIMEText("\n".join(altstr), _charset=encoding)) msg_alternative.attach(msg_related) + + if dkim: + DKIMHandler(dkim).add_dkim_sig_to_message(msg) + return msg diff --git a/yagmail/sender.py b/yagmail/sender.py index be970e5..9aee883 100644 --- a/yagmail/sender.py +++ b/yagmail/sender.py @@ -32,6 +32,7 @@ def __init__( encoding="utf-8", oauth2_file=None, soft_email_validation=True, + dkim=None, **kwargs ): self.log = get_logger() @@ -62,6 +63,7 @@ def __init__( self.num_mail_sent = 0 self.oauth2_file = oauth2_file self.credentials = password if oauth2_file is None else oauth2_info + self.dkim = dkim def __enter__(self): return self From 821cfdfb2274c26c447f5ef9b5a188b156e78e7a Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Wed, 15 Dec 2021 01:32:12 +0200 Subject: [PATCH 02/14] added tests --- tests/privkey.pem | 15 +++++++++++++++ tests/test_dkim.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ yagmail/dkim.py | 6 +++--- yagmail/message.py | 2 +- yagmail/sender.py | 3 +++ 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 tests/privkey.pem create mode 100644 tests/test_dkim.py diff --git a/tests/privkey.pem b/tests/privkey.pem new file mode 100644 index 0000000..6b42dc3 --- /dev/null +++ b/tests/privkey.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWgIBAAKBgG6DGHi2lU6cQ+Kq3LWOQHhlZvnLRNqZj/uFg/ddoRvj23lieHJy +58ZIVUg72bbJ6TYw9C0kO3N50FkEA4DMfzG6jK+TQRxeZWxk2v9Z8s6I+wXsbStq +sEj5xBWRHZP3x753fhdt6kao0EjhrpJtU8RF/dy+zzC3OvYiyMJ8/G1zAgMBAAEC +gYA/wd5ydlQ5oCoCMbVYAZPsYgRBBSbaP1I0OFlkc14pqxHKPCdlUCmr2btqCy2W +KXMk0qVtDcRG4PZ1BL3IpzKSCOVjS5ysGMGmZF5wbz45C+Nsgg3lu9BuZwe1cXev +2LStu85VH6vl+qip8PNumbcaUi497mdsE3OU5qO4KS3ggQJBALuJe5dSAofo9C0Q +ECo2PdKoq8eJIpwUIOQqnFiTXyOTBY7VPZUFA+LDZUqPZN/rcGTo1QKeEOq65U1i +VDI0tCECQQCW2yV2AoVxDKMTiWpm2Hjdc//yzGR/bq7hB/Rbue5Nfh0ZKhVt6WE5 +dZLOE7YfgfKdw5rmCgbnYdo/KvAl4S8TAkBoW5AH254G7U37SYYFR1vNvwigJ6K3 +wcev0DpiW9fEXwrnuafkJf9Mj7js7bCPnl9T9/CY3UbCqC/ziPUbXxKhAkADmsz0 +Wu8RWnnzF4+BUZ7hu8nb5WJpYR2wpt4B6xl0pJTRax4D+FWoZ9TQM8xgSFyhxHDJ +Gmg70yzB4abHYPAdAkB1ORFiu2Xntfu4T0KSMSYJ7Am3RwrdU8WtPYucC4BP3K9c +PdD93pcawKK3T9GgWRjrypq3jua6hN+00OHX/4uD +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/test_dkim.py b/tests/test_dkim.py new file mode 100644 index 0000000..cf9c4bf --- /dev/null +++ b/tests/test_dkim.py @@ -0,0 +1,44 @@ +from pathlib import Path +from unittest.mock import Mock + + +def test_email_with_dkim(): + from yagmail import SMTP + from yagmail.dkim import DKIM + + private_key_path = Path(__file__).parent / "privkey.pem" + + private_key = private_key_path.read_bytes() + + dkim = DKIM( + domain=b"a.com", + selector=b"selector", + private_key=private_key, + include_headers=[b"To", b"From", b"Subject"] + ) + yag = SMTP( + user="a@a.com", + host="smtp.blabla.com", + port=25, + dkim=dkim, + ) + + yag.login = Mock() + + to = "b@b.com" + + recipients, msg_string = yag.send( + to=to, + subject="hello from tests", + contents="important message", + preview_only=True + ) + + assert recipients == [to] + + dkim_string1 = "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=a.com; i=@a.com;\n " \ + "q=dns/txt; s=selector; t=" + assert dkim_string1 in msg_string + + dkim_string2 = "h=to : from : subject;" + assert dkim_string2 in msg_string diff --git a/yagmail/dkim.py b/yagmail/dkim.py index 489cd28..a821411 100644 --- a/yagmail/dkim.py +++ b/yagmail/dkim.py @@ -4,10 +4,10 @@ class DKIM(NamedTuple): - domain: str - private_key: str + domain: bytes + private_key: bytes include_headers: list - selector: str + selector: bytes class DKIMHandler: diff --git a/yagmail/message.py b/yagmail/message.py index c4232a9..cd257fa 100644 --- a/yagmail/message.py +++ b/yagmail/message.py @@ -151,7 +151,7 @@ def prepare_message( if dkim: DKIMHandler(dkim).add_dkim_sig_to_message(msg) - + return msg diff --git a/yagmail/sender.py b/yagmail/sender.py index 9aee883..762cc2c 100644 --- a/yagmail/sender.py +++ b/yagmail/sender.py @@ -112,6 +112,7 @@ def prepare_send( prettify_html=True, message_id=None, group_messages=True, + dkim=None, ): addresses = resolve_addresses(self.user, self.useralias, to, cc, bcc) @@ -131,6 +132,7 @@ def prepare_send( prettify_html, message_id, group_messages, + dkim, ) recipients = addresses["recipients"] @@ -164,6 +166,7 @@ def send( prettify_html, message_id, group_messages, + self.dkim, ) if preview_only: return (recipients, msg_string) From a10c89626a64d3f522124e18f32a91a656ac91c1 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Wed, 15 Dec 2021 01:34:04 +0200 Subject: [PATCH 03/14] added tests --- tests/test_dkim.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_dkim.py b/tests/test_dkim.py index cf9c4bf..fe33d31 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -42,3 +42,5 @@ def test_email_with_dkim(): dkim_string2 = "h=to : from : subject;" assert dkim_string2 in msg_string + + assert "Subject: hello from tests" in msg_string \ No newline at end of file From eb8e4fe4741b25b77b7775586911fc7055609c32 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Wed, 15 Dec 2021 01:40:32 +0200 Subject: [PATCH 04/14] added tests --- tests/test_dkim.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_dkim.py b/tests/test_dkim.py index fe33d31..78dc874 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -1,3 +1,4 @@ +import base64 from pathlib import Path from unittest.mock import Mock @@ -35,6 +36,9 @@ def test_email_with_dkim(): ) assert recipients == [to] + assert "Subject: hello from tests" in msg_string + text_b64 = base64.b64encode(b"important message").decode("utf8") + assert text_b64 in msg_string dkim_string1 = "DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=a.com; i=@a.com;\n " \ "q=dns/txt; s=selector; t=" @@ -42,5 +46,3 @@ def test_email_with_dkim(): dkim_string2 = "h=to : from : subject;" assert dkim_string2 in msg_string - - assert "Subject: hello from tests" in msg_string \ No newline at end of file From f4b1cc7941b9a21dac9c124363bd6d6c9b7ebf10 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Sun, 19 Dec 2021 15:43:37 +0200 Subject: [PATCH 05/14] CR fix, change dkim usage from class to a simple function --- yagmail/dkim.py | 36 ++++++++++++++++-------------------- yagmail/message.py | 6 +++--- yagmail/sender.py | 4 +--- 3 files changed, 20 insertions(+), 26 deletions(-) diff --git a/yagmail/dkim.py b/yagmail/dkim.py index a821411..627299b 100644 --- a/yagmail/dkim.py +++ b/yagmail/dkim.py @@ -1,3 +1,4 @@ +from email.mime.base import MIMEBase from typing import NamedTuple import dkim @@ -10,23 +11,18 @@ class DKIM(NamedTuple): selector: bytes -class DKIMHandler: - - def __init__(self, dkim: DKIM): - self.dkim = dkim - - def add_dkim_sig_to_message(self, msg) -> None: - # Based on example from: - # https://github.com/russellballestrini/russell.ballestrini.net/blob/master/content/ - # 2018-06-04-quickstart-to-dkim-sign-email-with-python.rst - sig = dkim.sign( - message=msg.as_bytes(), - selector=self.dkim.selector, - domain=self.dkim.domain, - privkey=self.dkim.private_key, - include_headers=self.dkim.include_headers, - ) - # add the dkim signature to the email message headers. - # decode the signature back to string_type because later on - # the call to msg.as_string() performs it's own bytes encoding... - msg["DKIM-Signature"] = sig[len("DKIM-Signature: "):].decode() +def add_dkim_sig_to_message(msg: MIMEBase, dkim_obj: DKIM) -> None: + # Based on example from: + # https://github.com/russellballestrini/russell.ballestrini.net/blob/master/content/ + # 2018-06-04-quickstart-to-dkim-sign-email-with-python.rst + sig = dkim.sign( + message=msg.as_bytes(), + selector=dkim_obj.selector, + domain=dkim_obj.domain, + privkey=dkim_obj.private_key, + include_headers=dkim_obj.include_headers, + ) + # add the dkim signature to the email message headers. + # decode the signature back to string_type because later on + # the call to msg.as_string() performs it's own bytes encoding... + msg["DKIM-Signature"] = sig[len("DKIM-Signature: "):].decode() diff --git a/yagmail/message.py b/yagmail/message.py index cd257fa..cce5a67 100644 --- a/yagmail/message.py +++ b/yagmail/message.py @@ -10,7 +10,7 @@ from email.mime.text import MIMEText from email.utils import formatdate -from yagmail.dkim import DKIMHandler +from yagmail.dkim import add_dkim_sig_to_message from yagmail.headers import add_message_id from yagmail.headers import add_recipients_headers from yagmail.headers import add_subject @@ -149,8 +149,8 @@ def prepare_message( msg_alternative.attach(MIMEText("\n".join(altstr), _charset=encoding)) msg_alternative.attach(msg_related) - if dkim: - DKIMHandler(dkim).add_dkim_sig_to_message(msg) + if dkim is not None: + add_dkim_sig_to_message(msg, dkim) return msg diff --git a/yagmail/sender.py b/yagmail/sender.py index 762cc2c..3e55ca2 100644 --- a/yagmail/sender.py +++ b/yagmail/sender.py @@ -112,7 +112,6 @@ def prepare_send( prettify_html=True, message_id=None, group_messages=True, - dkim=None, ): addresses = resolve_addresses(self.user, self.useralias, to, cc, bcc) @@ -132,7 +131,7 @@ def prepare_send( prettify_html, message_id, group_messages, - dkim, + self.dkim, ) recipients = addresses["recipients"] @@ -166,7 +165,6 @@ def send( prettify_html, message_id, group_messages, - self.dkim, ) if preview_only: return (recipients, msg_string) From 8b43f364f1a13edc5bea82141dbfc7fee13ca0b8 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Sun, 19 Dec 2021 19:17:44 +0200 Subject: [PATCH 06/14] add test checking the sent message has a valid dkim signature --- tests/domainkey-dns.txt | 1 + tests/privkey.pem | 38 +++++++++++++++++++++++++------------- tests/test_dkim.py | 23 +++++++++++++++++++++-- 3 files changed, 47 insertions(+), 15 deletions(-) create mode 100644 tests/domainkey-dns.txt diff --git a/tests/domainkey-dns.txt b/tests/domainkey-dns.txt new file mode 100644 index 0000000..089bdcd --- /dev/null +++ b/tests/domainkey-dns.txt @@ -0,0 +1 @@ +v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkwMu7eqAx9WrL4lwio01L65D425hBs54Aw4HODsHQYiwQejKsZdj+kneLpm9Zdvm3U1FDD+SfkBWGJmlScoj5Kg0nYx0c0RVeowKetVrmTL7t7d01ag+QRnCBHN1E/B99rFpy47WtwAOuPuKZKIc40JvkCphxVj6GbJZsPjyA2YuhLDp0zVvNzQ61mbM5OC50unppH73maqQVh4f3kIm3Cfxbe8yw8hfVlmZomuSwv3HpZLgrF4ktktI2f3q18Wx4e4OOHaanv/b8VrXo6qIV6RLH5FSteyzFfs+qZbbaWmSDjYEoIHS/oZkaNQOZOkr2T12Rnu/lk/ubDErqaCLXQIDAQAB \ No newline at end of file diff --git a/tests/privkey.pem b/tests/privkey.pem index 6b42dc3..f6322c8 100644 --- a/tests/privkey.pem +++ b/tests/privkey.pem @@ -1,15 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/tests/test_dkim.py b/tests/test_dkim.py index 78dc874..ab6dc9a 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -1,7 +1,16 @@ import base64 +import logging from pathlib import Path from unittest.mock import Mock +import dkim + + +def get_txt_from_test_file(*args, **kwargs): + dns_data_file = Path(__file__).parent / "domainkey-dns.txt" + + return Path(dns_data_file).read_bytes() + def test_email_with_dkim(): from yagmail import SMTP @@ -11,7 +20,7 @@ def test_email_with_dkim(): private_key = private_key_path.read_bytes() - dkim = DKIM( + dkim_obj = DKIM( domain=b"a.com", selector=b"selector", private_key=private_key, @@ -21,7 +30,7 @@ def test_email_with_dkim(): user="a@a.com", host="smtp.blabla.com", port=25, - dkim=dkim, + dkim=dkim_obj, ) yag.login = Mock() @@ -46,3 +55,13 @@ def test_email_with_dkim(): dkim_string2 = "h=to : from : subject;" assert dkim_string2 in msg_string + + l = logging.getLogger() + l.setLevel(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG) + + assert dkim.verify( + message=msg_string.encode("utf8"), + logger=l, + dnsfunc=get_txt_from_test_file + ) \ No newline at end of file From 589cefaca9f31574dd4b5a0504284080f04fc3f7 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Sun, 19 Dec 2021 19:24:34 +0200 Subject: [PATCH 07/14] added tests --- tests/test_dkim.py | 4 +++- yagmail/sender.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_dkim.py b/tests/test_dkim.py index ab6dc9a..5a418f5 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -37,13 +37,15 @@ def test_email_with_dkim(): to = "b@b.com" - recipients, msg_string = yag.send( + recipients, msg_bytes = yag.send( to=to, subject="hello from tests", contents="important message", preview_only=True ) + msg_string = msg_bytes.decode("utf8") + assert recipients == [to] assert "Subject: hello from tests" in msg_string text_b64 = base64.b64encode(b"important message").decode("utf8") diff --git a/yagmail/sender.py b/yagmail/sender.py index 3e55ca2..147edc8 100644 --- a/yagmail/sender.py +++ b/yagmail/sender.py @@ -135,7 +135,7 @@ def prepare_send( ) recipients = addresses["recipients"] - msg_string = msg.as_string() + msg_string = msg.as_bytes() return recipients, msg_string def send( From 54cd23f2de34533bc1e7a044fcfcb5c5d532e9da Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Sun, 19 Dec 2021 19:30:43 +0200 Subject: [PATCH 08/14] changed the type of sent message from string (msg_string) to bytes (msg_bytes), because the dkim verification fails when string is used --- yagmail/sender.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/yagmail/sender.py b/yagmail/sender.py index 147edc8..41f9af1 100644 --- a/yagmail/sender.py +++ b/yagmail/sender.py @@ -135,8 +135,8 @@ def prepare_send( ) recipients = addresses["recipients"] - msg_string = msg.as_bytes() - return recipients, msg_string + msg_bytes = msg.as_bytes() + return recipients, msg_bytes def send( self, @@ -154,7 +154,7 @@ def send( ): """ Use this to send an email with gmail""" self.login() - recipients, msg_string = self.prepare_send( + recipients, msg_bytes = self.prepare_send( to, subject, contents, @@ -167,14 +167,15 @@ def send( group_messages, ) if preview_only: - return (recipients, msg_string) - return self._attempt_send(recipients, msg_string) + return recipients, msg_bytes - def _attempt_send(self, recipients, msg_string): + return self._attempt_send(recipients, msg_bytes) + + def _attempt_send(self, recipients, msg_bytes): attempts = 0 while attempts < 3: try: - result = self.smtp.sendmail(self.user, recipients, msg_string) + result = self.smtp.sendmail(self.user, recipients, msg_bytes) self.log.info("Message sent to %s", recipients) self.num_mail_sent += 1 return result From 3bd29a0c252cd11b026659a82cc40d2cc494172c Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Mon, 20 Dec 2021 08:07:52 +0200 Subject: [PATCH 09/14] add dkimpy + updated version + add missing envs for tox --- setup.py | 6 +++--- tox.ini | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index c48e83e..8a8d9a6 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ with open('README.rst') as f: LONG_DESCRIPTION = f.read() MAJOR_VERSION = '0' -MINOR_VERSION = '14' -MICRO_VERSION = '256' +MINOR_VERSION = '15' +MICRO_VERSION = '0' VERSION = "{}.{}.{}".format(MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION) setup( @@ -18,7 +18,7 @@ author_email='kootenpv@gmail.com', license='MIT', extras_require={"all": ["keyring"]}, - install_requires=["premailer"], + install_requires=["premailer", "dkimpy"], keywords='email mime automatic html attachment', entry_points={'console_scripts': ['yagmail = yagmail.__main__:main']}, classifiers=[ diff --git a/tox.ini b/tox.ini index 188f961..8c781c3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37,py38 +envlist = py36,py37,py38,py39,py310 [testenv] # If you add a new dep here you probably need to add it in setup.py as well From b9ac962478fbacbc4700bdcb53da0395ebe9290f Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Thu, 23 Dec 2021 21:39:00 +0200 Subject: [PATCH 10/14] make dkim support optional and add example in README file --- README.md | 28 ++++++++++++++++++++++++++++ setup.py | 2 +- yagmail/dkim.py | 9 ++++++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66fe327..a3f6bdc 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,35 @@ Therefore, it is highly recommended setting the filename with extension manually A real-world example would be if the attachment is retrieved from a different source than the disk (e.g. downloaded from the internet or [uploaded by a user in a web-application](https://docs.streamlit.io/en/stable/api.html#streamlit.file_uploader)) +### DKIM Support +To send emails with dkim signature, you need to install the package with all related packages. +``` +pip install yagmail[all] +# or +pip install yagmail[dkim] +``` + +Usage: +```python +from yagmail import SMTP +from yagmail.dkim import DKIM +from pathlib import Path + +# load private key from file/secrets manager +private_key = Path("privkey.pem").read_bytes() + +dkim_obj = DKIM( + domain=b"a.com", + selector=b"selector", + private_key=private_key, + include_headers=[b"To", b"From", b"Subject"], +) + +yag = SMTP(dkim=dkim_obj) + +# all the rest is the same +``` ### Feedback I'll try to respond to issues within 24 hours at Github..... diff --git a/setup.py b/setup.py index 8a8d9a6..b70e96c 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ author='Pascal van Kooten', author_email='kootenpv@gmail.com', license='MIT', - extras_require={"all": ["keyring"]}, + extras_require={"all": ["keyring", "dkimpy"], "dkim": ["dkimpy"]}, install_requires=["premailer", "dkimpy"], keywords='email mime automatic html attachment', entry_points={'console_scripts': ['yagmail = yagmail.__main__:main']}, diff --git a/yagmail/dkim.py b/yagmail/dkim.py index 627299b..bbfac9a 100644 --- a/yagmail/dkim.py +++ b/yagmail/dkim.py @@ -1,7 +1,11 @@ from email.mime.base import MIMEBase from typing import NamedTuple -import dkim +try: + import dkim +except ImportError: + dkim = None + pass class DKIM(NamedTuple): @@ -12,6 +16,9 @@ class DKIM(NamedTuple): def add_dkim_sig_to_message(msg: MIMEBase, dkim_obj: DKIM) -> None: + if dkim is None: + raise RuntimeError("dkim package not installed") + # Based on example from: # https://github.com/russellballestrini/russell.ballestrini.net/blob/master/content/ # 2018-06-04-quickstart-to-dkim-sign-email-with-python.rst From 8c26f2e8d09ee4b51b7fa2c67eda4f130bfb43c1 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Thu, 23 Dec 2021 21:54:18 +0200 Subject: [PATCH 11/14] add example how to not include_headers and test for this case --- README.md | 2 ++ tests/test_dkim.py | 27 +++++++++++++++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index a3f6bdc..95d9605 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,8 @@ dkim_obj = DKIM( selector=b"selector", private_key=private_key, include_headers=[b"To", b"From", b"Subject"], + # To include all default headers just pass None instead + # include_headers=None, ) yag = SMTP(dkim=dkim_obj) diff --git a/tests/test_dkim.py b/tests/test_dkim.py index 5a418f5..56595f9 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -12,7 +12,7 @@ def get_txt_from_test_file(*args, **kwargs): return Path(dns_data_file).read_bytes() -def test_email_with_dkim(): +def _test_email_with_dkim(include_headers): from yagmail import SMTP from yagmail.dkim import DKIM @@ -24,8 +24,9 @@ def test_email_with_dkim(): domain=b"a.com", selector=b"selector", private_key=private_key, - include_headers=[b"To", b"From", b"Subject"] + include_headers=include_headers, ) + yag = SMTP( user="a@a.com", host="smtp.blabla.com", @@ -55,9 +56,6 @@ def test_email_with_dkim(): "q=dns/txt; s=selector; t=" assert dkim_string1 in msg_string - dkim_string2 = "h=to : from : subject;" - assert dkim_string2 in msg_string - l = logging.getLogger() l.setLevel(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG) @@ -66,4 +64,21 @@ def test_email_with_dkim(): message=msg_string.encode("utf8"), logger=l, dnsfunc=get_txt_from_test_file - ) \ No newline at end of file + ) + + return msg_string + + +def test_email_with_dkim(): + msg_string = _test_email_with_dkim(include_headers=[b"To", b"From", b"Subject"]) + + dkim_string2 = "h=to : from : subject;" + assert dkim_string2 in msg_string + + +def test_dkim_without_including_headers(): + msg_string = _test_email_with_dkim(include_headers=None) + + dkim_string_headers = "h=content-type : mime-version :\n date : subject : from : to : message-id : from;\n" + assert dkim_string_headers in msg_string + From 1e019dc859d8413eb6ca7f1d3cd7d5fc120bd723 Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Thu, 23 Dec 2021 21:55:54 +0200 Subject: [PATCH 12/14] remove dkimpy as mandatory dependency --- setup.py | 2 +- tests/test_dkim.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index b70e96c..c7be82a 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ author_email='kootenpv@gmail.com', license='MIT', extras_require={"all": ["keyring", "dkimpy"], "dkim": ["dkimpy"]}, - install_requires=["premailer", "dkimpy"], + install_requires=["premailer"], keywords='email mime automatic html attachment', entry_points={'console_scripts': ['yagmail = yagmail.__main__:main']}, classifiers=[ diff --git a/tests/test_dkim.py b/tests/test_dkim.py index 56595f9..3cbdd66 100644 --- a/tests/test_dkim.py +++ b/tests/test_dkim.py @@ -79,6 +79,6 @@ def test_email_with_dkim(): def test_dkim_without_including_headers(): msg_string = _test_email_with_dkim(include_headers=None) - dkim_string_headers = "h=content-type : mime-version :\n date : subject : from : to : message-id : from;\n" + dkim_string_headers = "h=content-type : mime-version :\n date : subject : from : to : message-id : from;\n" assert dkim_string_headers in msg_string From ed638c260ebee3a6cf5e178f0debed1c42c7778d Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Thu, 23 Dec 2021 22:00:12 +0200 Subject: [PATCH 13/14] add missing python versions to travis config --- .travis.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9e77fe8..94529cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,16 @@ language: python matrix: include: + - python: 3.10 + env: TOX_ENV=py310 + - python: 3.9 + env: TOX_ENV=py39 - python: 3.8 env: TOX_ENV=py38 - python: 3.7 env: TOX_ENV=py37 + - python: 3.6 + env: TOX_ENV=py36 install: - pip install tox script: From 734a17d8466e959f844ff974f1c3f46bfba5b80f Mon Sep 17 00:00:00 2001 From: Yonatan Bitton Date: Thu, 23 Dec 2021 22:05:44 +0200 Subject: [PATCH 14/14] add dkim and attaching files to table of contents --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 95d9605..c61d89e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ In 2020, I personally prefer: using an [Application-Specific Password](https://s |[Usability](#usability) | Shows some usage patterns for sending | |[Recipients](#recipients) | How to send to multiple people, give an alias or send to self | |[Magical contents](#magical-contents) | Really easy to send text, html, images and attachments | +|[Attaching files](#attaching-files) | How attach files to the email | +|[DKIM Support](#dkim-support) | Add DKIM signature to your emails with your private key | |[Feedback](#feedback) | How to send me feedback | |[Roadmap (and priorities)](#roadmap-and-priorities) | Yup | |[Errors](#errors) | List of common errors for people dealing with sending emails |