envelope 2.2.0__tar.gz → 2.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {envelope-2.2.0 → envelope-2.2.1}/PKG-INFO +12 -2
- {envelope-2.2.0 → envelope-2.2.1}/README.md +11 -1
- {envelope-2.2.0 → envelope-2.2.1}/envelope/__main__.py +5 -1
- envelope-2.2.1/envelope/_auth.py +79 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/envelope.py +24 -4
- {envelope-2.2.0 → envelope-2.2.1}/pyproject.toml +1 -1
- {envelope-2.2.0 → envelope-2.2.1}/LICENSE +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/__init__.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/address.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/attachment.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/constants.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/message.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/parser.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/smtp_handler.py +0 -0
- {envelope-2.2.0 → envelope-2.2.1}/envelope/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: envelope
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.1
|
|
4
4
|
Summary: Insert a message and attachments and send e-mail / sign / encrypt contents by a single line.
|
|
5
5
|
License: LGPL-3.0-or-later
|
|
6
6
|
Author: Edvard Rejthar
|
|
@@ -23,7 +23,7 @@ Description-Content-Type: text/markdown
|
|
|
23
23
|
|
|
24
24
|
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
25
25
|
|
|
26
|
-
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [M2Crypto](https://m2crypto.readthedocs.io/), [smtplib](https://docs.python.org/3/library/smtplib.html), [magic](https://pypi.org/project/python-magic/) and [email](https://docs.python.org/3/library/email.html?highlight=email#module-email) handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL.
|
|
26
|
+
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [cryptography](https://github.com/pyca/cryptography), [M2Crypto](https://m2crypto.readthedocs.io/), [smtplib](https://docs.python.org/3/library/smtplib.html), [magic](https://pypi.org/project/python-magic/) and [email](https://docs.python.org/3/library/email.html?highlight=email#module-email) handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL.
|
|
27
27
|
You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail.
|
|
28
28
|
Just single line of code. With the great help of the examples below.
|
|
29
29
|
|
|
@@ -692,6 +692,16 @@ if xarf := Envelope.load(path="xarf.eml")._report():
|
|
|
692
692
|
print(xarf['SourceIp']) # '192.0.2.1'
|
|
693
693
|
```
|
|
694
694
|
|
|
695
|
+
* `_check_auth()`: To determine whether DMACR, SPF and DKIM are alright in a loaded message.
|
|
696
|
+
```python3
|
|
697
|
+
from envelope import Envelope
|
|
698
|
+
auth = Envelope.load(path="test.eml")._check_auth()
|
|
699
|
+
auth # <AuthResult spf='pass', dkim='pass', dmarc='pass', spf_received='pass', verdict='pass', failure_reason=None>
|
|
700
|
+
if auth: # Use the output object as bool. It is True only if all present checks are 'pass'.
|
|
701
|
+
...
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
|
|
695
705
|
## Envelope object
|
|
696
706
|
|
|
697
707
|
### Converting object to str or bool
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/CZ-NIC/envelope/actions) [](https://pepy.tech/project/envelope)
|
|
4
4
|
|
|
5
|
-
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [M2Crypto](https://m2crypto.readthedocs.io/), [smtplib](https://docs.python.org/3/library/smtplib.html), [magic](https://pypi.org/project/python-magic/) and [email](https://docs.python.org/3/library/email.html?highlight=email#module-email) handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL.
|
|
5
|
+
Quick layer over [python-gnupg](https://bitbucket.org/vinay.sajip/python-gnupg/src), [cryptography](https://github.com/pyca/cryptography), [M2Crypto](https://m2crypto.readthedocs.io/), [smtplib](https://docs.python.org/3/library/smtplib.html), [magic](https://pypi.org/project/python-magic/) and [email](https://docs.python.org/3/library/email.html?highlight=email#module-email) handling packages. Their common use cases merged into a single function. Want to sign a text and tired of forgetting how to do it right? You do not need to know everything about GPG or S/MIME, you do not have to bother with importing keys. Do not hassle with reconnecting to an SMTP server. Do not study various headers meanings to let your users unsubscribe via a URL.
|
|
6
6
|
You insert a message, attachments and inline images and receive signed and/or encrypted output to the file or to your recipients' e-mail.
|
|
7
7
|
Just single line of code. With the great help of the examples below.
|
|
8
8
|
|
|
@@ -671,6 +671,16 @@ if xarf := Envelope.load(path="xarf.eml")._report():
|
|
|
671
671
|
print(xarf['SourceIp']) # '192.0.2.1'
|
|
672
672
|
```
|
|
673
673
|
|
|
674
|
+
* `_check_auth()`: To determine whether DMACR, SPF and DKIM are alright in a loaded message.
|
|
675
|
+
```python3
|
|
676
|
+
from envelope import Envelope
|
|
677
|
+
auth = Envelope.load(path="test.eml")._check_auth()
|
|
678
|
+
auth # <AuthResult spf='pass', dkim='pass', dmarc='pass', spf_received='pass', verdict='pass', failure_reason=None>
|
|
679
|
+
if auth: # Use the output object as bool. It is True only if all present checks are 'pass'.
|
|
680
|
+
...
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
|
|
674
684
|
## Envelope object
|
|
675
685
|
|
|
676
686
|
### Converting object to str or bool
|
|
@@ -171,7 +171,11 @@ def main():
|
|
|
171
171
|
or args["subject"] is True \
|
|
172
172
|
or args["message"] is True \
|
|
173
173
|
or select.select([sys.stdin, ], [], [], 0.0)[0]:
|
|
174
|
-
|
|
174
|
+
stdin = sys.stdin.read()
|
|
175
|
+
if "\r" in stdin or "\n" in stdin:
|
|
176
|
+
instance = Envelope.load(stdin)
|
|
177
|
+
else:
|
|
178
|
+
instance = Envelope(message=stdin)
|
|
175
179
|
except select.error: # XX check if using `select` to detect STDIN does not mess up Windows
|
|
176
180
|
pass
|
|
177
181
|
del args["load"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from email.message import EmailMessage
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class AuthResult:
|
|
9
|
+
verdict: str
|
|
10
|
+
spf: Optional[str] = None
|
|
11
|
+
dkim: Optional[str] = None
|
|
12
|
+
dmarc: Optional[str] = None
|
|
13
|
+
spf_received: Optional[str] = None
|
|
14
|
+
failure_reason: Optional[str] = None
|
|
15
|
+
|
|
16
|
+
def __bool__(self):
|
|
17
|
+
return self.verdict == "pass"
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return (f"<AuthResult spf={self.spf!r}, dkim={self.dkim!r}, dmarc={self.dmarc!r}, "
|
|
21
|
+
f"spf_received={self.spf_received!r}, verdict={self.verdict!r}, failure_reason={self.failure_reason!r}>")
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def _extract_results_from_text(text: str) -> dict:
|
|
25
|
+
def extract(key: str):
|
|
26
|
+
match = re.search(fr"{key}=([\w\-]+)", text, re.IGNORECASE)
|
|
27
|
+
return match.group(1).lower() if match else None
|
|
28
|
+
|
|
29
|
+
return extract("spf"), extract("dkim"), extract("dmarc")
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def from_headers(headers: EmailMessage) -> "AuthResult":
|
|
33
|
+
|
|
34
|
+
# Merge Authentication-Results and ARC-Authentication-Results hlavičky dohromady
|
|
35
|
+
auth_results_headers: list[str] = []
|
|
36
|
+
auth_results_headers += headers.get_all("Authentication-Results", [])
|
|
37
|
+
auth_results_headers += headers.get_all("ARC-Authentication-Results", [])
|
|
38
|
+
|
|
39
|
+
combined_auth_results = "; ".join(auth_results_headers)
|
|
40
|
+
|
|
41
|
+
# Extract from text
|
|
42
|
+
spf, dkim, dmarc = AuthResult._extract_results_from_text(combined_auth_results)
|
|
43
|
+
|
|
44
|
+
# SPF from Received-SPF
|
|
45
|
+
received_spf = headers.get("Received-SPF", "")
|
|
46
|
+
spf_received_match = re.search(r"([\w\-]+)", received_spf)
|
|
47
|
+
spf_received = spf_received_match.group(1).lower() if spf_received_match else None
|
|
48
|
+
|
|
49
|
+
# Determine the verdict
|
|
50
|
+
failure_reason = None
|
|
51
|
+
if spf and spf_received and spf != spf_received:
|
|
52
|
+
verdict = "fail"
|
|
53
|
+
failure_reason = f"SPF mismatch: Authentication-Results={spf}, Received-SPF={spf_received}"
|
|
54
|
+
|
|
55
|
+
elif dmarc == "pass":
|
|
56
|
+
verdict = "pass"
|
|
57
|
+
|
|
58
|
+
elif any(v == "fail" for v in [spf, dkim, dmarc, spf_received]):
|
|
59
|
+
verdict = "fail"
|
|
60
|
+
failure_reason = "At least one mechanism failed"
|
|
61
|
+
|
|
62
|
+
elif any(v == "softfail" for v in [spf, spf_received]):
|
|
63
|
+
verdict = "softfail"
|
|
64
|
+
failure_reason = "SPF softfail detected"
|
|
65
|
+
|
|
66
|
+
elif all(v == "pass" for v in [spf, dkim] if v and v != 'none'):
|
|
67
|
+
verdict = "pass"
|
|
68
|
+
else:
|
|
69
|
+
verdict = "none"
|
|
70
|
+
failure_reason = "No usable authentication results"
|
|
71
|
+
|
|
72
|
+
return AuthResult(
|
|
73
|
+
spf=spf,
|
|
74
|
+
dkim=dkim,
|
|
75
|
+
dmarc=dmarc,
|
|
76
|
+
spf_received=spf_received,
|
|
77
|
+
verdict=verdict,
|
|
78
|
+
failure_reason=failure_reason
|
|
79
|
+
)
|
|
@@ -24,6 +24,7 @@ from quopri import decodestring
|
|
|
24
24
|
from types import GeneratorType
|
|
25
25
|
from typing import Literal, Union, Optional, Any
|
|
26
26
|
|
|
27
|
+
from ._auth import AuthResult
|
|
27
28
|
from .address import Address, _getaddresses
|
|
28
29
|
from .attachment import Attachment
|
|
29
30
|
from .constants import ISSUE_LINK, smime_import_error, gnupg, CRLF, AUTO, PLAIN, HTML, SIMULATION, SAFE_LOCALE
|
|
@@ -205,13 +206,25 @@ class Envelope:
|
|
|
205
206
|
:param key: S/MIME key to decrypt with.
|
|
206
207
|
:param cert: S/MIME cert to decrypt with. (If not bundled with the key.)
|
|
207
208
|
:param gnupg_home: Path to the GNUPG_HOME or None if the environment default should be used.
|
|
209
|
+
|
|
210
|
+
:raise ValueError: If the message cannot be loaded.
|
|
208
211
|
"""
|
|
209
212
|
if path:
|
|
210
213
|
message = Path(path)
|
|
211
214
|
elif isinstance(message, Message):
|
|
212
215
|
message = str(message)
|
|
213
216
|
|
|
214
|
-
|
|
217
|
+
msg_ = assure_fetched(message, bytes)
|
|
218
|
+
o = message_from_bytes(msg_)
|
|
219
|
+
|
|
220
|
+
# NOTE this breaks up .load("hello text")
|
|
221
|
+
# quick check
|
|
222
|
+
for _ in o.keys():
|
|
223
|
+
break
|
|
224
|
+
else: # there are no headers
|
|
225
|
+
if not path and b"\n" not in msg_ and b"\r" not in msg_:
|
|
226
|
+
raise ValueError(f"Message cannot be loaded. Didn't you forgot to use the `Envelope.load(path=...)`?")
|
|
227
|
+
|
|
215
228
|
e = Envelope()
|
|
216
229
|
try:
|
|
217
230
|
return Parser(e, key=key, cert=cert, gnupg_home=gnupg_home or e._get_gnupg_home()) \
|
|
@@ -406,7 +419,8 @@ class Envelope:
|
|
|
406
419
|
split_addresses = []
|
|
407
420
|
for address in addresses:
|
|
408
421
|
split_addresses.extend(address.replace(';', ',').split(','))
|
|
409
|
-
|
|
422
|
+
# remove empty and whitespace-only strings
|
|
423
|
+
split_addresses = [x.strip() for x in split_addresses if x.strip()]
|
|
410
424
|
registry += (a for a in Address.parse(split_addresses) if a not in registry)
|
|
411
425
|
|
|
412
426
|
def to(self, email_or_more=None) -> Union["Envelope", list[Address]]:
|
|
@@ -607,7 +621,8 @@ class Envelope:
|
|
|
607
621
|
:param one_click: If True, rfc8058 List-Unsubscribe-Post header is added.
|
|
608
622
|
This says user can unsubscribe with a single click that is realized by a POST request
|
|
609
623
|
in order to prevent e-mail scanner to access the unsubscribe page by mistake. A 'https' url must be present.
|
|
610
|
-
:param web: URL. Ex: "
|
|
624
|
+
:param web: URL. Ex: "https://example.com/unsubscribe"
|
|
625
|
+
Without protocol, `https://` is prepended, "example.com/unsubscribe" -> "https://example.com/unsubscribe"
|
|
611
626
|
:param email: E-mail address. Ex: "me@example.com", "mailto:me@example.com"
|
|
612
627
|
:return: self
|
|
613
628
|
"""
|
|
@@ -1265,7 +1280,7 @@ class Envelope:
|
|
|
1265
1280
|
return load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization
|
|
1266
1281
|
except ImportError:
|
|
1267
1282
|
raise ImportError(smime_import_error)
|
|
1268
|
-
|
|
1283
|
+
|
|
1269
1284
|
def smime_sign_only(self, email, sign):
|
|
1270
1285
|
load_pem_private_key, pkcs7, load_pem_x509_certificate, hashes, serialization = self._import_cryptoraphy_modules()
|
|
1271
1286
|
# get sender's cert
|
|
@@ -1716,6 +1731,11 @@ class Envelope:
|
|
|
1716
1731
|
passed *= bool(self._smtp.connect()) # check SMTP
|
|
1717
1732
|
return passed
|
|
1718
1733
|
|
|
1734
|
+
def _check_auth(self):
|
|
1735
|
+
""" Experimental. """
|
|
1736
|
+
# NOTE Do not know how to map in API. Because .check() seems to be for sending and this one for receiving.
|
|
1737
|
+
return AuthResult.from_headers(self._headers)
|
|
1738
|
+
|
|
1719
1739
|
def smtp_quit(self=None):
|
|
1720
1740
|
""" Explicitly closes cached SMTP connections. Either class or instance can be called.
|
|
1721
1741
|
Envelope.smtp_quit() → closing all
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "envelope"
|
|
7
|
-
version = "2.2.
|
|
7
|
+
version = "2.2.1"
|
|
8
8
|
description = "Insert a message and attachments and send e-mail / sign / encrypt contents by a single line."
|
|
9
9
|
authors = ["Edvard Rejthar <edvard.rejthar@nic.cz>"]
|
|
10
10
|
license = "LGPL-3.0-or-later"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|