contacts

package
v0.2.32-rc4 Latest Latest
Warning

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

Go to latest
Published: Oct 1, 2026 License: MPL-2.0 Imports: 18 Imported by: 0

Documentation

Overview

Package contacts is the durable contacts store behind the chassis `contacts` personality (CardDAV) and the txco://contacts/* ops: accounts, per-account address books, and per-book vCard objects. The object bytes live here, not in the blob CAS — the protocol semantics are mutable (PUT replaces, DELETE deletes) and the store must support real deletion.

Storage is dialect-aware (registry.Dialect, the seam auth, scheduled, imap and calendar use); the bundled backend is a SQLite file of its own. It is deliberately NOT a set of runtime tables: the dbcache watcher reloads the whole runtime mirror on any runtime-DB write.

Identity rules: an object is addressed by its resource name (the URL segment) and carries a UID unique within its address book. A stack put addresses BY UID and keeps whatever resource name the object already has, so a client-created card and its later re-materializations are one object. Unlike the calendar store, the stored bytes are the client's own (CRLF-normalized) — vCard has no sound canonical encoder in the library stack, and a contacts app expects to read back exactly what it wrote (photos, X- properties, groups). The ETag is their sha256; a put whose content differs only in REV or PRODID is a no-op, so a re-materialization never churns etags. Deletes leave a tombstone (deleted_at) with a bumped modseq so a sync-collection REPORT can be added without a schema change; a later put of the same resource name resurrects the row.

Index

Constants

View Source
const (
	VerbPut           = "put"
	VerbDelete        = "delete"
	VerbMkaddressbook = "mkaddressbook"
	VerbRemove        = "remove"
	VerbProppatch     = "proppatch"

	ModeDeny    = "deny"
	ModeLocal   = "local"
	ModeObserve = "observe"
	ModeStack   = "stack"
)

Policy vocabulary: per address book, five verbs, each one of four modes. The chassis has no opinion about what an address book holds; it has one about which client mutations a stack hears of, and when. Same vocabulary as the calendar personality, with `mkaddressbook` for `mkcalendar`.

deny     refused at the protocol layer (403), no round trip
local    protocol state only, no event
observe  commit, then tell the `_contacts` stack (fire-and-forget)
stack    ask the `_contacts` stack first; absent/false @contacts.res.ok ⇒ 403

Resolution: the address book's own policy, then the account's default policy, then the chassis default — `put`/`delete` observe, `proppatch` local (display name / description changes), `mkaddressbook`/`remove` deny (address books are provisioned by ops unless a product opens that).

View Source
const (
	StatusActive   = "active"
	StatusDisabled = "disabled"
)

Account statuses.

View Source
const ProdID = "-//loremlabs//txco contacts//EN"

ProdID identifies chassis-rendered vCards.

Variables

View Source
var (
	// ErrUsernameTaken is returned by UpsertAccount when the username already
	// belongs to another tenant (usernames are globally unique: they are the
	// login identity).
	ErrUsernameTaken = errors.New("contacts: username belongs to another tenant")
	// ErrPrecondition is a failed If-Match / If-None-Match on a put.
	ErrPrecondition = errors.New("contacts: precondition failed")
	// ErrUIDConflict is a put whose UID is already held by another live
	// object in the same address book.
	ErrUIDConflict = errors.New("contacts: uid belongs to another object")
	// ErrNotFound is a missing address book or object.
	ErrNotFound = errors.New("contacts: not found")
)
View Source
var ErrInvalidCard = errors.New("contacts: invalid card")

ErrInvalidCard wraps a validation failure of the card or its bytes, so callers can answer "invalid argument" rather than "store failure".

View Source
var ErrUnsupportedVersion = errors.New("vcard: VERSION is not 3.0 or 4.0")

ErrUnsupportedVersion is a vCard whose VERSION is not 3.0 or 4.0 (the CardDAV `supported-address-data` precondition).

PolicyVerbs lists the verbs a policy object may name.

Functions

func DefaultObjectName

func DefaultObjectName(uid string) string

DefaultObjectName derives a resource name from a UID's local part.

func DefaultUID

func DefaultUID(name, username string) string

DefaultUID is the deterministic UID for a materialized object: <name without .vcf>.<local>@<domain> — stable across re-materializations, unique per persona. Stacks that spell a UID out must derive the same value.

func ETagOf

