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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: envelope
3
- Version: 2.2.0
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
  [![Build Status](https://github.com/CZ-NIC/envelope/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/envelope/actions) [![Downloads](https://static.pepy.tech/badge/envelope)](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
  [![Build Status](https://github.com/CZ-NIC/envelope/actions/workflows/run-unittest.yml/badge.svg)](https://github.com/CZ-NIC/envelope/actions) [![Downloads](https://static.pepy.tech/badge/envelope)](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
- instance = Envelope.load(sys.stdin.read())
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
- o = message_from_bytes(assure_fetched(message, bytes))
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
- split_addresses = [x.strip() for x in split_addresses if x.strip()] # remove empty and whitespace-only strings
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: "example.com/unsubscribe", "http://example.com/unsubscribe"
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.0"
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