CLI for searching, managing, drafting, and bulk-operating on emails via IMAP.
cargo install slashmailDownload a prebuilt binary from Releases, extract it, and place it on your PATH.
Requires Rust and a C compiler (for OpenSSL bindings).
git clone https://github.com/mwmdev/slashmail.git
cd slashmail
cargo build --release
cp target/release/slashmail ~/.local/bin/ # or anywhere on your PATHIf OpenSSL cannot be discovered on your system, build with
cargo build --release --features vendored-openssl.
| OS | Prerequisites |
|---|---|
| macOS | Xcode Command Line Tools (xcode-select --install) |
| Debian/Ubuntu | apt install build-essential pkg-config libssl-dev |
| Fedora/RHEL | dnf install gcc pkg-config openssl-devel |
| Arch | pacman -S base-devel openssl |
| NixOS | nix-shell (uses included shell.nix) |
| Windows | Install Rust via rustup, uses vendored OpenSSL |
slashmail [OPTIONS] <COMMAND>
Commands:
draft Save a new unsent email draft
reply Save an unsent reply draft for one message UID
attachments List or save attachments from one message UID
search Search messages by criteria
read Display the content of matching messages
delete Search + delete matching messages (move to Trash)
move Search + move matching messages to a folder
export Search + export matching messages as .eml files
mark Search + set/unset flags on matching messages
count Count matching messages (no FETCH)
quota Show mailbox quota usage
status Show per-folder message statistics
--host <HOST> IMAP host [default: 127.0.0.1]
--port <PORT> IMAP port [default: 1143 plain, 993 TLS]
--tls Use TLS (required for every non-loopback IMAP host)
-u, --user <USER> IMAP username (or SLASHMAIL_USER env)
--account <NAME> Use a named account from config
--all-accounts Query all configured accounts (read-only commands only)
For direct/legacy connections, the password is read from SLASHMAIL_PASS or prompted interactively. Named accounts use the environment variable named by their pass_env setting.
When a config file is loaded, slashmail automatically loads .env from the same directory. Values already present in the process environment take precedence. A missing .env is ignored; an unreadable or malformed one is an error. Because .env contains plaintext secrets, keep it out of version control and readable only by your user account.
draft and reply are different because stdin is reserved for the message body: they never prompt for a password. Direct/legacy use requires a nonempty SLASHMAIL_PASS. A named account must configure pass_env, and that variable must be nonempty in the process environment or adjacent .env. Slashmail checks credentials before reading stdin, so a missing password cannot consume a piped draft body.
Connection options are global and can appear before or after the subcommand.
Settings can be stored in a config file to avoid repeating connection options:
| OS | Path |
|---|---|
| Linux | ~/.config/slashmail/config.toml |
| macOS | ~/Library/Application Support/slashmail/config.toml |
| Windows | %APPDATA%\slashmail\config.toml |
Single-account config.toml:
host = "imap.gmail.com"
port = 993
tls = true
user = "user@gmail.com"
sender = "User Example <user@gmail.com>"
drafts_folder = "[Gmail]/Drafts"
trash_folder = "[Gmail]/Trash"
default_folder = "INBOX"All single-account fields are optional. CLI arguments and environment variables take precedence over these top-level config values.
Multi-account config.toml:
default_account = "personal"
[[accounts]]
name = "personal"
host = "imap.gmail.com"
port = 993
tls = true
user = "user@gmail.com"
pass_env = "SLASHMAIL_PERSONAL_PASS"
sender = "Personal User <user@gmail.com>"
drafts_folder = "[Gmail]/Drafts"
trash_folder = "[Gmail]/Trash"
default_folder = "INBOX"
[[accounts]]
name = "work"
host = "imap.fastmail.com"
port = 993
tls = true
user = "user@company.com"
pass_env = "SLASHMAIL_WORK_PASS"
sender = "Work User <user@company.com>"
drafts_folder = "Drafts"
default_folder = "INBOX"Create .env beside config.toml with the variables named by each account's pass_env:
SLASHMAIL_PERSONAL_PASS=your-personal-password
SLASHMAIL_WORK_PASS=your-work-passwordWhen [[accounts]] is configured, slashmail uses default_account by default, or the first account if default_account is omitted. Use --account <NAME> to select one account, or --all-accounts to aggregate read-only commands across every account.
--all-accounts is supported for search, read (without --uid), count, status, and quota. Mutating commands (delete, move, mark), export, drafts, replies, and received-attachment inspection require a single account.
Use --config <PATH> to specify an alternative config file location.
sender and drafts_folder are optional at both the top level and inside an [[accounts]] entry. An account value takes precedence over the top-level value. Draft composition falls back to user only when it is a valid email mailbox; configure sender when the IMAP login is not an email address.
The draft destination is resolved in this order: command --drafts-folder, the selected account's resolved drafts_folder (account value, then top-level value), then exactly one selectable server mailbox marked \Drafts. Slashmail fails without saving if the chosen override is invalid or server discovery finds zero or multiple valid Drafts mailboxes.
draft and reply save unsent messages with the IMAP \Draft flag; they do
not send mail. The body is read from stdin and is plain text unless --html
is used. Repeat --to, --cc, --bcc, or --attach to add multiple
recipients or local files.
# Create a draft with an attachment
printf '%s\n' 'Please review the attached proposal.' |
slashmail draft --account work \
--to client@example.com \
--subject "Proposal" \
--attach './documents/client proposal.pdf'
# Reply to UID 1842 without quoting the original message
printf '%s\n' 'Thanks, this looks good to me.' |
slashmail reply --account work --no-quote 1842
# Create an HTML draft
printf '%s\n' '<p>Please review the <strong>proposal</strong>.</p>' |
slashmail draft --html --to client@example.com --subject "Proposal"Replies use reply-all behavior, exclude the configured sender, preserve
available thread metadata, and quote the original by default. Use --folder
to select the source folder and --no-quote to omit the quote. The source
message remains unchanged.
Each --attach value must name a local regular file. Globs, directories, and
URLs are not supported, and attachments from the original message are not
copied into replies. Files are loaded into memory while the MIME message is
built, so large attachments may be limited by available memory or the mailbox
provider.
On success, slashmail prints the account, Drafts folder, UID, recipients, and
subject. Add --json to print the same receipt, plus the draft's
message_id, as one JSON object. This receipt may contain Bcc addresses, so
avoid copying it into public logs. If the APPEND outcome is reported as
unknown, inspect the Drafts folder before retrying to avoid creating a
duplicate.
attachments <UID> lists attachments from one message without marking it as
seen. The output includes MIME part IDs, filenames, content types, and decoded
sizes; add --json for machine-readable output.
# List attachments
slashmail attachments --account work 1842
# Save every attachment
slashmail attachments --account work --save \
--output-dir './received files' 1842
# Save selected MIME parts
slashmail attachments --account work --save \
--part 2.1 --part 3 --output-dir './received files' 1842Saving aborts before writing anything if a destination already exists, and
stops rather than overwrite an earlier part when two names resolve to the same
file (case- or accent-insensitive filesystems). Add
--force to replace existing files. Filenames are sanitized and kept inside
the output directory. Only parts declared as attachments are exposed;
inline/CID parts and attachments nested inside another attached message are
not extracted.
read --uid <UID> displays exactly that message from the selected folder
instead of the newest filter match. The folder is --folder, or else the
account's configured default_folder (INBOX if unset); UIDs are only unique
within one folder, so pass --folder explicitly when reusing a UID from
search. It fails when no message with that UID exists and cannot be combined
with --all-folders or --all-accounts. Like every read, it does not mark the
message as seen.
slashmail read --account work --folder INBOX --uid 1842
slashmail read --account work --folder INBOX --uid 1842 --jsonread --json prints an array with one object per message: uid, folder,
message_id, in_reply_to, references, full from/to/cc/date/
subject headers, timestamp, seen/answered/flagged, the decoded text
body, and attachments (MIME part, filename, content_type, size).
Pass an attachment's part to attachments --save --part.
Each search --json row contains uid, folder (set with --all-folders),
from, subject, date, timestamp, size, the thread identifiers
message_id (or null), in_reply_to, and references (arrays of
angle-bracketed IDs), and the seen, answered, and flagged booleans.
Rows include account when a named account is used. from, subject, and
date are the full decoded values; only the terminal table shortens them.
# Messages you have not answered yet
slashmail search --since 7d --json |
jq '.[] | select(.answered | not) | {uid, subject, message_id}'Search, read, count, and bulk message commands share these filter options:
-f, --folder <FOLDER> Folder to search [default: INBOX]
--all-folders Search across all folders (excludes Trash, Junk/Spam, All Mail)
--subject <TEXT> Subject contains
--from <TEXT> From address contains
--to <TEXT> To address contains
--cc <TEXT> CC address contains
--body <TEXT> Message body contains
--text <TEXT> Headers or body contains
--seen Only read messages
--unseen Only unread messages
--since <DATE> Messages since date (YYYY-MM-DD or 7d, 2w, 3m, 1y)
--before <DATE> Messages before date (YYYY-MM-DD or 7d, 2w, 3m, 1y)
--larger <SIZE> Messages larger than N bytes (supports K/M suffix)
--smaller <SIZE> Messages smaller than N bytes (supports K/M suffix)
--flagged Only flagged/starred messages
--unflagged Only unflagged messages
--answered Only replied-to messages
--draft Only draft messages
-n, --limit <N> Limit number of results
All filter criteria are AND'd together. Omitting all criteria matches all messages. Text filters (--subject, --from, --to, --cc, --body, --text) must not be empty, so an unset shell variable cannot turn a filter into "match everything". --folder and --all-folders cannot be combined.
--all-folders skips mailboxes the server marks \All, \Trash, or \Junk (for example Deleted Items and Junk Email), plus folders named Trash, Spam, Junk, or All Mail for servers without those markers. delete and move also never search their destination folder, and naming the destination as the only source folder is an error.
Commands that modify messages (delete, move, mark) support:
--yes Skip confirmation prompt
--dry-run Show what would happen without acting
delete also supports --trash-folder <NAME> (default: Trash) for servers that use a different name (e.g. Deleted Items, [Gmail]/Trash).
move requires --dest <FOLDER>; --to stays the recipient filter, as in every other command.
delete and move require the server to advertise MOVE or UIDPLUS. Without MOVE, messages are copied, flagged \Deleted, and removed with UID EXPUNGE of exactly those UIDs; other messages already flagged \Deleted are never expunged. This fallback is not atomic: if a step fails, slashmail stops and reports it without retrying, including how many messages were already moved or updated. Every mutating command and export/read refuse to act if the folder's UIDVALIDITY changed since the search. Immediately before delete, move, and mark act, slashmail asks the server which searched messages still exist; their receipts count only those and report any another client removed in the meantime.
export supports --yes, --force (replace existing files), and -o, --output-dir. Files are named <folder>_<uid>.eml, where the folder name is percent-encoded: ASCII letters, digits, and - are kept and every other byte becomes %XX (so on Linux and macOS Work/Projects is Work%2FProjects_1.eml and Work_Projects is Work%5FProjects_1.eml). On Windows, lowercase letters are also encoded so folders differing only by case stay distinct (Work/P is W%6F%72%6B%2FP_1.eml). Without --force, an existing file is skipped only when it already holds the same message (identical bytes or the same Message-ID). UIDs restart when a mailbox is recreated or migrated, so an existing file holding a different message is left unchanged and reported as an error after the other messages are exported; use --force or a new output directory. --force replaces only a regular file or symlink entry and never follows symlinks. New exports and saved attachments are created owner-only (0600) on Unix.
mark takes one or more actions: --read, --unread, --set-flagged, --clear-flagged.
Search terms containing non-ASCII text are sent as UTF-8 literals and require the server to advertise LITERAL+; otherwise the search fails before any mailbox is searched.
# Search INBOX (all messages, newest first)
slashmail search -u user@example.com
# Search with filters
slashmail search -u user@example.com --from "newsletter" --since 2025-01-01
slashmail search -u user@example.com --subject "invoice" --larger 1M
# Relative dates: last 7 days, 2 weeks, 3 months, 1 year
slashmail search -u user@example.com --since 7d
slashmail search -u user@example.com --since 3m --before 1m
# Show only the 10 most recent matches
slashmail search -u user@example.com --from "alerts" -n 10
# Filter by recipient or CC
slashmail search -u user@example.com --to "team@company.com"
slashmail search -u user@example.com --cc "me@example.com"
# Show only unread messages
slashmail search -u user@example.com --unseen --since 7d
# Search message body content
slashmail search -u user@example.com --body "invoice attached"
# Search everywhere (headers + body)
slashmail search -u user@example.com --text "quarterly report"
# JSON output for scripting
slashmail search -u user@example.com --from "alerts" --json | jq '.[].subject'
slashmail count -u user@example.com --json
# Search across all folders
slashmail search -u user@example.com --all-folders --from "noreply"
# Search across all configured accounts
slashmail search --all-accounts --from "newsletter"
slashmail read --all-accounts --subject "invoice" -n 3
# Use one named account from config
slashmail count --account work --unseen
# Delete with interactive confirmation
slashmail delete -u user@example.com --from "spam@example.com"
# Batch delete (no prompt)
slashmail delete -u user@example.com --subject "unsubscribe" --yes
# Preview what would be deleted
slashmail delete -u user@example.com --from "old-list" --dry-run
# Move messages to a folder
slashmail move -u user@example.com --from "receipts" --dest Archive
# Export messages as .eml files
slashmail export -u user@example.com --subject "contract" -o ./backup
# Mark messages as read
slashmail mark -u user@example.com --from "notifications" --read
# Flag important messages
slashmail mark -u user@example.com --subject "urgent" --set-flagged
# Count matching messages (fast, no FETCH)
slashmail count -u user@example.com --from "newsletter"
# Show folder statistics
slashmail status -u user@example.com
# Show mailbox quota
slashmail quota -u user@example.com
# Use with a remote IMAP server (Gmail, Fastmail, etc.)
slashmail search --tls --host imap.gmail.com -u user@gmail.com
# Use env vars to avoid typing credentials
export SLASHMAIL_USER=user@example.com
export SLASHMAIL_PASS=app-password
slashmail status# Bash
slashmail completions bash > ~/.local/share/bash-completion/completions/slashmail
# Zsh
slashmail completions zsh > ~/.zfunc/_slashmail
# Fish
slashmail completions fish > ~/.config/fish/completions/slashmail.fishslashmail includes a skill file (skills/slashmail/SKILL.md) that teaches AI agents how to manage your email through natural language.
Claude Code — copy the skill into your skills directory:
mkdir -p ~/.claude/skills/slashmail
cp skills/slashmail/SKILL.md ~/.claude/skills/slashmail/Codex — copy the skill into the shared agent skills directory:
mkdir -p ~/.agents/skills/slashmail
cp skills/slashmail/SKILL.md ~/.agents/skills/slashmail/Other agents — paste the contents of skills/slashmail/SKILL.md into your agent's system prompt or tool definitions.
Once installed, prompts like these just work:
> Check my latest emails
> Read the last email from Sarah
> Find emails about the quarterly report
> How many unread messages do I have?
> Show me large emails over 5MB from the last month
> Delete all newsletters from noreply@example.com older than 3 months
> Move flagged emails from last week to the Archive folder
> Export all invoices from 2025 to a backup folder
> Search my sent folder for emails to the finance team
> Draft a plain-text email to Sarah with subject "Project update"
> Save an HTML reply to message UID 1842 without quoting the original
Destructive operations always dry-run first and ask for confirmation.
- Gmail (via
--tls --host imap.gmail.com) - Fastmail (via
--tls --host imap.fastmail.com) - Dovecot
- Any standard IMAP4rev1 server
- All filtering runs server-side via IMAP SEARCH
- Uses IMAP SORT extension (RFC 5256) when available; falls back to client-side sort
- With SORT,
--limittruncates results before fetching (fewer bytes over the wire) search,delete,move,mark,countonly fetch headers and size -- never full messagesexportfetches full message bodies viaBODY.PEEK[]- Uses
BODY.PEEKto avoid marking messages as read, and opens folders read-only (EXAMINE) forsearch,read,count,export, and--dry-run, so they do not clear the\Recentflag - UID sets are compressed into ranges and chunked to stay within IMAP command length limits
- Message content, headers, folder names, and server errors are rendered inert in the terminal: escape sequences and control characters are removed, and invisible formatting characters (such as zero-width spaces) are shown as spaces in names and dropped from bodies, so look-alike names stay distinguishable.
--jsonoutput and exported.emlfiles keep the original data. - The password buffer slashmail reads is zeroed after login. Copies held by the process environment,
.envloading, or the IMAP library's LOGIN command are not guaranteed to be wiped.
0— Success1— Error (connection failure, invalid credentials, refused operation, etc.)2— Invalid command-line usage
All errors print to stderr. Combine --yes with cron or scripts for unattended operation.
- Verify host and port: ProtonMail Bridge uses
127.0.0.1:1143, Gmail usesimap.gmail.com:993 --tls - Check that the IMAP server is running and the port is not blocked by a firewall
- Gmail and Outlook require App Passwords, not your account password
- ProtonMail Bridge: use the bridge-generated password, not your ProtonMail account password
- Fastmail: use an app-specific password from Settings → Privacy & Security
- Run
slashmail statusto list all available folders and their names - Folder names are case-sensitive on most IMAP servers
- Gmail uses
[Gmail]/Trash,[Gmail]/All Mail, etc. — use--trash-folderwithdeleteif needed - Exchange/Outlook uses
Deleted Itemsinstead ofTrash
--tlsis required for all non-loopback IMAP hosts; plaintext is only allowed forlocalhostand loopback addresses (for example ProtonMail Bridge)- If you get certificate errors, ensure your system CA certificates are up to date