imapt (0.2.0)
Installation
pip install --index-url imaptAbout 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). Output is
plain, greppable lines: stdout carries only the result data, details go to
stderr.
- Pure standard library for the protocol work (
imaplib,sqlite3,ssl). - One runtime dependency: [
click]. - 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
With uv (recommended)
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. |
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,--mailboxalways wins, and conflicting mailboxes abort with an error telling you to pick one. --all-rulesetstakes no filters at all: instead of merging,imaptruns 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,--rulesetor 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 withimport-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:
- Explicit database entry — a row you added with
set-imap-server. Used as-is, never re-probed. - Thunderbird autoconfig — fetched from the provider's well-known config URL.
- Candidate probing — tries
imap.<domain>,mail.<domain>, and<domain>on port993(implicit TLS), then port143(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
Every result is one plain line, printed to stderr:
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
stdout stays free of noise and carries only the result data, so you can pipe
or redirect it directly: validate-login prints just a summary line at the
end, search prints only the rows that actually matched (its summary goes to
stderr too):
total=5 ok=1 auth_fail=1 conn_fail=1 no_server=1 skipped=1 matches=2
Every attempt is also recorded in the database, independently of the output.
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
okis skipped on latervalidate-loginruns instead of hitting the server again. Pass--force-recheckto 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,
imaptcaps auth failures per address (--account-limit) and per target host (--server-limit) within--windowseconds, counting both history and the current run. Once a limit is hit, further credentials are markedskippedrather 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