Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

## [0.8.1] - 2026-10-02

### Added

- Non-ASCII search terms also work on servers that advertise `LITERAL-` instead of `LITERAL+`, such as Gmail, for terms up to 4096 bytes

### Changed

- Non-ASCII folder names are shown decoded (`[Gmail]/Messages envoyés` instead of `[Gmail]/Messages envoy&AOk-s`), and folder options and config settings accept either form; `--json` output and `export` file names use the server's listed name (so `--folder inbox` exports `INBOX_<uid>.eml`)
- `--limit` fetches headers only for recent matches when they suffice instead of for every match, on servers without SORT, such as Gmail, and in each folder of `--all-folders` on every server (`search --limit 5` on a 7,480-message Gmail INBOX: 22.9 s to 1.6 s)
- On Proton Mail Bridge, `--all-folders` skips the label views (`Labels/...` and `Starred`), so a labelled message is listed and counted once, from its regular folder (on a 150,000-message account, `search --all-folders --limit 5` went from 33.9 s to 3.1 s and the `count --all-folders` total from 305,131 to 151,285)

### Fixed

- `--all-folders` and `status` skip containers that cannot be opened (`\Noselect`, such as Gmail's `[Gmail]`) instead of warning about or listing them
- On Gmail, `--all-folders` lists a message once even when it has several labels (the INBOX copy if there is one), and actions apply to that copy; `count --all-folders` totals count it once
- `--all-folders` also skips folders inside Trash, Spam/Junk, and All Mail (such as `[Gmail]/Trash/Old`)
- When slashmail orders messages itself (servers without SORT, `--all-folders`, `--all-accounts`), a Date header more than a day after a message arrived counts as arrival plus one day, so a wrong or forged future date no longer pins a message to the top or pushes newer messages out of a `--limit`

## [0.8.0] - 2026-09-28

### Added
Expand Down Expand Up @@ -180,7 +199,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
- Plaintext connection warning for non-loopback hosts
- Passwords securely zeroed from memory after login

[Unreleased]: https://github.com/mwmdev/slashmail/compare/v0.8.0...HEAD
[Unreleased]: https://github.com/mwmdev/slashmail/compare/v0.8.1...HEAD
[0.8.1]: https://github.com/mwmdev/slashmail/compare/v0.8.0...v0.8.1
[0.8.0]: https://github.com/mwmdev/slashmail/compare/v0.7.0...v0.8.0
[0.7.0]: https://github.com/mwmdev/slashmail/compare/v0.6.0...v0.7.0
[0.6.0]: https://github.com/mwmdev/slashmail/compare/v0.5.0...v0.6.0
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "slashmail"
version = "0.8.0"
version = "0.8.1"
edition = "2021"
rust-version = "1.88"
description = "CLI for searching, managing, drafting, and bulk-operating on emails via IMAP"
Expand Down
36 changes: 22 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Commands:
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)
count Count matching messages (no header or body fetch)
quota Show mailbox quota usage
status Show per-folder message statistics
```
Expand Down Expand Up @@ -266,7 +266,7 @@ 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)
--all-folders Search across all folders (excludes Trash, Junk/Spam, All Mail, and folders inside them)
--subject <TEXT> Subject contains
--from <TEXT> From address contains
--to <TEXT> To address contains
Expand All @@ -288,7 +288,13 @@ Search, read, count, and bulk message commands share these filter options:

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.
`--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, and every folder inside one of those (such as `Deleted Items/2024` or `[Gmail]/Trash/Old`). It also skips containers that cannot be opened (`\Noselect`, such as Gmail's `[Gmail]`) but still searches the folders inside them. `delete` and `move` also never search their destination folder, and naming the destination as the only source folder is an error.

On Gmail, where every label is a folder, `--all-folders` lists a message once even when it has several labels: the INBOX copy if there is one, otherwise the copy in the first folder listed. `read`, `export`, `mark`, `move`, and `delete` act on that copy. `count --all-folders` still shows each folder's own count, but its total counts each message once.

On Proton Mail Bridge, every message sits in exactly one regular folder (INBOX, Archive, Sent, `Folders/...`), and its `Labels/...` folders and `Starred` are views of those messages. `--all-folders` skips those views there, so each message is listed and counted once, from its regular folder.

Servers list non-ASCII folder names in IMAP's modified UTF-7 (`[Gmail]/Messages envoy&AOk-s`). The terminal shows them decoded (`[Gmail]/Messages envoyés`), and every folder option and config setting accepts either form. `--json` output keeps the server's form.

### Action options

Expand All @@ -305,11 +311,11 @@ Commands that modify messages (`delete`, `move`, `mark`) support:

`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.
`export` supports `--yes`, `--force` (replace existing files), and `-o, --output-dir`. Files are named `<folder>_<uid>.eml`, where `<folder>` is the server's listed name (`INBOX` even for `--folder inbox`, and non-ASCII names in their encoded form), 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 terms containing non-ASCII text are sent as UTF-8 literals and require the server to advertise `LITERAL+`, or `LITERAL-` (as Gmail does) for terms up to 4096 bytes; otherwise the search fails before any mailbox is searched. Proton Mail Bridge advertises neither, so non-ASCII search fails there. Bridge also matches `--subject` and `--text` against the raw message as stored, so words inside an encoded subject (common when it has non-ASCII characters, often base64) may not match, accented or not. To find such messages there, narrow with other filters (`--from`, `--since`) and check the decoded `subject` in `search --json`.

## Examples

Expand Down Expand Up @@ -376,7 +382,7 @@ 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)
# Count matching messages (fast, no header or body fetch)
slashmail count -u user@example.com --from "newsletter"

# Show folder statistics
Expand Down Expand Up @@ -455,17 +461,19 @@ Destructive operations always dry-run first and ask for confirmation.

## Tested with

- Gmail (via `--tls --host imap.gmail.com`)
- Fastmail (via `--tls --host imap.fastmail.com`)
- Dovecot
- Any standard IMAP4rev1 server
- Gmail (`--tls --host imap.gmail.com`, with an app password)
- Proton Mail Bridge 3.23 (`127.0.0.1:1143`, read-only checks)
- Dovecot 2.3
- GreenMail (automated test suite)

Other IMAP4rev1 servers should work. `delete` and `move` need `MOVE` or `UIDPLUS`, and non-ASCII search needs `LITERAL+` or `LITERAL-`.

## How it works

- All filtering runs server-side via IMAP SEARCH
- Uses IMAP SORT extension (RFC 5256) when available; falls back to client-side sort
- With SORT, `--limit` truncates results before fetching (fewer bytes over the wire)
- `search`, `delete`, `move`, `mark`, `count` only fetch headers and size -- never full messages
- Uses IMAP SORT extension (RFC 5256) when available for a single folder. Otherwise, and for every folder of `--all-folders` and every account of `--all-accounts`, slashmail orders by the Date header, but never later than one day after a message arrived, so a wrong or forged future date cannot keep a message at the top
- `--limit` keeps header fetches small. With SORT, a single-folder search is truncated before fetching. Otherwise (no SORT, as on Gmail and Proton Bridge, and each folder or account being merged), a large search is first narrowed to recently arrived messages with `SINCE`, and every match is fetched only when those hold too few. The result is the same as fetching everything
- `search`, `delete`, `move`, `mark` only fetch headers and size -- never full messages; `count` fetches neither (only message IDs with `--all-folders` on Gmail)
- `export` fetches full message bodies via `BODY.PEEK[]`
- Uses `BODY.PEEK` to avoid marking messages as read, and opens folders read-only (`EXAMINE`) for `search`, `read`, `count`, `export`, and `--dry-run`, so they do not clear the `\Recent` flag
- UID sets are compressed into ranges and chunked to stay within IMAP command length limits
Expand Down Expand Up @@ -497,7 +505,7 @@ All errors print to stderr. Combine `--yes` with cron or scripts for unattended
### Folder not found

- Run `slashmail status` to list all available folders and their names
- Folder names are case-sensitive on most IMAP servers
- `search`, `read`, `export`, `mark`, `move`, and `delete` need a name `slashmail status` lists, decoded as shown or as the server sends it (INBOX in any letter case). `count`, `reply`, and `attachments` also try a name exactly as typed, for mailboxes a server opens but does not list
- Gmail uses `[Gmail]/Trash`, `[Gmail]/All Mail`, etc. — use `--trash-folder` with `delete` if needed
- Exchange/Outlook uses `Deleted Items` instead of `Trash`

Expand Down
7 changes: 5 additions & 2 deletions skills/slashmail/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Optional `sender` and `drafts_folder` values can be set at the top level or per
| Flag | Description |
|------|-------------|
| `-f, --folder FOLDER` | Target folder (default: INBOX); cannot be combined with `--all-folders` |
| `--all-folders` | Search all folders (excludes Trash, Junk/Spam, All Mail, and the move/delete destination) |
| `--all-folders` | Search all folders (excludes Trash, Junk/Spam, All Mail and folders inside them, and the move/delete destination; on Proton Bridge also `Labels/...` and `Starred`; on Gmail each message is one row, from INBOX when it is there) |
| `--subject TEXT` | Filter by subject |
| `--from TEXT` | Filter by sender |
| `--to TEXT` | Filter by recipient |
Expand Down Expand Up @@ -143,7 +143,10 @@ With `--json`, the receipt is one object with `account`, `folder`, `uid`, `messa
- Never pass an empty text filter (for example an unset variable as `--from`); slashmail rejects it rather than matching every message.
- If `export` reports existing files that hold a different message, do not add `--force` without the user's approval; suggest a new output directory instead.
- If an action on searched messages fails because the folder's UIDVALIDITY changed, the mailbox was rebuilt since the search: run the search again and act on the new UIDs. Never reuse the old ones.
- `delete` and `move` require server support for `MOVE` or `UIDPLUS` and fail before changing anything otherwise. Non-ASCII search terms require `LITERAL+`; without it the search fails rather than matching nothing. Report these failures instead of working around them.
- `delete` and `move` require server support for `MOVE` or `UIDPLUS` and fail before changing anything otherwise. Non-ASCII search terms require `LITERAL+`, or `LITERAL-` for terms up to 4096 bytes; without it the search fails rather than matching nothing. Report these failures instead of working around them.
- `--json` `folder` values are the server's names (non-ASCII names in modified UTF-7, such as `[Gmail]/Messages envoy&AOk-s`). Pass them back unchanged; folder options also accept the decoded name the terminal shows.
- With `--all-folders`, a Gmail message with several labels is one row, from INBOX when it is there, and commands act on that copy. Gmail keeps flags per message, so `mark` changes it in every label. On Proton Bridge, label folders are skipped: to work with a label, name it with `--folder` (for example `--folder "Labels/Clients"`).
- On Proton Bridge, server search cannot see words inside an encoded subject (common when it has non-ASCII characters): `--subject` and `--text` both match the raw message. Narrow with other filters (`--from`, `--since`) and check the decoded `subject` field of `search --json` yourself, and tell the user before reporting a Bridge subject search as complete.

## Received Attachment Rules

Expand Down
Loading
Loading