mailkite-dev 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailkite-dev
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Official MailKite SDK for Python — send and manage email over your own authenticated domain.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://mailkite.dev/docs/libraries
@@ -8,6 +8,7 @@ Project-URL: Repository, https://github.com/mailkite/mailkite
8
8
  Keywords: mailkite,email,transactional,api
9
9
  Requires-Python: >=3.7
10
10
  Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography>=41
11
12
 
12
13
  # MailKite for Python
13
14
 
@@ -8,9 +8,11 @@ method per API endpoint. Zero dependencies — uses the standard library.
8
8
  res = mk.send({"from": ..., "to": ..., "subject": ..., "text": ...})
9
9
  """
10
10
 
11
+ import base64
11
12
  import hashlib
12
13
  import hmac
13
14
  import json
15
+ import os
14
16
  import time
15
17
  import urllib.error
16
18
  import urllib.parse
@@ -20,7 +22,7 @@ DEFAULT_BASE_URL = "https://api.mailkite.dev"
20
22
  # Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
21
23
  DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
22
24
 
23
- __all__ = ["MailKite", "MailKiteError", "verify_webhook"]
25
+ __all__ = ["MailKite", "MailKiteError", "verify_webhook", "reply_ok", "encrypt", "decrypt"]
24
26
 
25
27
 
26
28
  def verify_webhook(signature, payload, secret, toleranceMs=DEFAULT_TOLERANCE_MS):
@@ -51,6 +53,81 @@ def verify_webhook(signature, payload, secret, toleranceMs=DEFAULT_TOLERANCE_MS)
51
53
  return hmac.compare_digest(expected, v1)
52
54
 
53
55
 
56
+ def reply_ok():
57
+ """Return the canonical ``200 OK`` webhook acknowledgement body.
58
+
59
+ Inbound webhook handlers should reply with this exact string so MailKite
60
+ marks the delivery as accepted. No network call."""
61
+ return '{"status":"ok"}'
62
+
63
+
64
+ def encrypt(plaintext, public_key):
65
+ """Encrypt a UTF-8 string to a MailKite at-rest envelope (JSON string).
66
+
67
+ Hybrid encryption matching MailKite's at-rest scheme: a fresh AES-256-GCM
68
+ content key encrypts the plaintext, then that key is wrapped with the
69
+ customer's RSA-OAEP (SHA-256) ``public_key`` (SPKI/PEM). Only the holder of
70
+ the matching private key can :func:`decrypt`. No network call. Requires the
71
+ ``cryptography`` package."""
72
+ from cryptography.hazmat.primitives import hashes, serialization
73
+ from cryptography.hazmat.primitives.asymmetric import padding
74
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
75
+
76
+ pem = public_key.encode("utf-8") if isinstance(public_key, str) else public_key
77
+ pub = serialization.load_pem_public_key(pem)
78
+ spki_der = pub.public_bytes(
79
+ encoding=serialization.Encoding.DER,
80
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
81
+ )
82
+ fp = hashlib.sha256(spki_der).hexdigest()
83
+
84
+ content_key = AESGCM.generate_key(bit_length=256)
85
+ iv = os.urandom(12)
86
+ # AESGCM.encrypt returns ciphertext || 16-byte tag (matches WebCrypto).
87
+ ciphertext = AESGCM(content_key).encrypt(iv, plaintext.encode("utf-8"), None)
88
+ wrapped = pub.encrypt(
89
+ content_key,
90
+ padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
91
+ )
92
+
93
+ def b64(b):
94
+ return base64.b64encode(b).decode("ascii")
95
+
96
+ return json.dumps({
97
+ "v": 1,
98
+ "keyAlg": "RSA-OAEP-256",
99
+ "fp": fp,
100
+ "enc": "A256GCM",
101
+ "iv": b64(iv),
102
+ "wrappedKey": b64(wrapped),
103
+ "ciphertext": b64(ciphertext),
104
+ })
105
+
106
+
107
+ def decrypt(envelope, private_key):
108
+ """Decrypt a MailKite at-rest ``envelope`` (JSON string) back to plaintext.
109
+
110
+ Inverse of :func:`encrypt`: unwraps the AES-256-GCM content key with the
111
+ RSA-OAEP (SHA-256) ``private_key`` (PKCS8/PEM), then decrypts the ciphertext
112
+ (which carries its 16-byte GCM tag) and returns the UTF-8 string. No network
113
+ call. Requires the ``cryptography`` package."""
114
+ from cryptography.hazmat.primitives import hashes, serialization
115
+ from cryptography.hazmat.primitives.asymmetric import padding
116
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
117
+
118
+ env = json.loads(envelope)
119
+ pem = private_key.encode("utf-8") if isinstance(private_key, str) else private_key
120
+ priv = serialization.load_pem_private_key(pem, password=None)
121
+ content_key = priv.decrypt(
122
+ base64.b64decode(env["wrappedKey"]),
123
+ padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
124
+ )
125
+ plaintext = AESGCM(content_key).decrypt(
126
+ base64.b64decode(env["iv"]), base64.b64decode(env["ciphertext"]), None
127
+ )
128
+ return plaintext.decode("utf-8")
129
+
130
+
54
131
  class MailKiteError(Exception):
55
132
  def __init__(self, status, message, body=None):
56
133
  super().__init__(message)
@@ -168,3 +245,18 @@ class MailKite:
168
245
  :func:`verify_webhook` — this is a thin instance wrapper, so you can
169
246
  call it on an existing client without re-importing."""
