imapt (0.1.0)

Published 2026-09-02 04:11:17 +02:00 by homberger

Installation

pip install --index-url  imapt

About this package

Bulk IMAP credential validation and mailbox search from the command line.

imapt

English | Deutsch

Bulk IMAP credential validation and mailbox search from the command line.

imapt reads a list of email:password credentials, works out the right IMAP server for each account's domain, then either checks whether each credential can log in or searches mailboxes for a pattern (FROM / SUBJECT / BODY). Results are shown live with rich and fall back to plain, greppable lines when piped.

  • Pure standard library for the protocol work (imaplib, sqlite3, ssl).
  • Only two runtime dependencies: [click] and [rich].
  • Per-domain IMAP endpoints are cached in a small SQLite database.

This tool is intended for auditing your own accounts or authorized security testing. Only point it at credentials you are permitted to use.

Requirements

  • Python 3.10+
  • uv (recommended) for installation

Installation

Install the imapt command globally from a checkout of this repository:

git clone https://github.com/your-org/imapt.git
cd imapt
uv tool install .

This puts an imapt executable on your PATH (in uv's tool environment):

imapt --version

Install in editable mode if you plan to hack on it and want changes to apply immediately:

uv tool install -e .

Run without installing

From the repository root, uv run creates a project virtualenv, installs the dependencies, and runs the CLI in one step:

cd imapt
uv run imapt --help

With pip (alternative)

pip install .        # or: pip install -e .
python -m imapt --help

Quick start

Prepare a credential file (see Input format), then validate logins:

imapt validate-login -i creds.txt

Search each account's INBOX for a subject line:

imapt search --subject "invoice" -i creds.txt

Combine filters — any of them may match (logical OR, in a single login). Every filter option can also be repeated to list several alternatives:

imapt search --from alice@example.com --subject "invoice" -i creds.txt
imapt search --subject "invoice" --subject "receipt" --body "payment due" -i creds.txt

Search another folder, or add a raw IMAP expression (--raw becomes one more OR alternative and lets you use NOT, custom headers, …):

imapt search --subject "invoice" --mailbox Archive -i creds.txt
imapt search --from alice@example.com --header 'X-Mailer=Gmail' --raw 'NOT SEEN' -i creds.txt

Reuse saved filter combinations instead of retyping them every time — store a combination as a rule, group rules into rulesets, then run them:

imapt rule add invoice-hunt --from billing@example.com --subject invoice --description "all invoices"
imapt rule add unread-only --raw 'NOT SEEN' --mailbox Archive
imapt ruleset create audit2024 --description "yearly audit filter"
imapt ruleset add-rule audit2024 invoice-hunt
imapt ruleset add-rule audit2024 unread-only
imapt search --ruleset audit2024 -i creds.txt

The criteria within one rule stay AND (all must match together), but rules, rulesets and inline filters are OR-combined with each other. With --all-rulesets imapt runs one separate labelled search per stored ruleset — one login per credential, one result row per ruleset. See Saved rules and rulesets.

Read credentials from stdin instead of a file with -:

cat creds.txt | imapt validate-login -i -

Input format

Each non-empty, non-comment (#) line is one credential. The email and password may be separated by :, ,, tab, or space — whichever appears first after the domain:

user@example.com:password
user2@example.com,password2
user3@company.org p4ss word with spaces
# lines starting with # are ignored

JSON-lines are also accepted (one JSON object per line). Recognized keys (case-insensitive): email via email/user/login/username, password via password/pass/secret:

{"email": "user@example.com", "password": "secret"}

Entries are deduplicated on (email lowercased, exact password); the first casing seen is preserved. Blank lines and malformed lines are skipped silently.

Commands

Command Purpose
validate-login Check whether each credential can log in.
search Search mailboxes; combine header/body filters (--from, --to, --subject, --body, …) with OR.
set-imap-server Store an explicit IMAP server for a domain.
get-imap-server Show stored IMAP server configuration.
rule Manage stored search rules (add/list/show/delete).
ruleset Manage collections of rules (create/add-rule/remove-rule/list/show/delete).
export-rules Export all stored rules and rulesets as portable JSON.
import-rules Import rules/rulesets from an export-rules JSON file.

Run options

The two run commands (validate-login, search) share these options:

Option Default Description
-i, --input PATH (required) Credential file, or - for stdin.
-w, --workers N 16 Concurrent connections.
-t, --timeout SEC 10.0 Per-connection timeout.
--no-live off Disable the live rich display (plain lines only).

The search command additionally takes one or more filter options and/or saved rules (--rule/--ruleset) — at least one filter in total is required. Filters are combined with OR: a message counts when it matches at least one filter, and every filter option may be repeated to add more alternatives. Each filter runs as its own IMAP SEARCH over a single login per credential and the matches are unioned:

Option Description
--from TEXT Match the FROM header; repeatable (OR).
--to TEXT Match the TO header; repeatable (OR).
--cc TEXT Match the CC header; repeatable (OR).
--bcc TEXT Match the BCC header; repeatable (OR).
--subject TEXT Match the Subject header; repeatable (OR).
--body TEXT Match the message body; repeatable (OR).
--text TEXT Match headers or body (IMAP TEXT); repeatable (OR).
--header NAME=VALUE Match any header field; repeatable (OR). The value may contain =.
--raw IMAPSEARCH Free-form IMAP search expression as one more OR alternative; repeatable (advanced).
--rule NAME Also apply a stored rule; repeatable (see Saved rules and rulesets).
--ruleset NAME Apply all rules of a stored ruleset; repeatable.
--all-rulesets Run one separate search per stored ruleset (mutually exclusive with the filters above).
--mailbox NAME Folder to search; overrides any mailbox stored with rules (default INBOX).

All text filters are case-insensitive substring matches — IMAP has no */? wildcards, so --subject invoice also matches "Re: INVOICE #123". For non-ASCII folder names pass the mailbox in IMAP modified UTF-7 (e.g. Entw&APw-rfe).

Note on --raw: its value is sent to the server verbatim, so you are responsible for correct quoting/escaping. Use it for expressions the structured filters cannot express (NOT, OR, …), e.g. --raw 'NOT SEEN'.

Global options

These go before the subcommand:

Option Env var Default Description
--db PATH IMAPT_DB imapt.db SQLite database for stored endpoints.
--ca-bundle FILE IMAPT_CA_BUNDLE CA bundle for TLS verification.
--no-verify off Disable TLS certificate verification.
--allow-plaintext off Allow no-TLS on non-loopback hosts.
--window SECONDS IMAPT_WINDOW 600 Time window for failure limits and the success cache.
--account-limit N 3 Max auth failures per address in the window; 0 disables.
--server-limit N 5 Max auth failures per target host in the window; 0 disables.
--force-recheck off Ignore cached successful logins and re-test everything.

Example: keep state in a named database and trust a corporate CA:

imapt --db ~/imap-audit.db --ca-bundle /etc/ssl/corp-ca.pem search --subject "receipt" -i creds.txt

Saved rules and rulesets

A rule is a named, reusable combination of search filters (the same options search takes: --from, --subject, --header, --raw, …), optionally with a description and its own mailbox. All criteria within one rule are AND-combined — they must match together. A ruleset groups several rules; the member rules are OR-combined with each other (and with any inline filters).

imapt rule add invoice-hunt --from billing@example.com --subject invoice --description "all invoices"
imapt rule add unread-only --raw 'NOT SEEN' --mailbox Archive
imapt rule list
imapt rule show invoice-hunt
imapt rule delete invoice-hunt

imapt ruleset create audit2024 --description "yearly audit filter"
imapt ruleset add-rule audit2024 invoice-hunt
imapt ruleset remove-rule audit2024 unread-only
imapt ruleset list
imapt ruleset show audit2024
imapt ruleset delete audit2024      # removes the set, keeps the member rules

Run them with search (repeatable, freely mixable with inline filters — a rule given directly and via a ruleset is still applied only once):

imapt search --ruleset audit2024 -i creds.txt
imapt search --ruleset audit2024 --rule urgent --body deadline -i creds.txt

Combination semantics:

  • Every filter group (one inline option value, or one whole rule) becomes its own IMAP SEARCH; a message matching any group is counted once. Criteria inside one rule stay AND-ed together.
  • A rule may store a --mailbox; if all selected rules agree on one mailbox it is used automatically, --mailbox always wins, and conflicting mailboxes abort with an error telling you to pick one.
  • --all-rulesets takes no filters at all: instead of merging, imapt runs one separate search per stored ruleset — each ruleset resolves its own mailbox (conflicts within a ruleset still abort), the output labels every row with the ruleset name. It cannot be combined with --rule, --ruleset or any inline filter, and fails if no rulesets are defined.
  • Rules and rulesets live in the same SQLite database as endpoints and attempt logs (--db). Editing a rule means deleting and re-adding it — or simply importing a changed export with import-rules --overwrite (see below).

Export and import

export-rules writes all stored rules and rulesets as a portable JSON document (- = stdout by default), so you can move them to another instance or keep them under version control:

imapt export-rules -o rules.json          # ... or just `imapt export-rules` to stdout
imapt --db other.db import-rules rules.json
imapt import-rules - < rules.json         # read from stdin

Import matches by name: existing rules/rulesets are skipped unless you pass --overwrite, which updates them in place (rule id and ruleset memberships are kept). Anything new is added; ruleset members are resolved by rule name, so an import fails if a referenced rule does not exist. The whole import is atomic — if any part is invalid, nothing is written:

imapt export-rules -o rules.json
# ...edit rules.json (change criteria, add a rule)...
imapt import-rules --overwrite rules.json
# rules: 1 added, 2 updated, 0 skipped; rulesets: 0 added, 1 updated, 0 skipped

The JSON is versioned ("tool": "imapt", "kind": "rules", "version": 1); files that do not look like an export-rules document are rejected. Credentials and attempt logs are never exported — only rules and rulesets.

A complete importable file looks like this (hand-writing one works just as well; criteria are [FIELD, "pattern"] pairs ANDed within the rule, raw is an optional free-form IMAP expression, mailbox/description may be null, and ruleset members are referenced by rule name):

{
  "tool": "imapt",
  "kind": "rules",
  "version": 1,
  "exported_at": "2026-08-31T06:35:33Z",
  "rules": [
    {
      "name": "invoice-hunt",
      "description": "all invoices",
      "mailbox": "Archive",
      "raw": null,
      "criteria": [
        ["FROM", "billing@example.com"],
        ["SUBJECT", "invoice"]
      ]
    },
    {
      "name": "unread-only",
      "description": "plain raw example",
      "mailbox": null,
      "raw": "NOT SEEN",
      "criteria": []
    }
  ],
  "rulesets": [
    {
      "name": "audit2024",
      "description": "yearly audit filter",
      "rules": ["invoice-hunt", "unread-only"]
    }
  ]
}

Endpoint resolution

For each credential, imapt determines the IMAP server for its email domain in this order:

  1. Explicit database entry — a row you added with set-imap-server. Used as-is, never re-probed.
  2. Thunderbird autoconfig — fetched from the provider's well-known config URL.
  3. Candidate probing — tries imap.<domain>, mail.<domain>, and <domain> on port 993 (implicit TLS), then port 143 (STARTTLS), using the first that responds.

Endpoints discovered via autoconfig or probing are saved back to the database with source auto, so later runs reuse them without re-probing. Failures are cached too, avoiding repeated timeouts for dead domains.

Pinning a server manually

When auto-detection is wrong (or you want to force a specific host), store it explicitly:

imapt set-imap-server --domain example.com --host imap.mail.example.com --port 993

Use --no-ssl for a STARTTLS server on port 143:

imapt set-imap-server --domain example.org --host mail.example.org --port 143 --no-ssl

Inspect what is configured (omit --domain to list everything):

imapt get-imap-server --domain example.com
example.com   imaps://imap.mail.example.com:993   manual

The third column shows the source: manual or auto.

Output

On a terminal, results render as a live-updating table. When piped (or with --no-live) each result is one plain line:

ok         user@example.com [imaps://imap.example.com:993] matches=2 | Subject: invoice hello
auth_fail  bad@example.com [imaps://imap.example.com:993] | LOGIN failed
conn_fail  other@example.net [imaps://imap.example.net:993] | timed out
no_server  ghost@nowhere.invalid [-]
skipped    cached@example.com [-] | cached ok

A summary is printed at the end:

total=5 ok=1 auth_fail=1 conn_fail=1 no_server=1 skipped=1 matches=2

Status meanings: ok (login succeeded), auth_fail (rejected credentials), conn_fail (connection/TLS error), no_server (no reachable IMAP host for the domain), skipped (not attempted — already validated, or a failure limit was reached; see below).

Result caching & Fail2Ban-aware throttling

Every attempt is recorded in the database (login_attempts) with its email, password, domain, endpoint, mode, pattern, status, match count, sample, error, duration and an UTC timestamp. Two features build on this:

  • Success cache. A credential (matched on email and password) that previously logged in ok is skipped on later validate-login runs instead of hitting the server again. Pass --force-recheck to ignore the cache and test everything live. Searches (search) always connect, since results need a live mailbox query.
  • Failure throttling. To avoid tripping a server's Fail2Ban jail, imapt caps auth failures per address (--account-limit) and per target host (--server-limit) within --window seconds, counting both history and the current run. Once a limit is hit, further credentials are marked skipped rather than attempted. Limits are enforced per host (not per domain), because one mail server fronting many domains shares a single jail keyed on your source IP.

The limits are a defensive guard. For large bulk runs against your own servers, the clean approach is to whitelist your test IP instead — e.g. in /etc/fail2ban/jail.local:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

then fail2ban-client reload. That lets you test freely without bans; the throttling then simply never engages. Set --server-limit 0 / --account-limit 0 to disable the guard entirely.

Exit codes

  • 0 — run completed.
  • 2 — no credentials could be parsed from the input (or a usage error).

TLS notes

Connections use implicit TLS on port 993 or STARTTLS on port 143 by default, with certificate verification. Use --ca-bundle for private CAs, or --no-verify to skip verification (not recommended). Plaintext connections are only permitted automatically against loopback; use --allow-plaintext to permit them elsewhere.

Development

See CONTRIBUTING.md for architecture and conventions. Run the test suite with:

uv run python -m unittest discover -s tests

Requirements

Requires Python: >=3.10
Details
PyPI
2026-09-02 04:11:17 +02:00
3
88 KiB
Assets (2)
Versions (2) View all
0.2.0 2026-09-02
0.1.0 2026-09-02