176 lines
7.4 KiB
Go
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" }
|