func ETagOf(vcard []byte) string

ETagOf is the etag of stored bytes: bare sha256 hex.

func Normalize

func Normalize(b []byte) []byte

Normalize is all a client's bytes get before storage: BOM dropped, line endings CRLF, a trailing CRLF.

func NormalizeUsername

func NormalizeUsername(u string) string

NormalizeUsername lowercases and trims a login / op username.

func PolicyMode

func PolicyMode(ab *Addressbook, acct *Account, verb string) string

PolicyMode resolves one verb for an address book (nil for a top-level MKCOL, which resolves from the account alone).

func Register

func Register(name string, c Constructor)

Register adds a backend constructor. Called from a backend package's init().

func Render

func Render(c Card, now time.Time) ([]byte, error)

Render encodes c as a vCard 3.0 (the RFC 6352 baseline every contacts app reads): properties in a fixed order, text escaped, lines folded, CRLF. Deterministic apart from REV, which the store's no-op rule ignores.

func SameContent

func SameContent(a, b []byte) bool

SameContent reports whether two vCards differ only in their REV / PRODID lines — the re-materialization no-op rule. Folded continuation lines are joined before the comparison.

func ToLibrary

func ToLibrary(b []byte) (vcard.Card, error)

ToLibrary re-reads the bytes as a go-vcard Card for go-webdav's listing and query paths (which encode it with go-vcard's encoder). Values are unescaped the way that encoder expects to re-escape them; an escaped `\;` becomes a bare `;` (that encoder never escapes semicolons).

func ValidAddressbookName

func ValidAddressbookName(name string) bool

ValidAddressbookName reports whether name is an address book path segment: a URL segment of up to 128 chars (clients mint UUIDs; ops use short lowercase names).

func ValidMode

func ValidMode(s string) bool

ValidMode reports whether s is a policy mode.

func ValidObjectName

func ValidObjectName(name string) bool

ValidObjectName reports whether name is an object resource name (a URL segment; the conventional `.vcf` suffix is not required — clients choose).

func ValidatePolicy

func ValidatePolicy(raw json.RawMessage) error

ValidatePolicy checks a policy object: every key a known verb, every value a mode.

Types

type Account

type Account struct {
	Tenant    string
	Username  string
	Status    string
	Policy    json.RawMessage
	CreatedAt time.Time
	UpdatedAt time.Time
}

Account is a contacts_accounts row.

type Addressbook

type Addressbook struct {
	ID          string
	Tenant      string
	Username    string
	Name        string
	DisplayName string
	Description string
	SortOrder   int
	Policy      json.RawMessage
	SyncToken   int64
	CreatedAt   time.Time
	UpdatedAt   time.Time
}

Addressbook is a live contacts_addressbooks row.

type BatchError

type BatchError struct {
	Index int
	Op    string // "put" | "delete"
	Err   error
}

BatchError names the entry that failed a Batch (nothing was written).

func (*BatchError) Error

func (e *BatchError) Error() string

func (*BatchError) Unwrap

func (e *BatchError) Unwrap() error

type BatchResult

type BatchResult struct {
	Created int
	Updated int
	Noop    int
	Deleted int
	Missing int // deletes whose uid had no live object
	Puts    []PutResult
}

BatchResult reports what Batch did.

type Card

type Card struct {
	UID      string   `json:"uid,omitempty"`
	FN       string   `json:"fn,omitempty"`
	Name     *Name    `json:"name,omitempty"`
	Nickname string   `json:"nickname,omitempty"`
	Org      string   `json:"org,omitempty"`
	Title    string   `json:"title,omitempty"`
	Note     string   `json:"note,omitempty"`
	URL      string   `json:"url,omitempty"`
	Birthday string   `json:"birthday,omitempty"`
	Emails   []Typed  `json:"emails,omitempty"`
	Phones   []Typed  `json:"phones,omitempty"`
	Kind     string   `json:"kind,omitempty"`
	Members  []string `json:"members,omitempty"`
	// Categories are the card's CATEGORIES values (vCard's free tags —
	// "customer", "vendor"), trimmed and deduped, in order. A label a product
	// may read, never security state.
	Categories []string `json:"categories,omitempty"`

	// Facts the chassis fills in when parsing; ignored when rendering.
	Version   string   `json:"version,omitempty"`
	Rev       string   `json:"rev,omitempty"`
	Addresses []string `json:"addresses"` // EMAIL values, lowercased, deduped, in order
}

