Metadata-Version: 2.5
Name: mailcycle
Version: 0.1.0
Summary: Python client for the Mailcycle API. Create addresses that receive mail, read and decrypt it on your machine, send, and listen for events.
Project-URL: Homepage, https://mailcycle.email/
Project-URL: Documentation, https://docs.mailcycle.email/
Author-email: Northlab Studios Ltd <hello@mailcycle.email>
License: Copyright (c) 2026 Northlab Studios Ltd. All rights reserved.
        
        mailcycle for Python is proprietary software owned by Northlab Studios Ltd, a company
        registered in England and Wales ("Northlab Studios"). It is not open source.
        
        1. Licence. Northlab Studios grants you a limited, non-exclusive,
           non-transferable, revocable licence to install and run this software, in
           unmodified form, solely to call the Mailcycle API for a Mailcycle account you
           are authorised to use, while you hold such an account, and in accordance with
           the Mailcycle Terms of Service at https://mailcycle.email/legal/terms/.
        
        2. Restrictions. Except as expressly permitted above or by applicable law that
           cannot be excluded, you may not copy, modify, adapt, translate, merge,
           publish, distribute, sublicense, sell, rent or lease the software or any
           part of it, create derivative works from it, or decompile, disassemble or
           reverse engineer it.
        
        3. Ownership. The software is licensed, not sold. Northlab Studios and its
           licensors keep all right, title and interest in it, including all
           intellectual property rights. No rights are granted except as set out in
           section 1.
        
        4. Termination. This licence ends automatically if you breach it or when you
           stop holding a Mailcycle account. On termination you must stop using the
           software and delete every copy of it.
        
        5. No warranty. To the extent permitted by law, the software is provided "as
           is", without warranty of any kind, and Northlab Studios is not liable for
           any claim, damages or other liability arising from it or its use, except as
           set out in the Mailcycle Terms of Service.
        
        6. Governing law. This licence is governed by the laws of England and Wales,
           and the courts of England and Wales have exclusive jurisdiction.
        
        Contact: hello@mailcycle.email
License-File: LICENSE
Keywords: email,encryption,end-to-end,mailcycle,privacy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: cryptography>=42
Requires-Dist: httpx>=0.27
Provides-Extra: events
Requires-Dist: websockets>=13; extra == 'events'
Description-Content-Type: text/markdown

# mailcycle

A Python client for the Mailcycle API that does the encryption on your machine. Create addresses that receive mail, read what arrives, send, and listen for events. Mailcycle stores mail sealed to keys derived from your recovery phrase, and this package derives them locally; the phrase never leaves your machine.

Needs Python 3.10 or later.

```sh
pip install mailcycle          # add [events] for the live event stream
```

## Sign in

```python
import os
from mailcycle import Mailcycle

# With the recovery phrase: everything the app can do, on any plan.
mc = Mailcycle.sign_in(os.environ["MAILCYCLE_PHRASE"])

# With an API key (Operator and up). Add the phrase to open mail and create
# addresses that receive it.
scripted = Mailcycle.with_api_key(
    os.environ["MAILCYCLE_API_KEY"],
    phrase=os.environ.get("MAILCYCLE_PHRASE"),
)
```

Signing in uses the same handshake as the app: the server sends a challenge and the client proves it holds the key, so nothing secret crosses the wire. With an API key and a phrase, the phrase is checked against the key's account before anything is opened.

Deriving the keys runs the phrase through 120,000 PBKDF2 iterations, which takes a few tens of milliseconds here because Python's `hashlib` does it natively. The app pays far more for the same bytes, on its own JavaScript. Each `sign_in` opens a new 30-day session: a script that runs often can keep `mc.session_token` somewhere private and pick it up with `Mailcycle.resume(token, phrase)`, and `mc.sign_out()` ends a session it no longer needs.

`Mailcycle.create_account()` makes a new account and returns the client and its phrase. Keep the phrase. Nothing can recover the account without it, and nobody at Mailcycle can help.

## Addresses that receive mail

```python
address = mc.addresses.create(prefix="signup", label="Test run 42")
print(address.email_address)  # signup-x7k2@…
```

The address's public key is derived from the phrase and sent with it, so the mail server can seal what arrives. The label is sealed on your machine, and the server stores it as ciphertext. `addresses.list()`, `update()` and `delete()` do the rest.

## Reading mail

```python
for message in mc.messages.list(address.id):
    print(message.from_address, message.subject, message.text)

data = mc.messages.attachment(message, 0)   # bytes, opened locally
mc.messages.mark_read(message.id)
```

