mailkite-dev 0.3.0__tar.gz → 0.5.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.5.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", "reply_spam", "reply_drop", "reply_block_sender", "encrypt", "decrypt"]
24
26
 
25
27
 
26
28
  def verify_webhook(signature, payload, secret, toleranceMs=DEFAULT_TOLERANCE_MS):
@@ -51,6 +53,103 @@ 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 reply_spam():
65
+ """Control-mode reply telling MailKite to mark the message as spam.
66
+
67
+ Returns the exact string ``{"status":"spam"}``. No network call."""
68
+ return '{"status":"spam"}'
69
+
70
+
71
+ def reply_drop():
72
+ """Control-mode reply telling MailKite to drop (discard) the message.
73
+
74
+ Returns the exact string ``{"status":"drop"}``. No network call."""
75
+ return '{"status":"drop"}'
76
+
77
+
78
+ def reply_block_sender():
79
+ """Control-mode reply telling MailKite to block the sender.
80
+
81
+ Returns the exact string ``{"status":"ok","actions":[{"type":"block-sender"}]}``.
82
+ No network call."""
83
+ return '{"status":"ok","actions":[{"type":"block-sender"}]}'
84
+
85
+
86
+ def encrypt(plaintext, public_key):
87
+ """Encrypt a UTF-8 string to a MailKite at-rest envelope (JSON string).
88
+
89
+ Hybrid encryption matching MailKite's at-rest scheme: a fresh AES-256-GCM
90
+ content key encrypts the plaintext, then that key is wrapped with the
91
+ customer's RSA-OAEP (SHA-256) ``public_key`` (SPKI/PEM). Only the holder of
92
+ the matching private key can :func:`decrypt`. No network call. Requires the
93
+ ``cryptography`` package."""
94
+ from cryptography.hazmat.primitives import hashes, serialization
95
+ from cryptography.hazmat.primitives.asymmetric import padding
96
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
97
+
98
+ pem = public_key.encode("utf-8") if isinstance(public_key, str) else public_key
99
+ pub = serialization.load_pem_public_key(pem)
100
+ spki_der = pub.public_bytes(
101
+ encoding=serialization.Encoding.DER,
102
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
103
+ )
104
+ fp = hashlib.sha256(spki_der).hexdigest()
105
+
106
+ content_key = AESGCM.generate_key(bit_length=256)
107
+ iv = os.urandom(12)
108
+ # AESGCM.encrypt returns ciphertext || 16-byte tag (matches WebCrypto).
109
+ ciphertext = AESGCM(content_key).encrypt(iv, plaintext.encode("utf-8"), None)
110
+ wrapped = pub.encrypt(
111
+ content_key,
112
+ padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
113
+ )
114
+
115
+ def b64(b):
116
+ return base64.b64encode(b).decode("ascii")
117
+
118
+ return json.dumps({
119
+ "v": 1,
120
+ "keyAlg": "RSA-OAEP-256",
121
+ "fp": fp,
122
+ "enc": "A256GCM",
123
+ "iv": b64(iv),
124
+ "wrappedKey": b64(wrapped),
125
+ "ciphertext": b64(ciphertext),
126
+ })
127
+
128
+
129
+ def decrypt(envelope, private_key):
130
+ """Decrypt a MailKite at-rest ``envelope`` (JSON string) back to plaintext.
131
+
132
+ Inverse of :func:`encrypt`: unwraps the AES-256-GCM content key with the
133
+ RSA-OAEP (SHA-256) ``private_key`` (PKCS8/PEM), then decrypts the ciphertext
134
+ (which carries its 16-byte GCM tag) and returns the UTF-8 string. No network
135
+ call. Requires the ``cryptography`` package."""
136
+ from cryptography.hazmat.primitives import hashes, serialization
137
+ from cryptography.hazmat.primitives.asymmetric import padding
138
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
139
+
140
+ env = json.loads(envelope)
141
+ pem = private_key.encode("utf-8") if isinstance(private_key, str) else private_key
142
+ priv = serialization.load_pem_private_key(pem, password=None)
143
+ content_key = priv.decrypt(
144
+ base64.b64decode(env["wrappedKey"]),
145
+ padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None),
146
+ )
147
+ plaintext = AESGCM(content_key).decrypt(
148
+ base64.b64decode(env["iv"]), base64.b64decode(env["ciphertext"]), None
149
+ )
150
+ return plaintext.decode("utf-8")
151
+
152
+
54
153
  class MailKiteError(Exception):
55
154
  def __init__(self, status, message, body=None):
56
155
  super().__init__(message)
@@ -90,6 +189,15 @@ class MailKite:
90
189
  and ``templateData`` (dict) to supply its variables."""
91
190
  return self.request("POST", "/v1/send", message)
92
191
 
192
+ def uploadAttachment(self, file):
193
+ """Upload a file (base64 ``content``) and get back a secure,
194
+ time-limited URL to reference as a send() attachment
195
+ (``{ filename, url }``) or link inline — instead of base64-inlining
196
+ large files on every send. ``file`` is a dict with ``filename`` and
197
+ ``content`` (base64 str), plus optional ``contentType`` and
198
+ ``retentionDays`` (7 | 30 | 90 | 365, default 7)."""
199
+ return self.request("POST", "/v1/attachments", file)
200
+
93
201
  def agent(self, message):
94
202
  """Send a message to an AI agent inbox. ``message`` is a dict with
95
203
  ``text`` (required) and optional ``subject``, ``from``, ``html``,
@@ -168,3 +276,33 @@ class MailKite:
168
276
  :func:`verify_webhook` — this is a thin instance wrapper, so you can
169
277
  call it on an existing client without re-importing."""
170
278
  return verify_webhook(signature, payload, secret, toleranceMs)
279
+
280
+ def reply_ok(self):
281
+ """Canonical webhook acknowledgement body. See module-level
282
+ :func:`reply_ok`."""
283
+ return reply_ok()
284
+
285
+ def reply_spam(self):
286
+ """Control-mode reply marking the message as spam. See module-level
287
+ :func:`reply_spam`."""
288
+ return reply_spam()
289
+
290
+ def reply_drop(self):
291
+ """Control-mode reply dropping the message. See module-level
292
+ :func:`reply_drop`."""
293
+ return reply_drop()
294
+
295
+ def reply_block_sender(self):
296
+ """Control-mode reply blocking the sender. See module-level
297
+ :func:`reply_block_sender`."""
298
+ return reply_block_sender()
299
+
300
+ def encrypt(self, plaintext, public_key):
301
+ """Encrypt to a MailKite at-rest envelope. See module-level
302
+ :func:`encrypt`."""
303
+ return encrypt(plaintext, public_key)
304
+
305
+ def decrypt(self, envelope, private_key):
306
+ """Decrypt a MailKite at-rest envelope. See module-level
307
+ :func:`decrypt`."""
308
+ 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.5.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.5.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