Card is the chassis's structured form of one vCard: generic vCard vocabulary, no product words. On the way IN (txco://contacts/put WITH card, @contacts.res.card) the first block is read; on the way OUT (@contacts.card, txco://contacts/get) the facts are filled in as well. `kind` is individual (default), group, org or location; a group's `members` are the member UIDs as the client wrote them (urn:uuid:…).

func CardFromJSON

func CardFromJSON(raw []byte) (Card, error)

CardFromJSON reads a Card from its JSON form (the WITH card{} param or a stack's @contacts.res.card).

func Parse

func Parse(b []byte) (Card, error)

Parse reads the facts of a vCard: exactly one VCARD with a VERSION of 3.0 or 4.0 and a UID. The bytes need not be normalized.

type Config

type Config struct {
	// DBPath is the bundled SQLite backend's file path (--contacts-db-path).
	DBPath string
}

Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath. Adding a field here doesn't affect existing backends (same posture as calendar.Config / imap.Config).

type Constructor

type Constructor func(Config) (*Store, error)

Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.

type ListOpts

type ListOpts struct {
	SinceModSeq    int64
	IncludeDeleted bool
	Names          []string
}

ListOpts narrows ListObjects.

type Name

type Name struct {
	Family     string `json:"family,omitempty"`
	Given      string `json:"given,omitempty"`
	Additional string `json:"additional,omitempty"`
	Prefix     string `json:"prefix,omitempty"`
	Suffix     string `json:"suffix,omitempty"`
}

Name is the structured N property.

type Object

type Object struct {
	AddressbookID string
	Name          string
	UID           string
	ETag          string
	VCard         []byte
	Size          int64
	Version       string   // "3.0" | "4.0"
	FN            string   // the formatted name
	Kind          string   // individual | group | org | location
	Addresses     []string // EMAIL values, lowercased, deduped, in order
	ModSeq        int64
	CreatedAt     time.Time
	UpdatedAt     time.Time
	Deleted       bool
}

Object is a contacts_objects row.

func ObjectFromCard

func ObjectFromCard(username, name string, c Card, now time.Time) (Object, error)

ObjectFromCard renders c (vCard 3.0) into a storable Object. c.UID "" derives DefaultUID(name, username), so name is then required. name "" derives DefaultObjectName(uid).

func ObjectFromVCard

func ObjectFromVCard(name string, raw []byte) (Object, error)

ObjectFromVCard validates raw vCard text and wraps it (normalized) into a storable Object addressed by the bytes' own UID.

type PutOpts

type PutOpts struct {
	// ByUID addresses the object by obj.UID: an existing live object with
	// that UID is updated in place under its own resource name; obj.Name is
	// used only when creating. This is the stack's mode.
	ByUID bool
	// IfMatch / IfNoneMatch are the client's conditional headers, unquoted
	// ("*" for IfNoneMatch means create-only).
	IfMatch     string
	IfNoneMatch string
}

PutOpts steers PutObject.

type PutResult

type PutResult struct {
	Name    string
	UID     string
	ETag    string
	Created bool
	Noop    bool
	ModSeq  int64
}

PutResult reports what PutObject did.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the façade over the three tables. It carries the dialect (for `?`→`$n` rebinding) and a clock seam for tests.

func NewStore

func NewStore(db *sql.DB, d registry.Dialect) *Store

NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).

func Open

func Open(name string, cfg Config) (*Store, error)

Open constructs the named backend. Unknown name is a startup error listing what is available (so a misconfigured --contacts-store fails loudly).

func (*Store) Batch

func (s *Store) Batch(ctx context.Context, abID string, puts []Object, deletes []string) (BatchResult, error)

