Files
mesh-catalog/modules/mesh-mail-reader/cmd/mesh-mail-reader/store.go
T

176 lines
7.4 KiB
Go

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" }