mailsuite 2.2.1__tar.gz → 2.3.0__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.
@@ -109,6 +109,4 @@ venv.bak/
109
109
  private/
110
110
  *.eml
111
111
  *.msg
112
- # Sanitized sample messages used as test fixtures are tracked despite *.eml
113
- !samples/failure_report_samples/*.eml
114
112
  scratch.py
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: mailsuite
3
- Version: 2.2.1
3
+ Version: 2.3.0
4
4
  Summary: A Python package for retrieving, parsing, and sending emails
5
5
  Project-URL: Homepage, https://github.com/seanthegeek/mailsuite/
6
- Project-URL: Documentation, https://seanthegeek.github.io/mailsuite/
6
+ Project-URL: Documentation, https://seanthegeek.net/mailsuite/
7
7
  Project-URL: Issues, https://github.com/seanthegeek/mailsuite/issues
8
8
  Project-URL: Changelog, https://github.com/seanthegeek/mailsuite/blob/master/CHANGELOG.md
9
9
  Author-email: Sean Whalen <whalenster@gmail.com>
@@ -16,15 +16,20 @@ Classifier: Intended Audience :: Information Technology
16
16
  Classifier: License :: OSI Approved :: Apache Software License
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3
19
- Requires-Python: >=3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Requires-Python: >=3.10
20
25
  Requires-Dist: authres>=1.2.0
21
- Requires-Dist: cryptography>=41.0.0
26
+ Requires-Dist: cryptography>=50.0.0
22
27
  Requires-Dist: dkimpy>=1.1.0
23
28
  Requires-Dist: dnspython>=2.0.0
24
29
  Requires-Dist: expiringdict==1.2.2
25
30
  Requires-Dist: html2text>=2020.1.16
26
31
  Requires-Dist: imapclient>=3.1.0
27
- Requires-Dist: mail-parser>=4.2.1
32
+ Requires-Dist: mail-parser>=4.6.2
28
33
  Requires-Dist: publicsuffix2>=2.20190812
29
34
  Provides-Extra: all
30
35
  Requires-Dist: azure-identity>=1.15.0; extra == 'all'
@@ -50,6 +55,20 @@ A Python package for retrieving, parsing, and sending emails.
50
55
 
51
56
  ## Features
52
57
 
58
+ - Simplified IMAP client (`mailsuite.imap.IMAPClient`) — usable on its own,
59
+ and the engine behind the mailbox abstraction's IMAP backend
60
+ - Automatic reconnection and retries after dropped connections and timeouts
61
+ - Watch a folder for new messages with `IDLE` callbacks, including periodic
62
+ session refresh
63
+ - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login
64
+ - Always uses `/` as the folder hierarchy separator, converting to the
65
+ server's separator and prepending its namespace automatically, and
66
+ stripping folder-name characters that collide with the separator
67
+ - Works around server quirks across Gmail, Microsoft 365, Exchange,
68
+ Dovecot, and DavMail, including:
69
+ - Gmail / Google Workspace returning an empty `IDLE` response
70
+ - Random Microsoft 365 / Exchange `BAD` / "unexpected response" errors
71
+ - Nonstandard hierarchy separators and namespaces
53
72
  - Provider-agnostic mailbox abstraction (`mailsuite.mailbox`)
54
73
  - Single `MailboxConnection` interface for IMAP, Microsoft Graph, Gmail,
55
74
  and on-disk Maildir
@@ -58,33 +77,32 @@ A Python package for retrieving, parsing, and sending emails.
58
77
  - Folder management across every backend — create, rename, move, merge,
59
78
  delete, and existence checks, with consistent `FolderExistsError` /
60
79
  `FolderNotFoundError` semantics
61
- - Watch a folder for new messages — the IMAP `IDLE` command (with periodic
62
- session refresh) on the IMAP backend, polling on the cloud backends
80
+ - Watch a folder for new messages — the IMAP `IDLE` command on the IMAP
81
+ backend, polling on the cloud backends
63
82
  - Unified `send_message()` on backends that support sending (Microsoft
64
83
  Graph, Gmail) — IMAP and Maildir users send through
65
84
  `mailsuite.smtp.send_email`
66
- - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login for IMAP and SMTP
67
- - Automatic IMAP reconnection after dropped connections and timeouts
68
- - Always uses `/` as the folder hierarchy separator, converting to the
69
- server's separator and prepending its namespace automatically, and
70
- stripping folder-name characters that collide with the separator
71
- - Works around backend quirks across Gmail, Microsoft 365, Exchange,
72
- Dovecot, and DavMail, including:
73
- - Gmail / Google Workspace returning an empty `IDLE` response
74
- - Random Microsoft 365 / Exchange `BAD` / "unexpected response" errors
75
- - Nonstandard hierarchy separators and namespaces
76
- - Consistent email parsing
85
+ - Consistent email parsing (`mailsuite.utils`)
86
+ - Parse RFC 822 messages from a string, bytes, or file path into
87
+ consistent dictionaries, with HTML bodies also converted to Markdown
77
88
  - SHA256 hashes of attachments
78
89
  - Parsed `Authentication-Results` and `DKIM-Signature` headers
90
+ - Email address parsing into display name, local part, domain, and
91
+ second-level domain, tolerating noncompliant addresses
92
+ - Check whether a message passed DKIM or DMARC as a trusted domain
93
+ (`from_trusted_domain`)
94
+ - Forward and reverse DNS lookup helpers
79
95
  - Parse Microsoft Outlook `.msg` files using `msgconvert`
80
- - Simplified email creation and sending
96
+ - Simplified email creation and sending (`mailsuite.smtp`)
81
97
  - Easily add attachments, plain text, and HTML
98
+ - Optional DKIM signing of outgoing mail
82
99
  - Uses opportunistic encryption (`STARTTLS`) with SMTP by default
83
- - DKIM signing and verification
100
+ - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login
101
+ - DKIM signing and verification (`mailsuite.dkim`)
84
102
  - Generate RSA keypairs and the matching DNS TXT record
85
103
  - Sign outbound mail with a sensible default header set
86
104
  - Verify one or many `DKIM-Signature` headers on a received message
87
- - ARC (Authenticated Received Chain) sealing and verification
105
+ - ARC (Authenticated Received Chain) sealing and verification (`mailsuite.arc`)
88
106
  - Seal forwarded mail with an ARC set, extending an existing chain
89
107
  - Verify the ARC chain on a received message and read its `cv` result
90
108
 
@@ -7,6 +7,20 @@ A Python package for retrieving, parsing, and sending emails.
7
7
 
8
8
  ## Features
9
9
 
10
+ - Simplified IMAP client (`mailsuite.imap.IMAPClient`) — usable on its own,
11
+ and the engine behind the mailbox abstraction's IMAP backend
12
+ - Automatic reconnection and retries after dropped connections and timeouts
13
+ - Watch a folder for new messages with `IDLE` callbacks, including periodic
14
+ session refresh
15
+ - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login
16
+ - Always uses `/` as the folder hierarchy separator, converting to the
17
+ server's separator and prepending its namespace automatically, and
18
+ stripping folder-name characters that collide with the separator
19
+ - Works around server quirks across Gmail, Microsoft 365, Exchange,
20
+ Dovecot, and DavMail, including:
21
+ - Gmail / Google Workspace returning an empty `IDLE` response
22
+ - Random Microsoft 365 / Exchange `BAD` / "unexpected response" errors
23
+ - Nonstandard hierarchy separators and namespaces
10
24
  - Provider-agnostic mailbox abstraction (`mailsuite.mailbox`)
11
25
  - Single `MailboxConnection` interface for IMAP, Microsoft Graph, Gmail,
12
26
  and on-disk Maildir
@@ -15,33 +29,32 @@ A Python package for retrieving, parsing, and sending emails.
15
29
  - Folder management across every backend — create, rename, move, merge,
16
30
  delete, and existence checks, with consistent `FolderExistsError` /
17
31
  `FolderNotFoundError` semantics
18
- - Watch a folder for new messages — the IMAP `IDLE` command (with periodic
19
- session refresh) on the IMAP backend, polling on the cloud backends
32
+ - Watch a folder for new messages — the IMAP `IDLE` command on the IMAP
33
+ backend, polling on the cloud backends
20
34
  - Unified `send_message()` on backends that support sending (Microsoft
21
35
  Graph, Gmail) — IMAP and Maildir users send through
22
36
  `mailsuite.smtp.send_email`
23
- - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login for IMAP and SMTP
24
- - Automatic IMAP reconnection after dropped connections and timeouts
25
- - Always uses `/` as the folder hierarchy separator, converting to the
26
- server's separator and prepending its namespace automatically, and
27
- stripping folder-name characters that collide with the separator
28
- - Works around backend quirks across Gmail, Microsoft 365, Exchange,
29
- Dovecot, and DavMail, including:
30
- - Gmail / Google Workspace returning an empty `IDLE` response
31
- - Random Microsoft 365 / Exchange `BAD` / "unexpected response" errors
32
- - Nonstandard hierarchy separators and namespaces
33
- - Consistent email parsing
37
+ - Consistent email parsing (`mailsuite.utils`)
38
+ - Parse RFC 822 messages from a string, bytes, or file path into
39
+ consistent dictionaries, with HTML bodies also converted to Markdown
34
40
  - SHA256 hashes of attachments
35
41
  - Parsed `Authentication-Results` and `DKIM-Signature` headers
42
+ - Email address parsing into display name, local part, domain, and
43
+ second-level domain, tolerating noncompliant addresses
44
+ - Check whether a message passed DKIM or DMARC as a trusted domain
45
+ (`from_trusted_domain`)
46
+ - Forward and reverse DNS lookup helpers
36
47
  - Parse Microsoft Outlook `.msg` files using `msgconvert`
37
- - Simplified email creation and sending
48
+ - Simplified email creation and sending (`mailsuite.smtp`)
38
49
  - Easily add attachments, plain text, and HTML
50
+ - Optional DKIM signing of outgoing mail
39
51
  - Uses opportunistic encryption (`STARTTLS`) with SMTP by default
40
- - DKIM signing and verification
52
+ - Username/password or OAuth2 (XOAUTH2 / OAUTHBEARER) login
53
+ - DKIM signing and verification (`mailsuite.dkim`)
41
54
  - Generate RSA keypairs and the matching DNS TXT record
42
55
  - Sign outbound mail with a sensible default header set
43
56
  - Verify one or many `DKIM-Signature` headers on a received message
44
- - ARC (Authenticated Received Chain) sealing and verification
57
+ - ARC (Authenticated Received Chain) sealing and verification (`mailsuite.arc`)
45
58
  - Seal forwarded mail with an ARC set, extending an existing chain
46
59
  - Verify the ARC chain on a received message and read its `cv` result
47
60
 
@@ -1,3 +1,3 @@
1
1
  """A Python package to simplify receiving, parsing, and sending email"""
2
2
 
3
- __version__ = "2.2.1"
3
+ __version__ = "2.3.0"
@@ -18,7 +18,8 @@ like :mod:`mailsuite.dkim`.
18
18
  """
19
19
 
20
20
  import logging
21
- from typing import Any, Callable, List, Optional, Union
21
+ from collections.abc import Callable
22
+ from typing import Any
22
23
 
23
24
  import dkim as _dkim
24
25
 
@@ -31,14 +32,14 @@ class ARCError(RuntimeError):
31
32
 
32
33
 
33
34
  def seal_email(
34
- message: Union[str, bytes],
35
+ message: str | bytes,
35
36
  selector: str,
36
37
  domain: str,
37
- private_key: Union[str, bytes],
38
+ private_key: str | bytes,
38
39
  authserv_id: str,
39
- signed_headers: Optional[List[str]] = None,
40
- timestamp: Optional[int] = None,
41
- ) -> Union[str, bytes]:
40
+ signed_headers: list[str] | None = None,
41
+ timestamp: int | None = None,
42
+ ) -> str | bytes:
42
43
  """
43
44
  Adds an ARC set (seal) to an email and returns the sealed RFC 822 message
44
45
 
@@ -123,9 +124,9 @@ def seal_email(
123
124
 
124
125
 
125
126
  def verify_arc_chain(
126
- message: Union[str, bytes],
127
+ message: str | bytes,
127
128
  minkey: int = 1024,
128
- dns_func: Optional[Callable[[str], bytes]] = None,
129
+ dns_func: Callable[[str], bytes] | None = None,
129
130
  ) -> dict:
130
131
  """
131
132
  Verifies the ARC chain on an RFC 822 message
@@ -3,7 +3,7 @@
3
3
  import base64
4
4
  import email
5
5
  import logging
6
- from typing import Callable, Optional, Tuple, Union
6
+ from collections.abc import Callable
7
7
 
8
8
  import dkim as _dkim
9
9
  from cryptography.hazmat.primitives import serialization
@@ -58,7 +58,7 @@ def generate_dkim_private_key(key_size: int = 2048) -> str:
58
58
  return pem.decode("ascii")
59
59
 
60
60
 
61
- def get_dkim_public_key(private_key: Union[str, bytes]) -> str:
61
+ def get_dkim_public_key(private_key: str | bytes) -> str:
62
62
  """
63
63
  Derives the DKIM public key from a private key
64
64
 
@@ -74,7 +74,7 @@ def get_dkim_public_key(private_key: Union[str, bytes]) -> str:
74
74
  key_bytes = bytes(private_key)
75
75
  try:
76
76
  key = serialization.load_pem_private_key(key_bytes, password=None)
77
- except Exception as e:
77
+ except Exception as e: # noqa: BLE001
78
78
  raise DKIMError(f"Failed to load private key: {e}")
79
79
  der = key.public_key().public_bytes(
80
80
  encoding=serialization.Encoding.DER,
@@ -83,7 +83,7 @@ def get_dkim_public_key(private_key: Union[str, bytes]) -> str:
83
83
  return base64.b64encode(der).decode("ascii")
84
84
 
85
85
 
86
- def generate_dkim_keypair(key_size: int = 2048) -> Tuple[str, str]:
86
+ def generate_dkim_keypair(key_size: int = 2048) -> tuple[str, str]:
87
87
  """
88
88
  Generates a DKIM RSA keypair
89
89
 
@@ -98,11 +98,11 @@ def generate_dkim_keypair(key_size: int = 2048) -> Tuple[str, str]:
98
98
 
99
99
 
100
100
  def generate_dkim_txt_record(
101
- public_key: Union[str, bytes],
101
+ public_key: str | bytes,
102
102
  selector: str = "default",
103
- domain: Optional[str] = None,
104
- flags: Optional[str] = None,
105
- note: Optional[str] = None,
103
+ domain: str | None = None,
104
+ flags: str | None = None,
105
+ note: str | None = None,
106
106
  ) -> str:
107
107
  """
108
108
  Generates a DKIM TXT record
@@ -134,7 +134,7 @@ def generate_dkim_txt_record(
134
134
  elif "BEGIN" in key_str and "PUBLIC KEY" in key_str:
135
135
  try:
136
136
  pub = serialization.load_pem_public_key(key_str.encode("ascii"))
137
- except Exception as e:
137
+ except Exception as e: # noqa: BLE001
138
138
  raise DKIMError(f"Failed to load public key: {e}")
139
139
  der = pub.public_bytes(
140
140
  encoding=serialization.Encoding.DER,
@@ -159,14 +159,14 @@ def generate_dkim_txt_record(
159
159
 
160
160
 
161
161
  def sign_email(
162
- message: Union[str, bytes],
162
+ message: str | bytes,
163
163
  selector: str,
164
164
  domain: str,
165
- private_key: Union[str, bytes],
166
- additional_headers: Optional[list[str]] = None,
167
- canonicalize: Tuple[bytes, bytes] = (b"relaxed", b"relaxed"),
168
- identity: Optional[str] = None,
169
- ) -> Union[str, bytes]:
165
+ private_key: str | bytes,
166
+ additional_headers: list[str] | None = None,
167
+ canonicalize: tuple[bytes, bytes] = (b"relaxed", b"relaxed"),
168
+ identity: str | None = None,
169
+ ) -> str | bytes:
170
170
  """
171
171
  DKIM-signs an email and returns the signed RFC 822 message
172
172
 
@@ -210,7 +210,7 @@ def sign_email(
210
210
  headers_to_sign.extend(additional_headers)
211
211
 
212
212
  parsed = email.message_from_bytes(message_bytes)
213
- present = {key.lower() for key in parsed.keys()}
213
+ present = {key.lower() for key in parsed}
214
214
 
215
215
  include = [
216
216
  header.encode("ascii")
@@ -243,10 +243,10 @@ def sign_email(
243
243
 
244
244
 
245
245
  def verify_email(
246
- message: Union[str, bytes],
246
+ message: str | bytes,
247
247
  timeout: float = 5.0,
248
248
  minkey: int = 1024,
249
- dns_func: Optional[Callable[[str], bytes]] = None,
249
+ dns_func: Callable[[str], bytes] | None = None,
250
250
  ) -> dict:
251
251
  """
252
252
  Verifies the DKIM signature(s) on an RFC 822 message
@@ -318,7 +318,7 @@ def verify_email(
318
318
  sig_info["error"] = "Signature did not verify"
319
319
  except _dkim.DKIMException as e:
320
320
  sig_info["error"] = str(e)
321
- except Exception as e:
321
+ except Exception as e: # noqa: BLE001
322
322
  sig_info["error"] = f"Verification error: {e}"
323
323
 
324
324
  if sig_info["valid"]: