package main // The reader's own index (novox/hq ADR 0459 §4): mail entries, mail groups and reading lessons for the verbs // (1521, 1428), the waiting mails heard but not yet read (1518) and the reader's own health state (1519), each // over postgres (pgstore.go) or in memory for the tests. Nothing here keeps a mail body or text taken from one. import ( "context" "time" ) // Person is a sender, as mesh-mail's own event names one (name and address, never a body). type Person struct { Name string `json:"name,omitempty"` Address string `json:"address"` } // Mark is an entry's mark (novox/hq ADR 0459 §7). type Mark string const ( MarkMatters Mark = "matters" MarkScamWarning Mark = "scam warning" MarkNeither Mark = "neither" ) // Entry is one mail entry (ADR 0459 §4): the mail's own headers (never its body), the mail group it was // batched into, a one-line summary, a mark and why, a due date, and whether it was told on the mail signal. type Entry struct { ID int64 Account string MessageID string Folder string Date time.Time SenderName string SenderAddress string Subject string Group int64 // the mail group's id, 0 when ungrouped Summary string Mark Mark // "" until a batch judges it Why string DueDate time.Time // zero when none Told bool // Judged is when a batch judged this mail (issue 1520) — the week the weekly measure (1521/AC3) counts it // in, which is never the mail's own Date: a mail that arrived long before it was judged is read this week. Judged time.Time } // Group is one mail group (ADR 0459 §4). Count, how many entries hold it, is answered by Store.Groups // (1521/AC2) and is never itself kept. type Group struct { ID int64 Label string Created time.Time Count int } // Lesson is one reading lesson (ADR 0459 §8, §9): the corrected mail's own headers (sender name and address, // subject, account and folder — never its body), the change made, when, and whether it was undone (1428/AC3). // SenderAddress is the lesson's own matching key for the next batch's question (1427/AC1: "sender ... meets a // mail of the batch"); Group, when not zero, is a second matching key for a mail already known to be of that // group (the same clause's "or mail group") — no issue yet looks up a mail's group before it is judged, so in // practice only SenderAddress matching fires today (1427's own tests); the field and the comparison both exist // so that lookup can be added later without reshaping this type. type Lesson struct { ID int64 Account string Folder string SenderName string SenderAddress string Subject string Group int64 // 0 when the correction did not carry a mail group Change string Made time.Time Undone bool } // WaitingMail is one mail heard (an added event) but not yet handed to reading (ADR 0459 §3, §12, issue 1518): // headers only, never a body. Seq is the order it was heard in — a monotonic sequence, independent of Date — // so catch-up after an outage reads in arrival order (1518/AC4), including mail heard while reading was // unhealthy. type WaitingMail struct { Seq int64 Account string Folder string MessageID string UIDValidity uint32 UID uint32 Date time.Time From []Person Subject string Flags []string Size int64 Heard time.Time } // Key is the waiting mail's own identity: its message id wherever it has one (stable across a UIDVALIDITY // reset), else its account, folder, UIDVALIDITY and UID (novox/hq ADR 0459 §3; the same fallback mesh-mail's // own event dedupe, mesh-mail-notifier's EventBody.Handle, uses). func (w WaitingMail) Key() string { if w.MessageID != "" { return w.Account + "\x1f" + w.MessageID } return w.Account + "\x1f" + w.Folder } // HealthOutcome names why a batch's judgement did not succeed (novox/hq ADR 0459 §12); the empty outcome means // it succeeded. type HealthOutcome string const ( OutcomeOK HealthOutcome = "" OutcomeRefused HealthOutcome = "refused" OutcomeTimedOut HealthOutcome = "timed out" OutcomeOutOfShape HealthOutcome = "out of shape" ) // ReaderHealth is the kept health state (1519): unhealthy once a judgement is refused, times out or answers out // of shape, naming the cause, the judgement's own words, and since when; healthy again once one succeeds. Kept // in the module's own database, so a restart answers the same (1519/AC4). type ReaderHealth struct { Healthy bool Cause HealthOutcome // "" while healthy Why string // the judgement's own words Since time.Time // the first failure's own time; zero while healthy } // Store is everything mesh-mail-reader keeps of its own. A PgStore (pgstore.go) backs it in the running // module; the tests use an in-memory double (kit_test.go). type Store interface { // AddWaiting records an added event as a waiting mail before the event is acknowledged (1518/AC2): a mail // heard is never lost, even if the module crashes before it is read. The same mail heard twice (1518/AC3, // matched by WaitingMail.Key) is recorded once; AddWaiting itself never errors on a duplicate. AddWaiting(ctx context.Context, w WaitingMail) error // Waiting answers every waiting mail in arrival order, oldest first (1518/AC4). Waiting(ctx context.Context) ([]WaitingMail, error) // TakeWaiting removes a waiting mail once it has been handed to reading (1520). TakeWaiting(ctx context.Context, seq int64) error // ReaderHealth answers the kept health state (1519/AC4); healthy with a zero Since on a fresh database. ReaderHealth(ctx context.Context) (ReaderHealth, error) // SetReaderHealth replaces the kept health state. SetReaderHealth(ctx context.Context, h ReaderHealth) error // AddEntry records a new mail entry (1520/AC3) and answers its own id. AddEntry(ctx context.Context, e Entry) (int64, error) // UpdateEntry replaces an entry's fields by its id (1428/AC1, correct). ErrNotFound when unknown. UpdateEntry(ctx context.Context, e Entry) error // Entry answers one entry by id, and whether it was found (1428/AC1, AC5: an unknown id is refused). Entry(ctx context.Context, id int64) (Entry, bool, error) // Entries answers every mail entry; ordering and filtering for the verb are 1521's own pure functions, not // the store's. Entries(ctx context.Context) ([]Entry, error) // FindOrCreateGroup answers the mail group of this label, making one if none yet carries it (1520/AC3): the // judge names groups by their label, never by id. FindOrCreateGroup(ctx context.Context, label string) (Group, error) // Groups answers every mail group, each with how many entries hold it (1521/AC2). Groups(ctx context.Context) ([]Group, error) // AddLesson keeps one reading lesson at once (1428/AC1) and answers its own id. AddLesson(ctx context.Context, l Lesson) (int64, error) // Lessons answers every reading lesson, undone ones included (1427's own selection, and 1428's lessons // verb, each decide for themselves what to do with Undone). Lessons(ctx context.Context) ([]Lesson, error) // UndoLesson marks one reading lesson undone (1428/AC3). ErrNotFound when unknown. UndoLesson(ctx context.Context, id int64) error } // ErrNotFound is answered by a Store method asked for something it does not hold. var ErrNotFound = notFoundError{} type notFoundError struct{} func (notFoundError) Error() string { return "not found" }