170
247
  return verify_webhook(signature, payload, secret, toleranceMs)
248
+
249
+ def reply_ok(self):
250
+ """Canonical webhook acknowledgement body. See module-level
251
+ :func:`reply_ok`."""
252
+ return reply_ok()
253
+
254
+ def encrypt(self, plaintext, public_key):
255
+ """Encrypt to a MailKite at-rest envelope. See module-level
256
+ :func:`encrypt`."""
257
+ return encrypt(plaintext, public_key)
258
+
259
+ def decrypt(self, envelope, private_key):
260
+ """Decrypt a MailKite at-rest envelope. See module-level
261
+ :func:`decrypt`."""
262
+ return decrypt(envelope, private_key)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailkite-dev
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Official MailKite SDK for Python — send and manage email over your own authenticated domain.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://mailkite.dev/docs/libraries
@@ -8,6 +8,7 @@ Project-URL: Repository, https://github.com/mailkite/mailkite
8
8
  Keywords: mailkite,email,transactional,api
9
9
  Requires-Python: >=3.7
10
10
  Description-Content-Type: text/markdown
11
+ Requires-Dist: cryptography>=41
11
12
 
12
13
  # MailKite for Python
13
14
 
@@ -4,5 +4,6 @@ mailkite/__init__.py
4
4
  mailkite_dev.egg-info/PKG-INFO
5
5
  mailkite_dev.egg-info/SOURCES.txt
6
6
  mailkite_dev.egg-info/dependency_links.txt
7
+ mailkite_dev.egg-info/requires.txt
7
8
  mailkite_dev.egg-info/top_level.txt
8
9
  tests/test_sdk.py
@@ -0,0 +1 @@
1
+ cryptography>=41
@@ -7,12 +7,15 @@ build-backend = "setuptools.build_meta"
7
7
  # old account. Installs as `pip install mailkite-dev`; still imports as `import mailkite`
8
8
  # (the package dir below is unchanged). Rename back to `mailkite` once reclaimed.
9
9
  name = "mailkite-dev"
10
- version = "0.3.0"
10
+ version = "0.4.0"
11
11
  description = "Official MailKite SDK for Python — send and manage email over your own authenticated domain."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.7"
14
14
  license = { text = "MIT" }
15
15
  keywords = ["mailkite", "email", "transactional", "api"]
16
+ # `cryptography` powers the local at-rest encrypt()/decrypt() helpers; everything
17
+ # else in the SDK is standard-library only.
18
+ dependencies = ["cryptography>=41"]
16
19
 
17
20
  [project.urls]
18
21
  Homepage = "https://mailkite.dev/docs/libraries"
File without changes
File without changes