store

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 6, 2026 License: MIT Imports: 18 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func SplitTokens(q string) []string

SplitTokens is exported for the API package, which short-circuits blank queries before opening a DB connection.

Types

type Address

type Address struct {
	Name    string `json:"name"`
	Address string `json:"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

type CloudConnection struct {
	APIToken      string
	SandboxID     int64
	MirrorEnabled bool
}

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.

func (*Message) Read

func (m *Message) Read() bool

Read reports whether the message has been opened.

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

func Open(path string) (*Store, error)

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 OpenMemory

func OpenMemory() (*Store, error)

OpenMemory is a convenience for tests.

func (*Store) AllCategories

func (s *Store) AllCategories(ctx context.Context) ([]string, error)

AllCategories returns the distinct non-null `category` values, sorted.

func (*Store) AttachmentsCount

func (s *Store) AttachmentsCount(ctx context.Context, ids []string) (map[string]int, error)

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) Close

func (s *Store) Close() error

Close releases the underlying connection pool.

func (*Store) CloudDelete

func (s *Store) CloudDelete(ctx context.Context) error

func (*Store) CloudGet

func (s *Store) CloudGet(ctx context.Context) (*CloudConnection, error)

func (*Store) CloudUpsert

func (s *Store) CloudUpsert(ctx context.Context, c *CloudConnection) error

CloudUpsert replaces the singleton row.

func (*Store) DB

func (s *Store) DB() *sql.DB

DB exposes the raw handle for low-level operations / tests.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, ids ...string) ([]string, error)

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

func (s *Store) Get(ctx context.Context, id string) (*Message, error)

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

func (s *Store) Insert(ctx context.Context, p *IngestPayload) (string, error)

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

func (s *Store) List(ctx context.Context, opts ListOpts) (*ListResult, error)

List loads page of messages newest-first, plus the totals + category list a list endpoint needs.

func (*Store) LoadAttachments

func (s *Store) LoadAttachments(ctx context.Context, msgID string) ([]Part, error)

LoadAttachments returns the regular attachments.

func (*Store) LoadInline

func (s *Store) LoadInline(ctx context.Context, msgID string) ([]Part, error)

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

func (s *Store) LoadPartByID(ctx context.Context, msgID, partID string) (*Part, error)

LoadPartByID looks up a single attachment row by its message + the MIME part identifier the parser assigned. Returns ErrNotFound when missing.

func (*Store) MarkAsRead

func (s *Store) MarkAsRead(ctx context.Context, id string) error

MarkAsRead is the convenience used on GET /message/:id (single-row equivalent of MarkRead(true, id)).

func (*Store) MarkRead

func (s *Store) MarkRead(ctx context.Context, read bool, ids ...string) error

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) RelayDelete

func (s *Store) RelayDelete(ctx context.Context) error

func (*Store) RelayGet

func (s *Store) RelayGet(ctx context.Context) (*RelayConnection, error)

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

func (s *Store) SetSecrets(box *secrets.Box)

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) WebhookDelete

func (s *Store) WebhookDelete(ctx context.Context) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL