mail - WireCatLabs/cli-memo GitHub Wiki
This page helps you bring your email into Memo, so mail sits beside your notes and messages in search, in a person's context and in the sources your agent reads. Memo reads mail through Himalaya, a mail program you set up first. Your mailbox is opened read-only: Memo never sends, moves or deletes mail, and does not mark it as read.
Set up your mail account in Himalaya. For Gmail, use an IMAP account with a Gmail app password (a separate password for one program, which you can revoke at any time). Check that Himalaya lists your mail before you continue.
Help me set up Himalaya for my Gmail account with an app password, and check that it lists my inbox.
Then add the account to Memo's config and import the last 30 days of mail.
Name the account in Memo's config file, ~/.config/cli-memo/config.json.
name is the account's name in Himalaya; address is your email address:
{ "mail": { "accounts": [{ "name": "work", "address": "[email protected]" }] } }Gmail is the default. For another provider, add "mode": "imap" and the folders to read:
{ "mail": { "accounts": [{ "name": "home", "address": "[email protected]", "mode": "imap", "folders": ["INBOX", "Archive"] }] } }memo sync --account work --since-time 2026-09-01work is the account's name from the config, or use its address. Without --account, Memo reads every account in the
config and names each one in its report; it lists the accounts it knows when a name is wrong.
- Without
--since-time, Memo reads the last 30 days. It takes a date, an ISO 8601 time or30dago. - Each run reads up to 200 new emails per account (
--limitchanges this). Run it again and it continues where it stopped: it lists only the mail no earlier run finished, so a large mailbox is not listed again. - A whole listing marks mail deleted at the source and refreshes labels and read state. It runs at least once a
day, after any change to your rules, and with
--rescan; a window under 5,000 emails is listed whole every run. -
memo syncand the timer read every account in the config.
See how far an import is, without asking the mailbox:
memo mail status workThe first line counts the widest window ever imported, such as everything since 2004, and says "complete" only when all of it is stored. A narrower later run, like the timer's 30 days, is shown on its own line as the last run. It never counts as the whole mailbox.
A large mailbox is mostly newsletters and notifications. Look before a first large import:
memo mail inspect workIt reads headers only (about 1 KB an email) and groups mail by sender and mailing list: how many
emails, how much space, how much is unread or bulk mail, the years, and whether you ever wrote to the
sender. It suggests skip for groups of at least 20 emails, at least 80% bulk mail, that you never wrote to.
Each group also gets a score for how much it looks like spam, a scam or a robot, with the signals that make it up. Gmail already filters most spam, so a high score usually means a spoofed sender or a robot that no rule covers yet:
| Signal | Adds | From |
|---|---|---|
spam_folder |
4 | Most of the group's mail is in Gmail's Spam |
dmarc_fail |
3 | Gmail's own DMARC check failed when the mail arrived (Authentication-Results: mx.google.com) |
brand_spoof |
3 | A brand in the display name, such as PayPal or Apple, sent from another domain |
young_domain |
3 | The sender's domain was under 180 days old when the first email came |
reply_to_elsewhere |
2 | Replies go to another domain than the sender's |
never_opened |
2 | You opened none of the group's mail |
dkim_not_passed |
2 | Gmail's DKIM check did not pass on most of the group's mail |
disposable_domain |
2 | A throwaway-mail domain |
bulk_never_replied |
1 | At least 80% bulk mail, and you never wrote to the sender |
replied |
−5 | You wrote to the sender |
young_domain needs each domain's registration date. memo asks for it only when you add --domain-age:
memo mail inspect work --domain-ageIt asks the registry of each shown group's sender domain through RDAP, using IANA's list of registries, one
domain a second, and keeps the answer on this computer for good. Each question tells that registry the
domain's name, so it learns that someone looks at mail from it. Big mailbox providers such as gmail.com and
senders you wrote to are never asked. Most country domains, such as .ru, .de, .es and .io, have no RDAP:
they stay unknown and add nothing. Without --domain-age, only dates already kept are used.
You decide:
memo mail rules add work --domain shop.example --do envelope --reason 'newsletters'
memo mail rules add work --domain spam.example --do skip
memo mail rules add work --sender [email protected] --do keep
memo mail rules list workThe basic --do actions are listed below; protection and tags follow in the next section:
-
skip:memo syncnever reads or stores the email, and counts it as skipped. -
envelope:memo syncstores the sender, date, size, labels and the Gmail id, but not the text or attachments. The subject is kept in the email's metadata, so the email is never a search hit.memo mail rules listshows how many emails each envelope rule recorded and their size, so you can see what a cleanup would remove. An envelope costs no more from Gmail than a skip: its fields come with the listing. -
keep: imports the email even when a skip or envelope rule matches it.
A keep wins over an envelope, and an envelope wins over a skip. When you remove an envelope rule, or change it to keep, the next sync reads those emails in full. Mail stored in full before a rule stays as it is.
Besides --sender, --domain and --list-id, a rule can take a --filter query and a --kind. Every condition
given must hold:
In --filter
|
Matches mail |
|---|---|
from:<address>, from:<domain>
|
from this one sender, or from this domain and its subdomains |
from:<word> |
from a sender whose address or display name contains the word, any case |
to:<address> |
sent to this address, in To or Cc |
subject:<word>, or a bare word |
with every such word in the subject, any case |
older:<n>d |
received more than n days ago |
larger:<size> |
bigger than 5MB, 300KB or a number of bytes at the source |
has:attachment |
sent as multipart/mixed, the way mail with attachments is: a guess from the header |
label:<name> |
carrying this Gmail label, such as label:Promotions or label:"Two words"
|
is:unread |
never opened |
is:bulk |
sent to many: List-Id, List-Unsubscribe or Precedence: bulk
|
score:<n> |
from a sender whose score (above) is at least n |
--do takes one action and any number of tags, comma-separated: skip, envelope, keep or protect, plus
tag:<name>.
memo mail rules add work --filter 'label:Promotions older:365d' --do envelope
memo mail rules add work --domain shop.example --do keep,tag:shopping
memo mail rules add work --domain tax.gov.example --do protect
memo mail rules list work --filter is:protected-
tag:<name>puts a memo tag on every email the rule matches, whichever rule decides the action. A sync tags the mail it stores or records;memo mail rules applytags mail stored earlier. A tag the store refuses as too close to an existing label is put on as that label, and the rule is updated to it (tags_reusedin--json). A tag refused with nothing to reuse is skipped, the mail is still stored, and the rule lists it astags skipped(tags_skipped). A refused tag never stops the sync. -
protectkeeps the mail whole and out of every cleanup, whatever other rules or kinds say. A protected rule wins over all others and always stores in full, somemo mail rules applynever cuts its mail and a later mailbox cleanup must refuse it. Use it for government, tax and bank mail.memo mail rules listshows protected rules first, asPROTECTED. -
score:and--kindjudge a sender by its mail in the same listing: a sync that lists only new mail judges the sender from that mail.
--kind names a built-in kind of mail, matched by who sends it and how, not by your own senders:
| Kind | Matches |
|---|---|
social |
notifications from social networks |
newsletters |
mailing lists with List-Id and List-Unsubscribe, and newsletter services |
developer |
code hosting, CI, package and error-tracker notifications |
shopping |
shops, and what Gmail files under Promotions |
alerts |
monitoring and uptime alerts |
memo mail inspect ends with how much of your mail each kind would match. Nothing applies until you add a rule:
memo mail rules add work --kind social --do envelopeMail from someone you wrote to never matches a kind, and any explicit rule wins over a --kind rule.
A Google Takeout export of Mail holds your Gmail filters in Mail/User settings/Filters.json. memo proposes a
rule for each filter it can express:
memo mail rules import-filters work Filters.json # shows the proposed rules and the skipped filters
memo mail rules import-filters work Filters.json --yes # adds themEach Gmail label a filter adds is matched to memo's labels first: the same label in other capitals is reused,
and a close one is suggested in its place; the proposal lists each mapping as label … → ….
A filter that skips the inbox, marks read or deletes becomes an envelope rule; one that only adds a label becomes
a keep rule with that label as a tag. A filter naming several senders or recipients (from:a from:b,
from:(a OR b)) becomes one rule each, since one email cannot come from both. Words in a filter become subject
words: Gmail searched the whole email, memo matches the subject, and the rule's reason says so. A filter that
excludes words or uses a field memo does not match (list:, size:) is skipped and listed. Takeout's
{"filter": [{query, labelsToAdd, labelsToRemove}]}, Gmail's API shape (criteria and action) and the
settings export's flat properties (from, shouldArchive, label) are all read; a label given only by its Gmail
id is tagged with that id.
Mail stored in full before an envelope rule existed stays whole until you cut it:
memo mail rules apply work # says how many emails and how much text each rule would cut
memo mail rules apply work --yes # cuts them to envelopesIt reads the account's headers from the mailbox, as memo sync does, because mail stored before memo kept
List-Id lacks it. Each matching email loses its text, attachments and search entries and keeps its sender, date,
size, labels and subject in metadata. Mail only a skip rule matches stays as stored. Nothing changes in the
mailbox, and changing the rule to keep reads the emails in full again on the next sync.
Rules live in the local store, beside the mail they decide, so memo mail rules list counts each rule's
emails in one query. Each envelope email remembers the rule that recorded it. To review rules, keep a copy or
move them to another computer:
memo mail rules export work > rules.json
memo mail rules import work rules.json # a rule with the same sender, domain or list is replacedBefore 0.32.0 rules were kept in config.json. The first memo command that reads rules moves them into the
store, checks they all arrived, and only then drops them from the config. The old config stays beside it as
config.json.<time>.before-rules-in-store, readable only by you, and memo says so on stderr once. A keep rule
whose reason says "never remove" becomes protected: no cleanup may cut or delete what it matches.
Gmail limits how much IMAP can download a day, so years of old mail are faster from a Google Takeout export of Mail. Add the file as a second account with the same address, so its emails and the Gmail account's are one set:
{ "name": "archive", "address": "[email protected]", "mode": "mbox", "path": "~/Takeout/Mail/All mail Including Spam and Trash.mbox" }memo sync --account archive --since-time 2000-01-01 --limit 5000The file is only read when you name the account. An incomplete import scans it again; a completed
snapshot is skipped until its size or modification time changes. --rescan forces another scan.
It only adds mail: nothing
missing from the file is deleted from the store, and Spam and Trash are left out.
memo search mail 'invoice'
memo search mail 'from:kai subject:budget'
memo search mail 'mailbox:starred' --account work
memo search mail 'trip NOT mailbox:bulk'
memo search all 'budget'Mail sent to many — newsletters, notices, mailing lists, anything with a List-Id, List-Unsubscribe or
Precedence: bulk header — is in the mailbox bulk (memo:bulk), so NOT mailbox:bulk leaves it out when it
crowds a search. It is not left out by default: bills, bookings and other notices from companies carry the same
headers. memo marks such mail when it reads its headers, which it does for an account that has rules; a full
listing, at least daily, marks mail stored earlier. --account takes the account's name or its address.
Gmail labels such as Inbox, Important, Starred and your own labels become mail folders, so
mailbox:important finds them. See Search.
- The sender, the recipients (To, CC and BCC), the subject, the date and the text of each email.
- Whether an email is read or starred, as your mailbox shows it.
- The text of readable attachments, searched with the email. PDF and Word attachments need the
optional readers. After you install them, run
memo sync --account work --retry-attachmentsto read attachments that were skipped. It reads again, by their Gmail id, only the emails whose attachment a missing reader left unread, up to--limitof them; it lists nothing, so it is quick, and run again it continues with the rest. - Up to 1,000,000 characters of each email and 200,000 of each attachment (change the limits).
Mail is prepared for search by meaning when the search model is on your computer; --no-embed
skips this.
When you delete an email in your mailbox, its text leaves the local store on the next import. Mail moved to Spam counts as deleted. To protect you from mistakes:
- the first two days of the period are not checked for deletions;
- if a run finds an unusually large part of your mail missing, it deletes nothing and tells you.
To include a person's email in their context, link the
address to them in tg or max: tg contacts link 'Kai Sample' email:[email protected].