mailsync docs

This is the public part of the mailsync README. The security notes, the example configuration and the rest are in the private repository you get access to with your purchase.

Install

mailsync is licensed per purchase and distributed as a private GitHub repository. After buying you receive access to sofatools/mailsync; clone it and install it with uv (Python 3.12 or newer):

git clone git@github.com:sofatools/mailsync.git
cd mailsync
uv tool install .          # puts `mailsync` on your PATH
# or, without installing:
uvx --from . mailsync --help

To update, git pull and run uv tool install . --reinstall.

Configure

The configuration lives at ~/.config/mailsync/config.toml ($XDG_CONFIG_HOME is honoured; override the path with --config or $MAILSYNC_CONFIG). Start from config.example.toml:

mkdir -p ~/.config/mailsync
cp config.example.toml ~/.config/mailsync/config.toml

Every account is a [accounts.<key>] table with kind ("imap" or "ews"), host, username and exactly one password source:

  • password_env = "MAILSYNC_GMX_PASSWORD" – read from an environment variable.
  • password_cmd = "pass show mail/proton-bridge" – run a command, the first line of its output is the password. Works with pass, op read, secret-tool lookup, security find-generic-password -w and the like. The command runs without a shell, so pipes and && do not work; wrap them in a small script if you need them.

Passwords are never written to the configuration, the database or the logs. Optional keys: port (default 993), security ("ssl" or "starttls"), tls_verify, folders (list of folder paths or names to sync; default all; "Inbox", "Sent Items", "Drafts", "Trash", "Junk" and "Archive" also match the server’s own folder for that role, e.g. Posteingang on a German Exchange; a name that matches nothing is reported), display_name. The database path defaults to ~/.local/share/mailsync/mail.db and can be set with db = "..." at the top of the file.

Providers

Proton Mail (Proton Mail Bridge)

Proton has no public IMAP; the Proton Mail Bridge runs on your machine and exposes your mailbox over local IMAP (a paid Proton plan is required).

  1. Install the Bridge (package for your distribution, or the protonmail-bridge AUR package on Arch).
  2. Log in once with the CLI:
    protonmail-bridge --cli
    >>> login          # Proton address, password, 2FA code
    >>> info           # shows the IMAP user and the *Bridge password*
    >>> exit
    The Bridge password is generated by the Bridge for local IMAP access; it is not your Proton password. Store it, e.g. pass insert mail/proton-bridge.
  3. Keep the Bridge running whenever you sync – as a user service, for example systemd-run --user --unit=protonbridge protonmail-bridge --noninteractive, or a proper ~/.config/systemd/user/protonbridge.service.
  4. Configure the account as in the example: host = "127.0.0.1", port = 1143, security = "starttls", tls_verify = false (the Bridge certificate is self-signed). If you prefer SSL, run change imap-security in the Bridge CLI and set security = "ssl".

Proton labels (Labels/*) and the virtual “All Mail”/“Starred” folders are skipped; every real folder is synced.

iCloud Mail

Create an app-specific password at https://appleid.apple.com (Sign-In and Security → App-Specific Passwords). Then host = "imap.mail.me.com", port = 993, username = "you@icloud.com", password = the app-specific one.

GMX

Enable IMAP in the GMX web interface (Einstellungen → POP3/IMAP Abruf → IMAP-Zugriff erlauben). Then host = "imap.gmx.net", port = 993.

Exchange / EWS

Exchange Server (on-premises) with Exchange Web Services and Basic or NTLM authentication. Set kind = "ews", host (the EWS host, e.g. mail.example.com), username (DOMAIN\\you or UPN), email (your primary SMTP address) and auth = "ntlm" or "basic". If the server’s certificate comes from an internal CA your system does not trust, set tls_verify = false for that account. Microsoft 365 with modern authentication (OAuth) is not supported in this version.

Sync

mailsync sync                 # all accounts
mailsync sync --account gmx   # one account
mailsync sync --max 500       # at most 500 mails per account this run

The first run fetches the inbox first, then sent mail, the archive and other folders, with trash and junk last; within an IMAP folder the newest mail comes first. An interrupted or capped run resumes where it stopped. For IMAP, --max caps the older mail and new mail is always fetched; for Exchange it caps every change the server hands over, new ones included. Later runs are incremental (UIDVALIDITY/UID for IMAP, the SyncFolderItems state for Exchange). A failing account is reported and the other accounts still run; mailsync status shows the last error per account, and the exit code is 1 when any account or folder failed. Only one mailsync sync runs per database at a time; a second one stops with “already running”.

Run it on a timer, e.g. with systemd user units:

# ~/.config/systemd/user/mailsync.service
[Unit]
Description=mailsync
[Service]
Type=oneshot
ExecStart=%h/.local/bin/mailsync sync
# ~/.config/systemd/user/mailsync.timer
[Unit]
Description=mailsync every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target

systemctl --user enable --now mailsync.timer. If a password comes from pass or another agent-backed store, make sure the agent is reachable from the service (e.g. gpg-agent with a cached key).

CLI

mailsync search "kündigung vertrag"            # FTS5 query over subject, sender, text
mailsync search "" --from rechnung@ --since 2026-10-01 --account gmx
mailsync search invoice --json                 # machine-readable
mailsync show 123                              # headers, attachments, text
mailsync show 123 --html                       # include the HTML body
mailsync accounts                              # accounts, counts, last sync, last error
mailsync status                                # per-folder counts and backfill state

Queries use FTS5 syntax (word, "a phrase", wor*, a AND b, a NOT b). Characters that FTS5 would reject (@, :, unbalanced quotes) are handled: rechnung@firma.de finds that address. search, show, accounts and status take --json.

MCP

mailsync mcp serves the mirror over stdio. With Claude Code:

claude mcp add mailsync -- mailsync mcp

Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "mailsync": {
      "command": "mailsync",
      "args": ["mcp"]
    }
  }
}

Use the absolute path of the mailsync binary (which mailsync) if the client does not inherit your PATH. Run mailsync sync at least once before starting the server; without a database it exits with no database at … – run `mailsync sync` first instead of creating an empty one.

Tools, all annotated readOnlyHint: true, destructiveHint: false:

Tool Purpose
search_mail(query, account, sender, recipient, since, limit) full-text search; empty query lists the newest mail
get_mail(id, include_html) one mail with recipients, text and attachment names
list_threads(account, since, limit) conversations, newest activity first
get_thread(thread_key) all mails of one conversation, oldest first
list_accounts() accounts with counts, last sync and last error

There are no write tools.

Source: mailsync README.md, release v0.1.1, commit 3bfbea4