Skip to content
DerKezormPublic

About

Self-hosted email client for several IMAP mailboxes: conversations, full-text search, rules, contacts, tasks, multi-user and OpenID Connect sign-in, in one container

Topics

Resources

Stars

21 stars

Watchers

0 watching

Forks

Repository files navigation

nexmail

nexmail

A self-hosted email client. One container, one file, your mailboxes stay with you.

Report an issue

nexmail: folder column, message list and reading pane

nexmail brings several IMAP mailboxes together in one place and runs on your own machine. It is meant for people who already host things at home: one container, a single SQLite file, no second database, no cloud account, and no service that gets to see your mail on the way.

Roundcube is webmail bolted onto one mailbox. Thunderbird does not run in a browser. Commercial clients want to pull your mail onto their servers. nexmail is the third option — and if you can use Outlook, you should feel at home.

Version 0.29.0. It reads, writes, searches, sorts, labels, sends, schedules, prints, answers for you while you are away, keeps a calendar and your address books in sync with iCloud or Google, invites people to your own appointments, rephrases a draft with an AI service of your choice, backs itself up and reaches you when the browser is closed, on the desktop and on the phone. It is used daily by its author against real iCloud and IMAP mailboxes, and more than 2,000 automated tests watch over it, including more than 190 that drive a real browser against a real mailbox. It is still young: try it on a mailbox you can afford to have trouble with before you point it at the one that matters.

The screenshots below show a throwaway instance filled with invented mail. Every address in them is under example.com, example.org or example.net, which RFC 2606 reserves for exactly this purpose.

What it does

Mail. Several mailboxes side by side, plus a merged All inboxes view. Read, reply, reply-all, forward, move, archive, delete, mark, flag, undo. Drag and drop between folders. Keyboard: Del to trash, E to archive, and Ctrl+K opens a command palette that finds every action and folder. Which folder is the trash or the archive nexmail reads from the server's SPECIAL-USE flags and from the usual names; where a server has neither, right-click the folder and pick Use as (on a phone: the … next to the folder). Folders created or removed elsewhere show up with the next sync.

Reading pane, or not. A button next to the search field switches between three layouts: the reading pane to the right of the list, or no reading pane at all, with a message opening either in place of the list or in a window above it. Without the pane the list gets the full width, a click only selects, and a double-click or Enter opens. What opens after you delete, archive or move the open message is up to you, as in Outlook: nothing, the next message, the previous one or the newest (Settings, Appearance).

A face for every sender. Each row and the open message start with a round picture: the contact's photo from your address book, otherwise the sender's brand logo, otherwise their initials. Brand logos follow BIMI as strictly as Apple Mail does and are off by default (Settings, Appearance, Load company logos). A logo appears only when the message passed your provider's sender check, the domain enforces DMARC, and it publishes a mark certificate that chains up to a known BIMI issuer; the logo is taken from the certificate itself. The server looks each domain up once a week, your browser never contacts the sender. Most senders publish no BIMI record and get initials, and messages synced before 0.20.0 carry no sender check and never get a logo.

On the phone. A layout of its own rather than a squeezed desktop: list, then message, with the folders in a drawer. Swipe a row to archive or delete, long-press to select several and act on them from a bar within thumb reach. The calendar shows the month with a dot per calendar and the day's events below it, or the month as a running list.

Conversations. Related messages fold into one row — matched by the reply chain first, subject only as a fallback and only when sender, subject and a 30-day window all agree. Off by default: grouping wrongly hides a message, and you notice that at the worst possible moment.

Writing. A real formatting toolbar (Tiptap), attachments you can drop onto the window, pasted or dropped images inside the text, drafts stored in the mailbox rather than in the browser, and an outbox that survives a restart. Recipients become address bubbles as you type; nonsense turns red instead of hiding inside a comma list. Set importance, get a question when the text mentions an attachment that is not there, and forward a message as an untouched .eml when someone needs the real thing.

Send later, take it back. Schedule a message for this evening, tomorrow morning or any moment; it waits in nexmail's own outbox, visibly, and cancelling files it as a draft. Undo send holds every outgoing message for a few configurable seconds first — undo brings it straight back into the compose window.

The compose window with its formatting toolbar

Search. Full-text over everything cached, plus IMAP SEARCH at the provider on request. The interface says how far it looked — a search that hides its own reach lets you conclude a message does not exist.

Search results, with a line saying which part was covered

Tasks. Turn a message into a task, tick it off, drag it into order, give it a due date. The task survives its message: if the mail is moved from your phone, the task follows it by Message-ID; if the mail is deleted, the task stays and says so. Each user can connect their own Todoist account under Settings → Tasks and send tasks to a Todoist list instead. Only the title, the due date, the sender and a link back to the mail go there, never the text of the mail. Ticking a task in nexmail closes it in Todoist, and what you complete, reschedule or delete in Todoist comes back within a few minutes. The same works with a self-hosted nextasks: enter its address and an API token that may read and write, then pick a list, with its space and section. nexmail checks the token against /api/v1/me before it saves anything, sends each message at most once (nextasks' external_ref), and sees completed, reopened, rescheduled and binned tasks within a few minutes; when the token runs out, Renew token keeps your tasks connected. A nextasks on your local network needs the operator's permission under Administration → Servers, as webhooks and calendars do. For tools of your own there is a webhook target: nexmail posts each task as signed JSON to an address you choose (see Webhooks for tasks).

Contacts, rules, signatures. An address book with several numbers, addresses and postal addresses per contact, the card's photo, vCard import and export and collection from Sent; connected CardDAV address books (iCloud, Google or any other CardDAV server) that are read and written back line by line, with a question instead of an overwrite when a card changed on both sides, and a vCard file can be imported straight into one of them; contact groups that expand into their members when picked as a recipient; rules that run after each sync and only on new mail, can move, mark, flag and tag, can be run once over a folder that is already full, and can be started from a message with a right-click (Create rule); signatures per mailbox.

Housekeeping. Trash and junk can empty themselves after a number of days — off by default, measured by how long a message has been in the folder, and never touching a folder you created yourself. Printing produces a clean document and goes straight to the print dialog. On a phone, swiping a row archives or deletes, with the same undo as everywhere else.

Tasks Contacts
The task list The address book

Mailbox groups. Tag mailboxes (personal, work, club) and switch between them above the folder tree.

Labels. Colour a message Important, Work, Later. They are stored as IMAP keywords on the server, not in nexmail's database, so they outlive nexmail and show up in Thunderbird and on your phone. Labels another client set appear here too, with a name and a colour of their own.

Snooze. Park a message until this evening, tomorrow or next week. It moves into a real folder on the server (named Wiedervorlage there, shown as Snooze in the English interface) and comes back when it is due — visible from every other client, not a note that only nexmail knows.

Text templates. The paragraphs you write again and again, kept once and dropped into a message from the compose window.

Out of office. Per mailbox, with a period and a plain-text note. Every sender gets it at most once; mailing lists, other automated systems and delivery failures never get one, and mail that arrived before you left is not answered retroactively. You can see who received one. Note that nexmail can only answer while nexmail itself is running — your provider can usually do this on the mail server, independently of it.

Meeting invitations. An .ics shows up as a card rather than an unopenable attachment: title, time in your own time zone, place, organiser. Accept, tentative or decline goes back as a proper reply, and nexmail remembers what you answered. Add to calendar is a separate button on purpose: saying yes tells the organiser you are coming, it does not mean you want the entry.

Your own invitations. Put people on one of your events and nexmail asks whether to send the invitation — it never goes out by itself, not when you save and not when you change something later. Their answers come back into the attendee list as they arrive, so you can see who is coming without leaving the app. Delete the event and it offers to tell them; without that the appointment would stay in their calendars and they would turn up.

Calendar. Month, week and day, with your own calendars in nexmail or connected over CalDAV to iCloud, Nextcloud or anything else that speaks it. A server on your own network or VPN is refused until the operator allows it under Administration → Server; the same switch covers address books and subscribed calendars. Google works too, through the same consent you gave for the mailbox. Recurring events are stored once and worked out for the window on screen, so a every Monday does not fill the database to the year 2031; this one, this and following, all is asked the way every calendar asks it, because the format leaves no other answer. A published .ics link can be subscribed to read-only. A calendar can be downloaded as an .ics file and events read back in from one, so moving in or out does not mean retyping anything; events that are already there are skipped, so reading the same file twice is safe. What nexmail does not understand in a foreign event travels through untouched: alarms, attendees and a dozen X-APPLE- properties come back out the way they came in.

Reminders live on the event as a VALARM, so what you set here also goes off on your phone. They reach you through the browser as well, with nexmail closed — see Notifications below.

Notifications. nexmail can reach you with the browser closed: an event reminder that falls due, and mail the sync actually just fetched. Every device grants permission for itself; what you get notified about belongs to your account and is the same everywhere. New mail is announced after the rules have run, so a message a rule just filed into junk does not claim to be in your inbox — and a mailbox's very first sync announces nothing, because at that point every message is new. Quiet hours drop notifications rather than deferring them: a reminder delivered at seven in the morning for an event at eleven last night is not a reminder. On a phone this needs the site on the home screen; iOS delivers nothing to an ordinary tab, and says nothing about why.

Google and Microsoft mailboxes. Both providers stopped accepting a password for IMAP and SMTP, so nexmail signs in with OAuth. Two levels, kept apart on purpose: the operator registers one app with the provider, every user gives consent for their own mailbox. No secret ships with nexmail — the repository is public, and both providers revoke a key they find in one, so each operator registers their own. The administration page says so plainly rather than letting you guess why nothing works.

Rephrasing with AI, if you want it. Every user can connect their own AI service on the AI page: an address, a key and a model. nexmail knows no provider; anything that speaks the common chat completions interface works, from Claude, Gemini or OpenAI to Ollama on a machine in your own network. In the editor it fixes spelling, translates, or rephrases in one of nine tones, and shows old and new side by side before anything is replaced. Quotes and your signature are never sent, and the AI page lists verbatim what went out. Off by default, and the operator can block it for the whole installation or for single accounts.

Received mail, only if you allow it. With the same service, a button in the reading pane summarises or translates a message you received, or answers a question about it, and a reply can start from a suggested draft. All of that sends text somebody else wrote to your AI provider, so it is a decision of its own: the operator has a separate switch for received mail, each user has one more, and nothing goes out until you click. What went out then appears verbatim in the same list as everything else. Answers are shown as plain text and labelled as what they are, because a message can try to talk the model into saying something else. A suggested reply is plain text too, leaves what it cannot know in square brackets, and only reaches your draft when you apply it.

Unsubscribing. A message that names a way off its mailing list gets a button below the sender, and a question first that says who is about to hear from you. If the sender offers one-click unsubscribing (RFC 8058) and the message passed your provider's sender check, nexmail's server sends the request. Otherwise the sender's page opens in a new tab, or a prepared message waits in the compose window for you to send. In the junk folder there is no button: unsubscribing there tells a spammer the address is read. Messages opened before this feature existed keep what they had; the next one from the list has the button.

New mail right away. An open window now follows what the server fetched, so the list updates by itself. By default that happens with the regular sync, every two minutes. Turn on See new mail right away for a mailbox and nexmail keeps a second connection open (IMAP IDLE), and a new message shows up within seconds. It is off by default because some providers limit connections; if the server cannot do IDLE, the form says so and the regular sync carries on.

Moving between mailboxes. Drag a message onto a folder of another account, or right-click it, Move, and pick one. MOVE and COPY only work inside one connection, so this fetches, appends at the target, checks that it really arrived, and only then deletes at the source. That order is the whole safety: the worst case is a message that exists twice, which you can see and clean up. A selection may span mailboxes and folders, as in All inboxes or a search: Del puts every message into the trash of its own mailbox, Archive into its own archive, and undo brings them all back.

Moving in and out. Import a Thunderbird mbox, or download a folder as mbox or as a ZIP of .eml files. What is already there is skipped by Message-ID, so an interrupted import can simply be repeated. Read and flagged state travels both ways.

HTML mail, defused. Server-side sanitising with an allow-list, images unhooked until you ask for them, and an iframe sandbox without scripts. The same sanitising runs on what you write, on the way out.

When you do ask for the images, nexmail's server fetches them, your browser never does. The sender learns that the message was opened, which is the price of seeing the pictures in any client, but not your address, your browser or anything else about you. Allow a sender for good with one click, or switch the asking off entirely under Appearance. Addresses inside your own network are refused: a message pointing at http://192.168.x.x/reset would otherwise make the server a remote control for your home network.

What it is not

  • No PGP or S/MIME. Key management is a project of its own, and half-built encryption is worse than none.
  • No Microsoft calendar. Microsoft speaks no CalDAV; events there need the Graph API, which is a client of its own. Mail through IMAP and SMTP works.
  • No out-of-office reply while nexmail is down. It answers only when the container runs. Your provider can usually do this on the server instead, independently of nexmail.

Built with

FastAPI · SQLAlchemy · SQLite · React · Vite · Tailwind CSS · Tiptap · IMAPClient

One container. SQLite instead of Postgres is not a shortcut here, it is a feature: no second container, no database credentials, and a backup is a copy.

Running with Docker

services:
  nexmail:
    image: ghcr.io/derkezorm/nexmail:latest
    container_name: nexmail
    restart: unless-stopped
    ports:
      - "5174:8000"
    volumes:
      - ./data:/data
    environment:
      PUID: 1000
      PGID: 1000
docker compose up -d

Then open http://<server>:5174. On first run you set a username and password. After that, this route is closed.

⚠️ Until the first account exists, anyone can create it. There is no account that could authenticate at that point — it cannot work any other way. Set it up on your home network before you expose it.

The full compose file with every option and the reasoning behind each one is docker-compose.yml in this repository.

Your mailbox credentials

nexmail asks for them in its own interface and nowhere else. You add mailboxes under Settings → Mailboxes. They are stored encrypted and never leave your server.

The mailbox list under Settings

For iCloud you need an app-specific password from account.apple.com. Your normal Apple password is refused, and iCloud reports that with the same message it uses for a typo. nexmail says so in the connection test, so you do not spend ten minutes doubting your password.

What to back up

Everything is under /data:

nexmail.db mailboxes, messages, contacts, rules, sessions
secret.key the key to every stored mailbox password
blobs/ attachments, stored by checksum

⚠️ secret.key and the database belong together. Without the key the database is unreadable. With both, whoever holds them holds your mailboxes.

Backup and restore

Under Administration → Backup. Two things live there, and telling them apart matters when it counts:

Restore points are complete copies next to the database, made automatically when an update adds or rebuilds a table (not for a mere new column or index), and by hand whenever you want, on a schedule if you like. They are for a failed update or a contact deleted by mistake. If the disk dies, they die with it.

A backup is what you download: an encrypted ZIP, and deliberately without the messages. Those are still in the mailbox on the server, and nexmail fetches them back after a restore. Measured on a real database: 1890 messages turn 0.36 MB into 2.79 MB, and the part that cannot be fetched again does not grow with the mailbox. An archive that grows with your mail is one nobody downloads.

Restoring happens in two steps. The first only looks: it reports the version, whether the key is included, and whether the public address in the archive differs from the one you are using. It has to ask about that one, because invitation links and the OpenID Connect redirect are built from it — and the redirect URI registered with your provider is something nexmail cannot change for you, so the report spells it out.

⚠️ Put /data on a local disk, never on an SMB or NFS share. That is the one way SQLite genuinely loses data: its locking does not work reliably over network filesystems. On a NAS, use a path on the internal volume.

Behind a reverse proxy

Set NEXMAIL_URL_BASE=/nexmail and nexmail runs under https://example.org/nexmail. It answers every address twice — with and without the prefix — because proxies are configured both ways.

Set NEXMAIL_COOKIE_SECURE to match your setup. auto (the default) marks the session cookie Secure when the request arrived over HTTPS. Use on only if a proxy terminates TLS and forwards plain HTTP internally — and never if nexmail should also be reachable over http://, because the browser discards a Secure cookie sent over http and then nobody gets in.

Trusted proxies. NEXMAIL_CLIENT_IP=proxy (or proxy:2 for two proxies in a row) tells nexmail that a reverse proxy sits in front. Only then does it believe the proxy's headers: the client address in X-Forwarded-For (for the sign-in brake) and the scheme in X-Forwarded-Proto (the rightmost value, http or https). That scheme decides the redirect address for a sign-in provider while no public address is set, and Secure on the session cookie with NEXMAIL_COOKIE_SECURE=auto. Left empty (the default) or direct, nexmail takes neither header from anyone. This is the setting the authentik card means when it warns that the server sees another scheme than your browser.

Notifications on the iPhone. Apple's push key cannot ship in a public image, so the iPhone app gets its notifications through a small relay at https://push.nexapps.dev. Sender, subject and the start of the mail are encrypted for the device before they leave your server; the relay sees a ticket, that ciphertext and the number for the app icon. NEXMAIL_PUSH_RELAY points to another relay. Set it empty and nothing goes there, and the app offers no notifications.

Signing in through an external provider

nexmail speaks OpenID Connect: authentik, Microsoft Entra ID, Keycloak, Authelia, Pocket ID and anything else that follows the spec. Everything lives under Administration → Sign-in, in three cards:

  1. Sign-in. Sign-in with a password can be switched off once a provider is active; then the sign-in page shows only the provider buttons. You as the operator can always sign in with your password (a small link on the sign-in page), so a provider that fails never locks you out. The public address is the address people use to reach nexmail, base path included; invitations, providers and links in mails use it.
  2. Sign-in providers. Add a provider by hand: name on the button, a short name (part of the redirect address, fixed once saved), the issuer, client ID and secret. The redirect address to register with the provider is shown with a copy button: <public address>/api/oidc/<short name>/callback. Saving asks the provider's discovery document first; an issuer that calls itself something else is refused at the field. Changing the issuer of a provider asks first and then drops its links, because another issuer is another provider. Removing one tells you how many accounts lose their link.
  3. authentik in one step. Enter the address of your authentik and a one-time API token that may create applications. nexmail creates the signing key, provider and application in authentik and adds itself as a provider; the steps are listed with a reason if one fails. The token is used for these calls only and never stored. If you would rather not hand over a token, download the blueprint instead and import it in authentik. Without a public address the card suggests the one your browser uses and stores it before anything is registered.

Each provider has two more switches: New people get an account (off by default: only invited people and linked accounts come in) and The provider checks the second factor itself (on by default; switched off, an account with its own second factor is asked for the code after the provider, as after the password).

Microsoft Entra ID works with a single tenant (https://login.microsoftonline.com/<tenant ID>/v2.0) as well as with common or organizations. Entra only sends an email address as an optional claim; nexmail does not need one.

The sign-in tab with one registered provider

Who comes in:

  1. An existing link. You create it while signed in, under Settings → Security → Sign-in providers; your password confirms it. The operator sees the links in the account list and can remove one, with their own password.
  2. An invitation. On the invitation page, Continue with … takes the invitation key along (in the request body, never in an address). The new account is named after the person at the provider, keeps the display name and contact address from the invitation, and the invitation is spent.
  3. New people, only where a provider has New people get an account on.

⚠️ No email address opens anything, not even an invitation. Matching by address would be the place where a provider claiming an address it does not own could open somebody else's account; in authentik, for instance, users can change their own address. A person is known by provider and subject only.

⚠️ Providers set up in nexmail 0.21 keep their redirect address /api/oidc/<short name>/zurueck; you do not have to change anything at the provider. New providers use /api/oidc/<short name>/callback. Under a sub path (NEXMAIL_URL_BASE) the stored public address stays without the prefix, as in 0.21, and nexmail adds the prefix itself, so the redirect address stays the same and going back to 0.21 keeps working. An address saved with the prefix is stored without it.

⚠️ The issuer must be one address that both the browser and the server can reach. The browser fetches the sign-in page; the container fetches the discovery document and the token. If you run the provider in the same Docker network, enter it under the address the browser knows, not its container name.

Several people

Invite them under Administration → Users. nexmail sends the invitation itself, which needs an outgoing mail server of its own under Administration → Server — deliberately not the operator's mailbox, so that removing that mailbox does not take the invitations with it.

The invitation page offers both ways: set a password, or accept it through a provider. Not everyone has an account with your identity provider, and not everyone should need a password they will never use.

A forgotten password can be reset through a link sent to the contact address stored on the account; the link lasts two hours and signs out every session when it is used. The operator role can be handed on to another user, after confirming with your own password, so an installation never ends up without someone who can administer it.

Administration with users and open invitations

Every user has their own mailboxes, contacts, rules and signatures. A guard test walks the entire route table and checks that no address answers without authentication and none hands out another user's data.

Dashboards: read-only API keys

A dashboard such as nexdeck can show how many unread messages are waiting and, if you allow it, sender and subject of the latest ones. It reads them with an API key that each user creates for their own mailboxes under Settings → API keys.

Off by default. API keys are a way out of nexmail, so the operator decides whether they exist on an installation at all, with a switch on the same page. Turning it off stops every key immediately; no key is deleted.

Each key has two limits, set when it is created. It sees only the mailboxes ticked on it, and it has one of two scopes: count only, or count, sender and subject. The text of a message is never available through a key. Keys are stored as hashes and shown once; revoking one takes effect on the next request.

All addresses answer JSON and expect the key as Authorization: Bearer nxm_…. A browser session does not open them, and a key opens nothing else. Only the task addresses write, each only with its own option on the key: May tick off tasks for complete and reopen, May tag messages for tags. Both are meant for the receiver of a webhook.

Address Returns
GET /api/v1/me owner, key name, scope (count or headers), tasks (whether the key may tick off tasks), tags (whether it may tag messages), the mailboxes the key may see
GET /api/v1/summary unread messages in the inbox, in total and per mailbox, with status ok or sign_in_failed
GET /api/v1/messages/latest sender, subject, date and read state of the newest inbox messages; needs scope headers. Optional: mailbox=<id> (repeatable), limit=1..20 (default 5), unread_only=true
POST /api/v1/tasks/{id}/complete ticks off a task from one of the key's mailboxes; returns id, external_id, title, done, completed_at
POST /api/v1/tasks/{id}/reopen reopens it; same answer
GET /api/v1/tags the owner's tags with id, name and keyword; needs May tag messages
GET /api/v1/tasks/{id}/tags the tags on the message of a task, as task_id and tags
POST /api/v1/tasks/{id}/tags/{tag_id} adds a tag to the message of a task on the mail server; returns the tags afterwards
DELETE /api/v1/tasks/{id}/tags/{tag_id} removes it; same answer

Errors come as {"detail": "<code>"}: api_schluessel_fehlt and api_schluessel_ungueltig (401), api_schluessel_abgeschaltet, api_schluessel_nur_anzahl, api_schluessel_ohne_aufgaben and api_schluessel_ohne_schlagworte (403), postfach_unbekannt, aufgabe_unbekannt and schlagwort_unbekannt (404), aufgabe_ohne_mail (409), schlagworte_nicht_unterstuetzt (400, the mail server takes no keywords in that folder) and 502 when the mail server cannot be reached. Counts are as fresh as nexmail's last sync with the mail server.

Webhooks for tasks

Under Settings → Tasks each user can connect a webhook next to Todoist. It then shows up as a list in Turn into task, and nexmail sends the task as a POST with a JSON body to your address. Before it saves the webhook, nexmail sends a ping and keeps the address only if it answers with 2xx; Send test does the same later and shows the status code if it does not.

What arrives. task.created when a message becomes a task, task.completed and task.reopened when it is ticked or unticked in nexmail, and ping.

{
  "event": "task.created",
  "sent_at": "2026-11-03T08:15:00Z",
  "task": {
    "id": "42",
    "external_id": null,
    "title": "Order confirmation",
    "due_date": "2026-11-10",
    "completed_at": null
  },
  "message": {
    "message_id": "<abc123@example.org>",
    "subject": "Order confirmation",
    "from": "supplier@example.com",
    "from_name": "Supplier",
    "date": "2026-11-03T08:12:00Z",
    "mailbox": "Work",
    "mailbox_address": "me@example.com",
    "link": "https://nexmail.example.org/?mail=%3Cabc123%40example.org%3E",
    "tags": [{ "id": 3, "name": "Work", "keyword": "Work" }]
  }
}

task.completed and task.reopened carry the same task and only message_id, link and tags under message; tags is null there if the message is no longer in nexmail. If your endpoint answers task.created with JSON such as {"id": "T-17", "url": "https://…"}, nexmail keeps both: the id comes back as external_id in later events, and the url becomes the link to the task in nexmail.

The text of the message (text, html, text_truncated under message) is only sent if the user turns it on for the webhook and the operator allows it under Administration → Server. It is off by default, because on a shared installation any user can enter any address. html is the cleaned HTML nexmail shows itself, without scripts; very long messages are cut, and text_truncated says so.

The signature. Every request carries X-Nexmail-Signature: sha256=<hex>, an HMAC-SHA256 over <X-Nexmail-Timestamp>.<raw body> with the webhook's secret. The secret is shown once when you connect; leave the field empty and nexmail creates one. Check the signature against the raw body and reject old timestamps, so a recorded request cannot be replayed. In Python:

import hashlib, hmac, time

def valid(secret: str, headers, body: bytes) -> bool:
    stamp = headers["X-Nexmail-Timestamp"]
    if abs(time.time() - int(stamp)) > 300:
        return False
    expected = hmac.new(secret.encode(), stamp.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(headers["X-Nexmail-Signature"], f"sha256={expected}")

X-Nexmail-Event names the event, X-Nexmail-Delivery is a random ID per request.

Who owns the checkbox. A webhook cannot be asked what happened, so by default the task stays in nexmail and nexmail owns the checkbox. Two ways to change that:

  • Send only: nexmail sends task.created and keeps no task of its own. The message still remembers where it went, so a second click does not send it again.
  • Report back: create an API key with May tick off tasks for the same mailboxes and call POST /api/v1/tasks/{id}/complete or /reopen with the task.id from the payload. Such a change is not sent back to the webhook.

Tags. An API key with May tag messages can list the user's tags and add or remove them on the message of a task, for example a Done tag when the task is ticked off in your tool. Tags are IMAP keywords, so this writes to the mail server, the same way as tagging in nexmail does, and every copy of the message in that mailbox gets it.

  • GET /api/v1/tags returns {"tags": [{"id": 3, "name": "Done", "keyword": "Done"}]}. Use the id: it stays when a tag is renamed, and a name with accents does not have to go into the URL.
  • GET /api/v1/tasks/{id}/tags returns the tags on the message now.
  • POST /api/v1/tasks/{id}/tags/{tag_id} adds one, DELETE removes it. Both answer with the tags afterwards, as {"task_id": "42", "tags": [...]}.

A task whose message is gone answers 409 with aufgabe_ohne_mail; a key without the option gets 403. Tag changes send no event to the webhook.

Addresses. Only http and https, no credentials in the URL, and redirects are not followed. Addresses on the local network work only after the operator allows them under Administration → Server (the same switch as for calendar servers). localhost never works: inside the container it is nexmail itself. If your tool runs on the same host as the nexmail container, use the host's address on your network, for example http://10.0.0.20:8080/hook.

Security, honestly

Two-factor is available and optional. TOTP with replay protection, ten single-use recovery codes, and a per-user switch — including for the operator. On a purely local network it is reasonable to leave it off; reachable from the internet it is not, and nexmail says so where you make the choice.

Sessions live on the server, not in a token. That costs one query per request and buys sign out everywhere with immediate effect. Devices are listed and can be revoked individually.

The security tab with two-factor and signed-in devices

Passwords use Argon2id, sign-ins are rate-limited, and a spent recovery code stays spent.

What two factors cannot do — plainly. Mailbox passwords must be decryptable on disk, because syncing runs while nobody is signed in. The key sits in /data/secret.key. Whoever has file access has your mailboxes, and no login changes that. Setting NEXMAIL_SECRET_KEY moves the key out of the data directory, which is the one effective lever: a stolen backup alone is then worthless.

Development

# Backend
cd backend && python -m venv .venv && .venv/bin/pip install -r requirements.txt
NEXMAIL_DATA_DIR=../data-dev .venv/bin/python -m uvicorn app.main:app --reload --port 8010

# Frontend
cd frontend && npm install && npm run dev
cd backend && python -m pytest        # server, no mailbox needed
cd frontend && npm test               # Vitest, no browser
cd frontend && npm run test:ui        # Playwright, two viewports

The interface tests run in a real browser against a real IMAP mailbox. That is why they prove anything — jsdom would not have found a single one of the layout and rendering faults they were written for.

They only start the Vite server, though. Whatever answers on port 8010 is what they measure, and a backend left running from yesterday will report faults the code no longer has. Check data-dev/logs/nexmail.log for the last is ready line before you believe a red run, or point the proxy elsewhere with NEXMAIL_API=http://127.0.0.1:8011 npm run dev.

Licence

GNU Affero General Public License v3.0. Third-party licences, including the bundled fonts, are listed in THIRD-PARTY.md.

About

Self-hosted email client for several IMAP mailboxes: conversations, full-text search, rules, contacts, tasks, multi-user and OpenID Connect sign-in, in one container

Topics

Resources

Stars

21 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages