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.
- {mailsuite-2.2.1 → mailsuite-2.3.0}/.gitignore +0 -2
- {mailsuite-2.2.1 → mailsuite-2.3.0}/PKG-INFO +40 -22
- {mailsuite-2.2.1 → mailsuite-2.3.0}/README.md +29 -16
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/__init__.py +1 -1
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/arc.py +9 -8
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/dkim.py +19 -19
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/imap.py +69 -66
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/base.py +20 -15
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/gmail.py +29 -20
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/graph.py +54 -47
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/imap.py +12 -11
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/maildir.py +6 -5
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/smtp.py +26 -25
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/utils.py +94 -86
- {mailsuite-2.2.1 → mailsuite-2.3.0}/pyproject.toml +9 -4
- {mailsuite-2.2.1 → mailsuite-2.3.0}/LICENSE +0 -0
- {mailsuite-2.2.1 → mailsuite-2.3.0}/mailsuite/mailbox/__init__.py +3 -3
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: mailsuite
|
|
3
|
-
Version: 2.
|
|
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.
|
|
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
|
-
|
|
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>=
|
|
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
|
|
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
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
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
|
-
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
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
|
-
-
|
|
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
|
|
|
@@ -18,7 +18,8 @@ like :mod:`mailsuite.dkim`.
|
|
|
18
18
|
"""
|
|
19
19
|
|
|
20
20
|
import logging
|
|
21
|
-
from
|
|
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:
|
|
35
|
+
message: str | bytes,
|
|
35
36
|
selector: str,
|
|
36
37
|
domain: str,
|
|
37
|
-
private_key:
|
|
38
|
+
private_key: str | bytes,
|
|
38
39
|
authserv_id: str,
|
|
39
|
-
signed_headers:
|
|
40
|
-
timestamp:
|
|
41
|
-
) ->
|
|
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:
|
|
127
|
+
message: str | bytes,
|
|
127
128
|
minkey: int = 1024,
|
|
128
|
-
dns_func:
|
|
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
|
|
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:
|
|
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) ->
|
|
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:
|
|
101
|
+
public_key: str | bytes,
|
|
102
102
|
selector: str = "default",
|
|
103
|
-
domain:
|
|
104
|
-
flags:
|
|
105
|
-
note:
|
|
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:
|
|
162
|
+
message: str | bytes,
|
|
163
163
|
selector: str,
|
|
164
164
|
domain: str,
|
|
165
|
-
private_key:
|
|
166
|
-
additional_headers:
|
|
167
|
-
canonicalize:
|
|
168
|
-
identity:
|
|
169
|
-
) ->
|
|
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
|
|
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:
|
|
246
|
+
message: str | bytes,
|
|
247
247
|
timeout: float = 5.0,
|
|
248
248
|
minkey: int = 1024,
|
|
249
|
-
dns_func:
|
|
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"]:
|