Documentation
¶
Overview ¶
Package store owns the SQLite database — schema, models, queries.
Single struct (Store) encapsulates the *sql.DB plus prepared statements. The handler layer never sees raw SQL; tests can inject a :memory: store via OpenMemory.
Index ¶
- Variables
- func SplitTokens(q string) []string
- type Address
- type CloudConnection
- type IngestPayload
- type ListOpts
- type ListResult
- type Message
- type Part
- type PartIn
- type RelayConnection
- type SearchOpts
- type Store
- func (s *Store) AllCategories(ctx context.Context) ([]string, error)
- func (s *Store) AttachmentsCount(ctx context.Context, ids []string) (map[string]int, error)
- func (s *Store) Close() error
- func (s *Store) CloudDelete(ctx context.Context) error
- func (s *Store) CloudGet(ctx context.Context) (*CloudConnection, error)
- func (s *Store) CloudUpsert(ctx context.Context, c *CloudConnection) error
- func (s *Store) DB() *sql.DB
- func (s *Store) Delete(ctx context.Context, ids ...string) ([]string, error)
- func (s *Store) Get(ctx context.Context, id string) (*Message, error)
- func (s *Store) Insert(ctx context.Context, p *IngestPayload) (string, error)
- func (s *Store) List(ctx context.Context, opts ListOpts) (*ListResult, error)
- func (s *Store) LoadAttachments(ctx context.Context, msgID string) ([]Part, error)
- func (s *Store) LoadInline(ctx context.Context, msgID string) ([]Part, error)
- func (s *Store) LoadPartByID(ctx context.Context, msgID, partID string) (*Part, error)
- func (s *Store) MarkAsRead(ctx context.Context, id string) error
- func (s *Store) MarkRead(ctx context.Context, read bool, ids ...string) error
- func (s *Store) RelayDelete(ctx context.Context) error
- func (s *Store) RelayGet(ctx context.Context) (*RelayConnection, error)
- func (s *Store) RelayUpsert(ctx context.Context, r *RelayConnection) error
- func (s *Store) Search(ctx context.Context, opts SearchOpts) (*ListResult, error)
- func (s *Store) SetSecrets(box *secrets.Box)
- func (s *Store) WebhookDelete(ctx context.Context) error
- func (s *Store) WebhookGet(ctx context.Context) (*WebhookConnection, error)
- func (s *Store) WebhookUpsert(ctx context.Context, w *WebhookConnection) error
- type WebhookConnection
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("store: not found")
ErrNotFound is returned by the Get helpers when the requested row doesn't exist. Callers can check with errors.Is.
Functions ¶
func SplitTokens ¶
SplitTokens is exported for the API package, which short-circuits blank queries before opening a DB connection.
Types ¶
type Address ¶
Address mirrors the wire shape (PascalCase keys come from the JSON tags on api.Address, not here). Internally we always carry it as {Name, Address}.
type CloudConnection ¶
CloudConnection mirrors the singleton row that drives forwarding to a Mailtrap cloud sandbox. There's at most one row at any time.
type IngestPayload ¶
type IngestPayload struct {
SMTPFrom string `json:"smtp_from"`
SMTPTo []string `json:"smtp_to"`
MessageID string `json:"message_id"`
From *Address `json:"from"`
To []Address `json:"to"`
Cc []Address `json:"cc"`
Bcc []Address `json:"bcc"`
ReplyTo []Address `json:"reply_to"`
ReturnPath string `json:"return_path"`
Subject string `json:"subject"`
Date string `json:"date"`
Category string `json:"category"`
Text string `json:"text"`
HTML string `json:"html"`
Raw []byte `json:"raw"`
Size int `json:"size"`
Snippet string `json:"snippet"`
Inlines []PartIn `json:"inlines"`
Attachments []PartIn `json:"attachments"`
}
IngestPayload is the decoded form of the JSON the SMTP layer hands us. Same field set as the /api/v1/ingest contract — snake_case keys so the HTTP and in-process paths share one struct, and so captured payloads stay readable in logs.
type ListOpts ¶
type ListOpts struct {
Start int // 0-based offset, clamped to [0, 1_000_000] by caller
Limit int // 1..200
Category string // optional exact-match filter (empty = no narrowing)
}
ListOpts narrows + paginates `List`.
type ListResult ¶
type ListResult struct {
Total int
Unread int
AllCategories []string
Messages []*Message
AttachmentsCnt map[string]int // ID → count (cheap to fetch alongside)
}
ListResult is the aggregate List() returns. Counts reflect the filtered scope (matching ListOpts.Category if set); AllCategories is distinct categories across the *unfiltered* sandbox so the caller can render a category picker without a second roundtrip.
type Message ¶
type Message struct {
ID string
SMTPFrom string
SMTPTo []string
MessageID string
FromName string
FromAddress string
ToAddresses []Address
CcAddresses []Address
BccAddresses []Address
ReplyTo []Address
ReturnPath string
Subject string
Date *time.Time // nullable — only set when the Date header parsed
Category *string // nullable
TextBody string
HTML string
Raw []byte
Size int64
Snippet string
RecipientsText string
ListUnsubscribe json.RawMessage // nullable JSON object; nil when absent
ReadAt *time.Time // nil = unread
CreatedAt time.Time
UpdatedAt time.Time
}
Message is the in-memory view of a row in `messages` plus its attachments (loaded on demand). Times are kept as time.Time; JSON-shaped columns are unmarshalled into typed slices.
type Part ¶
type Part struct {
ID int64 // attachments.id
MessageID string
PartID string
Filename string
ContentType string
ContentID string
Disposition string // "inline" | "attachment"
Size int64
Content []byte
ChecksumMD5 string
ChecksumSHA1 string
ChecksumSHA256 string
}
Part is the persisted shape for an inline image or attachment. Mirrors the columns of the `attachments` table.
type PartIn ¶
type PartIn struct {
PartID string `json:"part_id"`
Filename string `json:"filename"`
ContentType string `json:"content_type"`
ContentID string `json:"content_id"`
Size int `json:"size"`
Content []byte `json:"content"` // base64-decoded by encoding/json
}
PartIn is the decoded inline/attachment shape on the way in.
type RelayConnection ¶
type RelayConnection struct {
Host string
Port int
Username string // may be ""
Password string // may be ""; never returned in API responses
Auth string // plain | login | none | cram_md5
TLS string // auto | ssl | off | always | never
AutoRelayEnabled bool
OverrideFrom string // optional From: rewrite
ReturnPath string // optional MAIL FROM rewrite
}
RelayConnection drives the "forward through a real SMTP relay" feature (per-message Forward + auto-relay). One row.
type SearchOpts ¶
type SearchOpts struct {
Query string
Start int
Limit int
Category string // optional further narrowing
}
SearchOpts narrows search results.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps a *sql.DB opened against SQLite. Safe for concurrent use. SQLite is single-writer, so we let database/sql's connection pool serialize writes naturally; reads scale across the pool.
func Open ¶
Open returns a Store backed by the SQLite file at `path`. Creates the file (and its parent directory) if missing, applies the schema on first open, enables WAL + foreign keys.
The empty path is treated as ":memory:" (used by tests).
func (*Store) AllCategories ¶
AllCategories returns the distinct non-null `category` values, sorted.
func (*Store) AttachmentsCount ¶
AttachmentsCount is the public version that takes IDs directly — used by main.go's live-broadcast helper which has the ID, not a *Message slice.
func (*Store) CloudUpsert ¶
func (s *Store) CloudUpsert(ctx context.Context, c *CloudConnection) error
CloudUpsert replaces the singleton row.
func (*Store) Delete ¶
Delete removes the listed message IDs (and their attachments via the FK CASCADE). When ids is empty, deletes ALL messages — matches the "DELETE without IDs == truncate" wire contract.
Returns the IDs that were actually deleted (so the caller can broadcast destroyed events on the live channel).
func (*Store) Get ¶
Get returns the message with the given ID, or ErrNotFound.
`latest` is honored as an alias for "most recent message" so the SPA can deep-link to /api/v1/message/latest in dev.
func (*Store) Insert ¶
Insert persists a message + its attachments in a single transaction. The message ID is generated here (10-byte url-safe base64). Returns the assigned ID.
func (*Store) List ¶
List loads page of messages newest-first, plus the totals + category list a list endpoint needs.
func (*Store) LoadAttachments ¶
LoadAttachments returns the regular attachments.
func (*Store) LoadInline ¶
LoadInline returns the inline parts (Disposition='inline') for a message, ordered by id. Caller pre-decides whether the bytes are needed (this loads them); for list contexts, bytes are wasted IO.
func (*Store) LoadPartByID ¶
LoadPartByID looks up a single attachment row by its message + the MIME part identifier the parser assigned. Returns ErrNotFound when missing.
func (*Store) MarkAsRead ¶
MarkAsRead is the convenience used on GET /message/:id (single-row equivalent of MarkRead(true, id)).
func (*Store) MarkRead ¶
MarkRead sets read_at on the listed IDs. With no IDs, marks ALL. `read=true` → read_at = now; `read=false` → read_at = NULL.
func (*Store) RelayUpsert ¶
func (s *Store) RelayUpsert(ctx context.Context, r *RelayConnection) error
func (*Store) Search ¶
func (s *Store) Search(ctx context.Context, opts SearchOpts) (*ListResult, error)
Search runs a multi-token AND search against the FTS5 index. Each whitespace-separated token is wrapped as a quoted phrase, then joined with spaces — FTS5's default AND-of-phrases semantics gives the same "every token matches somewhere" behaviour the old LIKE implementation had, but with an actual index doing the work.
func (*Store) SetSecrets ¶
SetSecrets attaches a secrets.Box for at-rest encryption of the sensitive connection fields (cloud API token, relay password, webhook secret). Without it, the Store falls back to plaintext — fine for unit tests, never used by the real binary which always calls SetSecrets right after Open.
func (*Store) WebhookGet ¶
func (s *Store) WebhookGet(ctx context.Context) (*WebhookConnection, error)
func (*Store) WebhookUpsert ¶
func (s *Store) WebhookUpsert(ctx context.Context, w *WebhookConnection) error
type WebhookConnection ¶
type WebhookConnection struct {
URL string
Secret string // optional; never returned in API responses
Enabled bool
}
WebhookConnection drives outbound webhook fan-out on every newly captured message.