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 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")