Help us shape the future of Go! Take the 2026 Go Developer Survey

contacts

package
v0.2.50 Latest Latest
Warning

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

Go to latest
Published: Oct 22, 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