Watch IMAP mailboxes and run commands when a message matches a keyword rule
npm install -g @tomasko/mails
# pnpm add -g @tomasko/mails
# yarn global add @tomasko/mails
# bun add -g @tomasko/mails
# npx @tomasko/mails version (run without installing)mkdir -p ~/.config/mails
export MAILS_CONFIG=~/.config/mails/mails.toml
mails initmoon install kupas/mails/cmd/mailscurl -LsSf https://raw.githubusercontent.com/tomasky/mails/master/install.sh | bashcurl -L -o mails \
https://github.com/tomasky/mails/releases/latest/download/mails-linux-x86_64
chmod +x mails
./mails version| Platform | Release asset |
|---|---|
| Linux x86-64 | mails-linux-x86_64 (static, musl) |
| Linux arm64 | mails-linux-aarch64 (static, musl) |
| macOS Apple silicon | mails-darwin-aarch64 |
curl -L -O https://github.com/tomasky/mails/releases/latest/download/mails-linux-x86_64
curl -L -O https://github.com/tomasky/mails/releases/latest/download/mails-linux-x86_64.sha256
sha256sum -c mails-linux-x86_64.sha256git clone https://github.com/tomasky/mails && cd mails
moon build --target native --release
mkdir -p ~/.local/bin
install -m 755 _build/native/release/build/cmd/mails/mails.exe ~/.local/bin/mailsmails init # writes mails.toml next to the binary and prints the path
$EDITOR /path/printed/by/mails/init/mails.toml
mails keygen # creates .env with a fresh MAILS_KEY
mails encrypt --account work --write
# prompts for the IMAP password, seals it, and edits mails.toml in place
mails check # verify config, key and logins
mails watch # run in the foreground; use a supervisor in production[general]
startup_sync = false # process existing mail on startup
idle_timeout = "29m" # re-enter IDLE this often
reconnect_delay = "30s" # first reconnect backoff, doubled up to 5m
max_body_bytes = 262144 # fetch at most this much of each body
command_timeout = "30s" # default rule timeout
body_chars = 4000 # default {{body}} truncation
env_file = ".env" # relative to this file
key_env = "MAILS_KEY" # variable holding the encryption key
[[rules]]
name = "invoice"
keywords = ["invoice", "payment due"]
fields = ["subject", "body"]
match = "any" # "any" (default) or "all"
case_sensitive = false # default false
regex = false # treat keywords as MoonBit regexes
command = 'notify-send "{{subject}}" "{{from}}"'
[[rules]]
name = "from-boss"
keyword = "boss@example.com"
fields = ["from"]
exec = ["/usr/local/bin/on-boss-mail", "--subject", "{{subject}}"]
[accounts.work]
host = "imap.example.com"
port = 993
username = "me@example.com"
password = "enc:v1:..." # written by `mails encrypt`
mailboxes = ["INBOX", "Work"]
# tls = true
# tls_verify = true
# startup_sync = true
# idle_timeout = "10m"
# reconnect_delay = "1m"
# max_body_bytes = 1048576| Key | Default | Meaning |
|---|---|---|
| name | rule-N | Name used in logs and {{rule}} |
| keywords | required | Array of keywords; each must appear in one of fields |
| keyword | – | Single-keyword shorthand |
| fields | ["subject", "body"] | Any of subject, body, from, to |
| match | "any" | any: one keyword is enough. all: every keyword must match |
| case_sensitive | false | ASCII/Unicode lowercase comparison |
| regex | false | Compile keywords as regexes ((?i:...) when case-insensitive) |
| command | – | Shell string, run with sh -c |
| exec | – | argv array; the first element is the program |
| timeout | command_timeout | Kill the command after this duration |
| workdir | – | Working directory for the command |
| body_chars | general.body_chars | Truncate {{body}} and MAILS_BODY |
| accounts | all | Restrict the rule to these account names |
| mailboxes | all | Restrict the rule to these mailboxes |
| Placeholder | Value |
|---|---|
| {{account}}, {{mailbox}}, {{uid}} | Where the message lives |
| {{subject}}, {{from}}, {{to}}, {{date}}, {{message_id}} | Decoded headers |
| {{body}} | Readable text body, truncated to body_chars |
| {{rule}}, {{keywords}}, {{fields}} | Which rule fired and why |
| {{*_q}} | The same value single-quoted for a POSIX shell |
command = 'logger -t mails "$MAILS_SUBJECT"'mails keygen # writes MAILS_KEY to .env (mode 0600)
mails encrypt --account work # prints the envelope to paste
mails encrypt --account work --write # edits mails.toml in place
mails encrypt --account work --new-key --write # rotate the key firstmails <command> [options]
watch follow mailboxes with IMAP IDLE
sync process existing matching mail once, then exit
check verify config, key, logins and mailbox counts
encrypt seal an account password with the key in .env
keygen create or rotate the encryption key in .env
rules list configured rules
env show .env with secret values masked
init write a commented mails.toml next to the binary
version print the version
help print usage| Option | Effect |
|---|---|
| -c, --config PATH | Configuration file (default: mails.toml next to the binary) |
| -a, --account NAME | Operate on one account only |
| --once | For watch: process existing mail, then exit (same as sync) |
| --write | For encrypt: update mails.toml in place |
| --new-key | For encrypt: rotate the key before sealing |
| -v, --verbose | Debug logging |
| -q, --quiet | Warnings and errors only |
| Variable | Read by | Effect |
|---|---|---|
| MAILS_CONFIG | mails | Config path when --config is not passed |
| MAILS_KEY | mails | Encryption key fallback when .env does not define general.key_env |
| MAILS_BINARY, MAILS_CACHE_DIR, … | npm launcher | Control the downloaded binary; see npmjs/README.md |
| Status | Meaning |
|---|---|
| 0 | The command did what it was asked. For sync / watch --once, every mailbox synced |
| 1 | Bad arguments or config, a missing or wrong key, a failed login, or at least one mailbox failed |
# ~/.config/systemd/user/mails.service
[Unit]
Description=mails IMAP watcher
After=network-online.target
[Service]
ExecStart=%h/.local/bin/mails watch --config %h/.local/bin/mails.toml
Restart=always
RestartSec=30
[Install]
WantedBy=default.targetsystemctl --user daemon-reload
systemctl --user enable --now mails
journalctl --user -u mails -fmoon build --target native --release
# binary: _build/native/release/build/cmd/mails/mails.exemoon build --target native
./_build/native/debug/build/cmd/mails/mails.exe versionmoon test --target native
moon check --target native --deny-warn
moon fmt --checksudo apt-get install musl-tools
./scripts/build-linux-static.sh dist/mails-linux-x86_64pub suberror MailsError {
MailsError(String)
}pub struct ActionContext {
account : String
mailbox : String
message : MailMessage
rule : Rule
keywords : Array[String]
}pub struct Defaults {
startup_sync : Bool
idle_timeout_ms : Int
reconnect_ms : Int
max_body_bytes : Int
command_timeout_ms : Int
body_chars : Int
}pub struct FetchedMessage {
uid : String
size : Int
header : Bytes
body : Bytes
}pub struct ImapClient {
stream : ImapStream
host : String
tag_counter : Int
capabilities : Array[String]
}async fn ImapClient::connect(host : String, port : Int, use_tls : Bool, verify_tls : Bool, username : String, password : String) -> ImapClientasync fn ImapClient::fetch_message(client : ImapClient, uid : String, max_body_bytes : Int) -> FetchedMessage?pub struct MailMessage {
uid : String
mailbox : String
subject : String
from : String
to : String
date : String
message_id : String
body : String
raw_bytes : Int
}pub enum MatchField {
Subject
Body
From
To
}fn build_message(mailbox : String, uid : String, raw_size : Int, header : Bytes, body : Bytes) -> MailMessagefn collapse_whitespace(text : String) -> Stringfn decode_charset(data : Bytes, charset : String) -> Stringfn decode_encoded_words(value : String) -> Stringfn decode_modified_utf7(name : String) -> Stringfn error_message(err : Error) -> Stringfn imap_quote(text : String) -> Stringasync fn main_cli() -> Boolfn match_rules(rules : Array[Rule], account : String, mailbox : String, message : MailMessage) -> Array[RuleMatch]fn message_body_text(raw_body : Bytes) -> Stringfn needs_key(configured : String) -> Boolfn parse_duration_ms(text : String) -> Int?async fn run_action(account : String, mailbox : String, message : MailMessage, hit : RuleMatch) -> ActionOutcomefn sample_config() -> Stringfn shell_quote(text : String) -> Stringfn truncate_chars(text : String, max_chars : Int) -> Stringfn update_account_password(source : String, account : String, value : String) -> String raise MailsErrorInstall
Download zipWatch IMAP mailboxes and run commands when a message matches a keyword rule