Skip to content

Add message filters and capture markers for human mailboxes (#04, #22)#6

Merged
EgorGruzdev merged 4 commits into
masterfrom
feature/04-22-filters-and-marks
Jun 11, 2026
Merged

Add message filters and capture markers for human mailboxes (#04, #22)#6
EgorGruzdev merged 4 commits into
masterfrom
feature/04-22-filters-and-marks

Conversation

@EgorGruzdev

Copy link
Copy Markdown
Contributor

Реализует #04 (фильтрация) и #22 (ящики-люди) — вместе они закрывают сценарий «ящик операторов: искать символ в теме, обрабатывать, не помечать прочитанным».

#4 — фильтры захвата

Карта filters (глобально или на маршруте): allow/deny по from/subject/header/has_attachment, регэкспы или wildcard'ы. deny побеждает allow, пустой allow пропускает всё. Отфильтрованное письмо помечается маркером ящика, но не попадает в журнал/архив/доставку. Битые правила отвергаются при конструировании матчера и ловятся доктором.

#22 — маркер просмотренного

Опция mark (глобально или на маршруте):

  • seen (дефолт) — как раньше;
  • keyword:<имя> — кастомный IMAP-кейворд: выборка UNKEYWORD, прочитанность не трогается, курсор на сервере;
  • none — письмо не трогается вовсе; UID-курсор в новой таблице relay_cursors со сбросом при смене UIDVALIDITY. Курсор продвигает pull; в IDLE новые письма захватываются дедупом, перечитанный после рестарта хвост отбрасывается без повторной доставки.

mailspoon:doctor валидирует mark и фильтры по каждому ящику и напоминает про PERMANENTFLAGS \* для keyword-ящиков.

Сценарий целиком

'routes' => [
    'operators' => [
        'endpoint' => 'https://crm.example.com/api/mailgun/mime',
        'key' => 'key-operators',
        'mark' => 'keyword:Mailspoon',
        'filters' => ['allow' => ['subject' => ['/⚡/u']]],
    ],
],

Тесты

71 passed (249 assertions), Pint чистый. Новые: матчер (10 кейсов), маркер (6), listener с фильтром/кейвордом/none, выборки pull по маркеру, resume/restart UID-курсора, доктор с битым фильтром и репортом маркера.

A `filters` map — global or per route — declares allow/deny rules on
from, subject, header and has_attachment. Patterns are regular
expressions when delimited and case-insensitive wildcards otherwise.
Deny wins over allow; an empty allow list allows everything. A
filtered message is still marked seen so it is not picked up again,
but never reaches the journal, archive or endpoint. Malformed rules
(broken regex, unknown field) are rejected at matcher construction
instead of silently skipping mail.
A `mark` option — global or per route — chooses how mailspoon marks
messages it has already viewed. The default `seen` keeps using the
\Seen flag, right for robot mailboxes. For mailboxes read by humans
the read state stays untouched: `keyword:<name>` marks with a custom
IMAP keyword (selection switches to UNKEYWORD; invisible in mail
clients, cursor lives on the server), and `none` leaves the message
alone entirely, tracking position in a new relay_cursors table by UID,
restarted when UIDVALIDITY changes (cron-poll only — IDLE does not
advance the cursor).

The marker applies to every viewed message, including filtered ones,
so nothing is re-read on the next pull; only messages passing the #4
filter are journaled and relayed. mailspoon:doctor validates the mark
and filter rules per mailbox and reminds about PERMANENTFLAGS for
keyword mailboxes.
@EgorGruzdev EgorGruzdev merged commit 3ee38fe into master Jun 11, 2026
@EgorGruzdev EgorGruzdev deleted the feature/04-22-filters-and-marks branch June 11, 2026 22:03
EgorGruzdev added a commit that referenced this pull request Jun 12, 2026
A configurable `after` map (global and per route) decides what happens to
a message in its mailbox once its relay outcome is known: none (default),
seen, keyword:<name>, move:<folder> or delete. The vocabulary is shared
with capture markers instead of the originally sketched mark_seen/flag:.

Outcomes land in different store-and-forward phases. `filtered` is known
at capture, so its action runs right in the listener while the connection
is open (marker first, so a failing action cannot cause a re-pull).
`delivered` and `failed` are reached in mailspoon:deliver without an IMAP
connection — they are applied by the new scheduled mailspoon:tidy
command, which reconnects and finds each message by the UID stored at
capture. Because UIDs are only stable within one UIDVALIDITY epoch, tidy
verifies the found message against the journal record (Message-Id, or
raw fingerprint when the header is absent) before acting.

`failed` means the final outcome: a message is only acted on once its
delivery attempts are exhausted. mailspoon:replay resets the tidy state
along with the attempt counter, so the new outcome gets its own action.
Move targets are created automatically; delete only flags \Deleted.
mailspoon:doctor validates and reports the actions per mailbox.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant