mailcycle 0.1.0__py3-none-any.whl
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.
- mailcycle/__init__.py +81 -0
- mailcycle/_encoding.py +57 -0
- mailcycle/client.py +700 -0
- mailcycle/crypto/__init__.py +30 -0
- mailcycle/crypto/asymmetric.py +111 -0
- mailcycle/crypto/authproof.py +60 -0
- mailcycle/crypto/cipher.py +133 -0
- mailcycle/crypto/identity.py +122 -0
- mailcycle/crypto/mnemonic.py +127 -0
- mailcycle/crypto/primitives.py +93 -0
- mailcycle/crypto/sealedbox.py +120 -0
- mailcycle/crypto/wordlist.py +275 -0
- mailcycle/errors.py +62 -0
- mailcycle/events.py +271 -0
- mailcycle/http.py +115 -0
- mailcycle/keys.py +92 -0
- mailcycle/mail.py +128 -0
- mailcycle/models.py +177 -0
- mailcycle/py.typed +0 -0
- mailcycle/trackers.py +260 -0
- mailcycle/webhooks.py +64 -0
- mailcycle-0.1.0.dist-info/METADATA +213 -0
- mailcycle-0.1.0.dist-info/RECORD +25 -0
- mailcycle-0.1.0.dist-info/WHEEL +4 -0
- mailcycle-0.1.0.dist-info/licenses/LICENSE +35 -0
mailcycle/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Mailcycle for Python.
|
|
2
|
+
|
|
3
|
+
Privacy-first email you can automate. Create addresses that receive mail, read
|
|
4
|
+
and decrypt it on your own machine, send, and listen for events.
|
|
5
|
+
|
|
6
|
+
from mailcycle import Mailcycle
|
|
7
|
+
|
|
8
|
+
client = Mailcycle.sign_in("your twelve word recovery phrase ...")
|
|
9
|
+
address = client.addresses.create(label="Sign-in codes")
|
|
10
|
+
message = client.messages.wait_for(address_id=address.id)
|
|
11
|
+
print(message.subject, message.text)
|
|
12
|
+
|
|
13
|
+
The recovery phrase never leaves this machine. Mail is sealed to a key derived
|
|
14
|
+
from it, so the API serves ciphertext it cannot read, and this package opens it
|
|
15
|
+
locally. Lose the phrase and the mail is gone; nobody at Mailcycle can help.
|
|
16
|
+
|
|
17
|
+
Documentation: https://docs.mailcycle.email/
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .client import Mailcycle
|
|
23
|
+
from .crypto.mnemonic import generate_phrase, validate_phrase
|
|
24
|
+
from .errors import (
|
|
25
|
+
APIError,
|
|
26
|
+
AuthProofError,
|
|
27
|
+
Base64URLError,
|
|
28
|
+
DecryptionError,
|
|
29
|
+
MailcycleError,
|
|
30
|
+
PhraseError,
|
|
31
|
+
)
|
|
32
|
+
from .events import EventStream
|
|
33
|
+
from .keys import Keys
|
|
34
|
+
from .models import (
|
|
35
|
+
Account,
|
|
36
|
+
AccountEvent,
|
|
37
|
+
ActivityEntry,
|
|
38
|
+
Address,
|
|
39
|
+
Attachment,
|
|
40
|
+
Device,
|
|
41
|
+
Domain,
|
|
42
|
+
Message,
|
|
43
|
+
MessagePage,
|
|
44
|
+
OutgoingAttachment,
|
|
45
|
+
Plan,
|
|
46
|
+
)
|
|
47
|
+
from .trackers import BlockedTracker, strip_remote_content, summarize_blocked
|
|
48
|
+
from .webhooks import verify_webhook_signature, webhook_signature_header
|
|
49
|
+
|
|
50
|
+
__version__ = "0.1.0"
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
"Mailcycle",
|
|
54
|
+
"Keys",
|
|
55
|
+
"EventStream",
|
|
56
|
+
"Account",
|
|
57
|
+
"AccountEvent",
|
|
58
|
+
"ActivityEntry",
|
|
59
|
+
"Address",
|
|
60
|
+
"Attachment",
|
|
61
|
+
"BlockedTracker",
|
|
62
|
+
"Device",
|
|
63
|
+
"Domain",
|
|
64
|
+
"Message",
|
|
65
|
+
"MessagePage",
|
|
66
|
+
"OutgoingAttachment",
|
|
67
|
+
"Plan",
|
|
68
|
+
"MailcycleError",
|
|
69
|
+
"APIError",
|
|
70
|
+
"AuthProofError",
|
|
71
|
+
"Base64URLError",
|
|
72
|
+
"DecryptionError",
|
|
73
|
+
"PhraseError",
|
|
74
|
+
"generate_phrase",
|
|
75
|
+
"validate_phrase",
|
|
76
|
+
"strip_remote_content",
|
|
77
|
+
"summarize_blocked",
|
|
78
|
+
"verify_webhook_signature",
|
|
79
|
+
"webhook_signature_header",
|
|
80
|
+
"__version__",
|
|
81
|
+
]
|
mailcycle/_encoding.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""base64url without padding, decoded strictly.
|
|
2
|
+
|
|
3
|
+
The permissive decoders accept several spellings of the same bytes, which
|
|
4
|
+
matters here: these strings are compared, stored and fed to a MAC, and a
|
|
5
|
+
server picks how they are spelled. So unknown characters are an error, padding
|
|
6
|
+
is an error, a length that cannot come from whole bytes is an error, and the
|
|
7
|
+
unused low bits of the final character must be zero. This is
|
|
8
|
+
`fromBase64Url` in the app's `src/crypto/bytes.ts`, rule for rule.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import base64
|
|
14
|
+
|
|
15
|
+
from .errors import Base64URLError
|
|
16
|
+
|
|
17
|
+
_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
|
18
|
+
_INDEX = {char: value for value, char in enumerate(_ALPHABET)}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def to_b64u(data: bytes) -> str:
|
|
22
|
+
"""Bytes to base64url, unpadded."""
|
|
23
|
+
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def from_b64u(value: str) -> bytes:
|
|
27
|
+
"""base64url to bytes, refusing anything non-canonical."""
|
|
28
|
+
if not isinstance(value, str):
|
|
29
|
+
raise Base64URLError("Expected base64url text.")
|
|
30
|
+
# One leftover character cannot encode any whole byte.
|
|
31
|
+
if len(value) % 4 == 1:
|
|
32
|
+
raise Base64URLError("Truncated base64url.")
|
|
33
|
+
|
|
34
|
+
out = bytearray()
|
|
35
|
+
buffer = 0
|
|
36
|
+
bits = 0
|
|
37
|
+
for position, char in enumerate(value):
|
|
38
|
+
index = _INDEX.get(char)
|
|
39
|
+
if index is None:
|
|
40
|
+
raise Base64URLError(f"Unexpected character at {position}.")
|
|
41
|
+
buffer = (buffer << 6) | index
|
|
42
|
+
bits += 6
|
|
43
|
+
if bits >= 8:
|
|
44
|
+
bits -= 8
|
|
45
|
+
out.append((buffer >> bits) & 0xFF)
|
|
46
|
+
buffer &= (1 << bits) - 1
|
|
47
|
+
|
|
48
|
+
# Whatever is left is padding bits, and in a canonical encoding they are
|
|
49
|
+
# zero. Non-zero means two strings would decode to the same bytes.
|
|
50
|
+
if bits and buffer:
|
|
51
|
+
raise Base64URLError("Non-canonical trailing bits.")
|
|
52
|
+
return bytes(out)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def be64(value: int) -> bytes:
|
|
56
|
+
"""Eight bytes, big-endian. The additional data's length, in a MAC input."""
|
|
57
|
+
return value.to_bytes(8, "big")
|