ETAF mailing-list client: public-inbox threads, fixed-header reader, local read/star state and smtpmail replies
  • Emacs Lisp 84%
  • Python 15.3%
  • Makefile 0.7%
Find a file
Kinneyzhang 727816a7ee
Some checks failed
Acceptance / acceptance (push) Has been cancelled
Repository structure / structure (push) Has been cancelled
test(maillist): measure the list, the reader and a cached search
Three workloads registered with the workspace driver, each seeding a store
through the public API and mounting the client over it, so a sample measures
the client rather than the network: scrolling a three-hundred-thread list,
opening one thread after another from it, and searching nine hundred cached
bodies.

They found the two defects the preceding commits fix, and the manual now
records how to run them and what a run measured (p50 26.4 ms per move,
22.4 ms per thread opened, 5.9 ms per cached-body search).

Validation: performance.py run --scenario 'maillist.*' --mode gui reports
valid samples for all three scenarios inside their 150-200 ms budgets, with no
failed assertion; make check stays green (49 scenarios).
2026-10-31 11:05:23 +08:00
.githooks chore(package): adopt the shared workspace gates 2026-10-22 00:04:04 +08:00
.github/workflows ci(maillist): run the client's own acceptance suite 2026-10-30 19:37:38 +08:00
benchmarks test(maillist): measure the list, the reader and a cached search 2026-10-31 11:05:23 +08:00
docs perf(maillist): derive the theme palette once and repaint on a switch 2026-10-31 11:04:51 +08:00
examples docs(maillist): release 0.3.0 with its workflows and failures 2026-10-30 22:14:05 +08:00
lisp fix(maillist): answer store queries from the store's own file 2026-10-31 11:05:14 +08:00
scripts feat(maillist): answer a whole thread and jump between unread mail 2026-10-30 22:33:47 +08:00
tests fix(maillist): answer store queries from the store's own file 2026-10-31 11:05:14 +08:00
.editorconfig chore(package): adopt the shared workspace gates 2026-10-22 00:04:04 +08:00
.gitignore feat(app): read a public-inbox list as a mail client 2026-10-22 00:04:04 +08:00
CHANGELOG.md fix(maillist): answer store queries from the store's own file 2026-10-31 11:05:14 +08:00
LICENSE docs(maillist): release 0.3.0 with its workflows and failures 2026-10-30 22:14:05 +08:00
maillist.el feat(maillist): answer a whole thread and jump between unread mail 2026-10-30 22:33:47 +08:00
Makefile test(maillist): drive the client through a GUI scenario 2026-10-30 22:13:59 +08:00
README.md feat(maillist): answer a whole thread and jump between unread mail 2026-10-30 22:33:47 +08:00

maillist

ETAF mailing-list client for Emacs. It reads public-inbox archives — the thread list, the conversation and each message — and keeps the reader's own read and starred marks, and the fetched bodies, in a local SQLite file. Replies and new posts open in message-mode and leave through smtpmail.

At a wide frame the list and the open thread share the window; the divider between them is draggable:

The client at a wide frame

Below maillist-narrow-width the two columns become one: the list first, then the open thread with a way back above it.

The client at a narrow frame

A short window keeps the header, the rows and the reader without stretching either column past the canvas:

The client in a short window

Capabilities

  • Thread list with the archive's own date grouping, unread and starred scopes, and one row per discussion.
  • Reads the conversation message by message: quotes, patch hunks and the addresses a message mentions are marked up, and a long thread opens with its newest messages and grows on demand.
  • Search in the archive's own syntax (from:, subject:, dates) plus local marks; a search the archive cannot answer is served from the cached bodies.
  • Read marks: opening a thread marks it read once its body is on screen, M-u puts it back in the queue and A clears a scope.
  • Older mail than the archive's first page loads from its date search, one batch per keypress.
  • Several archives (emacs-devel, emacs-user, emacs-bugs, list tangents) switch from inside the client, which remembers the last one.
  • An open client syncs on a schedule; a thread the archive added is counted in the summary and tinted in the list.
  • Reply to the list, reply to the author, or post anew, with In-Reply-To and References filled in.

Requirements

  • Emacs 29.1 or newer, built with SQLite.
  • For a checkout, the provider repositories on load-path: etaf, etaf-ui, ebox, ecss, etaf-db.

Install

From the ETAF package archive:

(require 'package)
(add-to-list 'package-archives '("etaf" . "https://etaf.top/packages/"))
(package-initialize)
(package-refresh-contents)
(package-install 'etaf-install)

Then M-x etaf-install, pick Maillist, and the installer brings ETAF, ETAF UI and ETAF DB with it. M-x maillist-open starts the client.

From a checkout, add this repository and the provider roots to load-path:

(require 'maillist)
M-x maillist-open

Keys

Key Action
j / k next / previous thread
RET or a click open the thread
1 2 3 all, unread only, starred only
M-u mark the open thread unread
A mark every shown row read
n open the next unread thread
G jump to the last row
s star or unstar
e expand the older messages a long thread holds back
o load mail older than the list
/ search
M-p / M-n recall the previous / next search
g sync again (re-runs the open search when one is active)
C copy the archive page of the thread
B open that page in the browser
r / R / C-u r reply to the list / reply-all / reply to the author
c post a new message
L switch archive
u back from the thread to the list (narrow layout)
drag the divider resize the list (arrow keys move it once focused)
n / p / wheel scroll inside the list or the reader

Search syntax

Form Effect
from: to: subject: body: message: the matching archive field
after:2026-10-01 before:2026-10-10 date range
is:unread is:starred local marks, no request

A query with only local conditions filters the list in place; g runs the current search again. The thread being read stays in the list, so j carries on from it.

Sending

Sending goes through smtpmail. Store the account's application password in auth-source once:

(setq maillist-mail-address "you@gmail.com")
;; then M-x maillist-mail-configure and paste the application password

The defaults are smtp.gmail.com:465 with SSL; change maillist-smtp-server, maillist-smtp-service and maillist-smtp-stream-type for another provider. The password is read when the mail is sent, so a reply can be written before the account is configured. GNU lists hold posts from unsubscribed addresses for moderation.

Data and network

  • Threads, marks, cached bodies and the last archive live in ~/.emacs.d/maillist/store.sqlite3 (maillist-store-file). The file uses WAL, so a second Emacs can read while the first writes.
  • The cache is bounded: maillist-thread-limit threads per archive and maillist-body-limit cached bodies; a prune keeps the newest of each and never drops a mark.
  • maillist-network-route picks the leading route: auto tries direct first, proxy tries the proxy first, direct never uses one. A route that cannot connect or stops answering hands the request to the other one (maillist-route-attempt-timeout), so a proxy client that is not running does not take the archive offline, and a blocked direct path still has the proxy. maillist-proxy names the proxy; empty takes it from url-proxy-services or the environment. A failed request names what it could not reach, for example 无法连接代理 127.0.0.1:7890;无法连接档案 yhetil.org:443.
  • maillist-auto-refresh (300 seconds by default) syncs an open client. Set it to nil to sync only on demand.

docs/manual.md covers the workflows, every configuration variable, the storage layout and what to do when a sync, a search or a send fails.

Development

make check     # structure, byte compilation, acceptance scenarios, public API
make test      # acceptance scenarios only
make gui       # drive the maintained GUI scenario on a running Emacs server
make clean

The acceptance scenarios read the fixture archive under tests/fixtures/archive over file: URLs, so they need no network. make gui takes SERVER=<name> for a dedicated instance and prints the evidence directory of the run; the frames it saves are what a UI change is reviewed against.