`messages.iterate(address.id)` walks every page for you.

A message that will not open, because there is no phrase or it is the wrong one, comes back with `opened=False` and empty content rather than raising, so one unreadable message does not hide the rest of a page.

`message.html` is the HTML as the sender wrote it, remote images and all. `message.safe_html` has everything remote removed, the same way the app does it, and is the one to render; `message.blocked` says what was taken out and who it would have reported to.

## Waiting for mail

```python
code = mc.messages.wait_for(address_id=address.id, subject="verification", timeout=60)
```

This listens on the event stream and returns the next matching message, opened. Where the stream is not available (an API key below Scale, or no `websockets` installed) it checks the address every five seconds instead, so `address_id` is needed then. If nothing arrives in time it raises an `APIError` with `code="timeout"`. Mail received from `since` on counts, even if it landed before the stream was up; it defaults to now, so to wait for a reply to something you are about to send, take the time before sending.

## Sending

```python
mc.messages.send(
    from_address=address.email_address,
    to="someone@example.com",
    subject="Hello",
    text="Hi",
)

mc.messages.send(
    from_address=address.email_address,
    to=message.reply_to,
    subject=f"Re: {message.subject}",
    text="Thanks",
    reply_to=message,     # fills in the threading headers
)
```

Attachments take raw bytes and the client encodes them. A message can carry up to ten files, 3.5 MB between them. Give a file a `content_id` to show it inline, where the HTML says `cid:` and that id. Program and script files, like `.exe` and `.js`, are refused.

```python
from pathlib import Path
from mailcycle import OutgoingAttachment

mc.messages.send(
    from_address=address.email_address,
    to="someone@example.com",
    subject="Your invoice",
    text="Attached.",
    html='<img src="cid:logo"><p>Attached.</p>',
    attachments=[
        OutgoingAttachment("invoice.pdf", Path("invoice.pdf").read_bytes(), mime_type="application/pdf"),
        OutgoingAttachment("logo.png", Path("logo.png").read_bytes(), mime_type="image/png", content_id="logo"),
    ],
)
```

A session can send on every plan; an API key on Scale and up. Limits are in the [rate limits](https://docs.mailcycle.email/reference/rate-limits/).

## Usage and activity

```python
usage = mc.usage()
print(usage["addresses"]["used"], "of", usage["addresses"]["limit"], "addresses")

for entry in mc.activity(50):
    print(entry.created_at, entry.kind)
```

`usage()` reads back the counters the API enforces its limits on, so it says what a request would be refused on before it is refused. `activity()` is the account's own event log: ids and times, never content.

## Events

```python
with mc.events.stream() as events:
    for event in events:
        print(event.type, event.payload)
```

The same events as webhooks, the moment they happen, and they carry ids rather than content. The stream reconnects after a drop, and events that happen while it is down are not replayed, so `on_reconnect=` is where to refetch whatever you are showing. Needs `pip install 'mailcycle[events]'`.

To check a webhook came from Mailcycle:

```python
from mailcycle import verify_webhook_signature

verify_webhook_signature(secret, raw_body, request.headers.get("Mailcycle-Signature"))
```

Pass the body exactly as it arrived, before any parsing.

## Errors

Everything raises a subclass of `MailcycleError`. An API failure is an `APIError` carrying the HTTP `status` and the API's own `code`, such as `plan_required` or `session_required`, which is what to match on rather than the message. See [errors](https://docs.mailcycle.email/reference/errors/).

## How it is checked

The encryption here is a second implementation of Mailcycle's scheme, and a second implementation is a liability unless it is proven rather than trusted. `tests/test_vectors.py` runs it against `packages/crypto-vectors/vectors.json`, the frozen vectors the app and the mail server are checked against: the same keys from the same phrase, the same boxes opened, the same proof and the same signature. The negative cases are checked too, because each is something a hostile server can try: the wrong additional data, another address's key, a box relabelled as the other version, an attachment served under the wrong object key.

Two further suites cover what the vectors cannot. `test_cross_implementation.py` has this package and the app seal for each other, since the additional data a record is bound to is the client's choice rather than the format's. `test_trackers.py` runs both tracker strippers over the same markup and requires identical output, because a tracker one reader blocks and another fetches is a privacy promise broken rather than a test going red.

## Licence

Proprietary. Copyright (c) 2026 Northlab Studios Ltd. All rights reserved. You may run this package, unmodified, to call the Mailcycle API for an account you're authorised to use. You may not copy, modify, redistribute or reverse engineer it. See [LICENSE](LICENSE) and the [Terms of Service](https://mailcycle.email/legal/terms/).