Batch applies puts (each addressed by UID, the stack's mode) and deletes (by UID) in ONE transaction with one sync_token advance: the bounded write a stack without loops needs to reconcile a list. An error on any entry rolls the whole batch back and names the entry (*BatchError). Unknown uids in deletes are counted as Missing, not errors.

func (*Store) Close

func (s *Store) Close() error

Close releases the underlying DB handle.

func (*Store) DB

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

DB exposes the handle for tests and backends. Not for request paths.

func (*Store) DeleteObject

func (s *Store) DeleteObject(ctx context.Context, abID, name string) (string, bool, error)

DeleteObject tombstones the live object under name. Returns its etag and whether it existed.

func (*Store) EnsureAddressbook

func (s *Store) EnsureAddressbook(ctx context.Context, ab Addressbook) (Addressbook, bool, error)

EnsureAddressbook returns the live address book (tenant, username, name), creating it when absent. On an existing book every non-empty display field of ab (DisplayName, Description, Policy) and a non-zero SortOrder are applied; empty ones are left unchanged. A soft-deleted book of the same name is resurrected (its objects stay tombstoned). created reports a fresh row.

func (*Store) EnsureSchema

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

EnsureSchema creates the tables + indexes if absent. Portable DDL: TEXT ids (hxid), TEXT RFC3339 timestamps, JSON as TEXT, native partial indexes, BIGINT counters — one DDL serves SQLite and Postgres.

func (*Store) GetAccount

func (s *Store) GetAccount(ctx context.Context, username string) (Account, bool, error)

GetAccount looks an account up by username (the login identity).

func (*Store) GetAddressbook

func (s *Store) GetAddressbook(ctx context.Context, tenant, username, name string) (Addressbook, bool, error)

GetAddressbook returns the live address book (tenant, username, name).

func (*Store) GetAddressbookByID

func (s *Store) GetAddressbookByID(ctx context.Context, id string) (Addressbook, bool, error)

GetAddressbookByID returns a live address book by id.

func (*Store) GetObject

func (s *Store) GetObject(ctx context.Context, abID, name string) (Object, bool, error)

GetObject returns the live object under a resource name.

func (*Store) GetObjectByUID

func (s *Store) GetObjectByUID(ctx context.Context, abID, uid string) (Object, bool, error)

GetObjectByUID returns the live object with this UID.

func (*Store) ListAddressbooks

func (s *Store) ListAddressbooks(ctx context.Context, tenant, username string) ([]Addressbook, error)

ListAddressbooks returns the account's live address books, by sort order then name.

func (*Store) ListObjects

func (s *Store) ListObjects(ctx context.Context, abID string, opts ListOpts) ([]Object, error)

ListObjects returns an address book's objects (live only unless IncludeDeleted), optionally after a modseq and/or restricted to names, by name.

func (*Store) PutCard

func (s *Store) PutCard(ctx context.Context, abID, username, name string, c Card, now time.Time) (PutResult, error)

PutCard materializes c into the address book, addressed by UID: an existing object with that UID is updated in place under its own resource name (name is used only on create), unchanged content is a no-op. The write path txco://contacts/put and the CONTACTS/ seed share.

func (*Store) PutObject

func (s *Store) PutObject(ctx context.Context, abID string, obj Object, opts PutOpts) (PutResult, error)

PutObject stores obj (its VCard is the bytes to keep; Name, UID, Version, FN, Kind, Addresses are the facts the caller parsed from them). The address book's sync_token advances with the object's modseq in one transaction. See the package comment for the identity, no-op and tombstone rules.

func (*Store) PutVCard

func (s *Store) PutVCard(ctx context.Context, abID, name string, raw []byte) (PutResult, error)

PutVCard materializes raw vCard text into the address book, addressed by the bytes' own UID.

func (*Store) RemoveAddressbook

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

RemoveAddressbook soft-deletes an address book and tombstones its live objects.

func (*Store) SetAddressbookProps

func (s *Store) SetAddressbookProps(ctx context.Context, id string, displayName, description *string, sortOrder *int) error

SetAddressbookProps applies a client PROPPATCH: only the fields whose pointer is non-nil change (an empty string clears).

func (*Store) SetClock

func (s *Store) SetClock(now func() time.Time)

SetClock pins the store's clock (tests).

func (*Store) UpsertAccount

func (s *Store) UpsertAccount(ctx context.Context, tenant, username, status string, policy json.RawMessage) (created bool, err error)

UpsertAccount creates the account or updates it. An empty status / policy leaves the stored value unchanged on update. created reports whether the row was new. The account holds no password: who may sign in as it is the identity store's business (chassis/authn — a binding and a credential).

type Typed

type Typed struct {
	Value string   `json:"value"`
	Type  []string `json:"type,omitempty"`
	Pref  bool     `json:"pref,omitempty"`
}

Typed is one EMAIL or TEL value with its TYPE parameters.

Jump to

Keyboard shortcuts

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