mailbox

package
v1.99.0 Latest Latest
Warning

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

Go to latest
Published: Jun 20, 2026 License: GPL-3.0 Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const (
	RightLookup        = 'l' // visible to LIST/LSUB/SELECT result
	RightRead          = 'r' // SELECT/EXAMINE/FETCH; \Seen excluded
	RightWriteSeen     = 's' // STORE of \Seen
	RightWrite         = 'w' // STORE of other flags except \Seen, \Deleted
	RightInsert        = 'i' // APPEND, COPY destination (private namespaces)
	RightPost          = 'p' // delivery via MTA (public / shared namespaces)
	RightCreate        = 'k' // CREATE / RENAME of children — was obsolete 'c'
	RightDeleteMailbox = 'x' // DELETE / RENAME of the mailbox itself
	RightDeleteMessage = 't' // STORE \Deleted on a message — was part of 'd'
	RightExpunge       = 'e' // EXPUNGE — was part of 'd'
	RightAdminister    = 'a' // SETACL / DELETEACL / GETACL / LISTRIGHTS
)

RFC 4314 §2.1 single-letter ACL rights. The 11 codes below are the canonical set used in this package. Obsolete codes 'c' / 'd' are accepted only on input by ParseRights and immediately expanded — they never appear in canonical output.

Letter map mirrors dovecot-2.4/src/plugins/acl/acl-rights.c:15-28.

View Source
const FullRights = Rights("lrswipkxtea")

FullRights is the union of every RFC 4314 right.

Variables

This section is empty.

Functions

func AttrKey added in v1.19.0

func AttrKey(scope AttrScope, folderGUID [16]byte, attrName string) string

AttrKey returns the dict key for a per-folder attribute on a personal-namespace mailbox.

scope chooses the priv/ or shared/ namespace; folderGUID is the rename-stable identifier of the target folder (Folder.GUID); attrName is the path-component (without leading slash) returned by ParseAttrEntry.

The resulting key is of the form:

priv/box/<hex(guid)>/<attrName>

The "box/" segment exists so a future extension (e.g. per-message or per-user-scoped attributes) can co-exist under priv/<other>/... without colliding with mailbox attributes.

For shared / public namespaces use SharedAttrKey instead — it adds a per-accessing-user dimension to the priv/ scope so users do not see each other's private annotations on a shared folder.

func AttrPrefix added in v1.19.0

func AttrPrefix(scope AttrScope, folderGUID [16]byte) string

AttrPrefix returns the dict-iteration path for "all attributes of this personal-namespace folder under this scope". Useful for METADATA GETMETADATA with DEPTH 1 / DEPTH INFINITY.

func ExpandVars added in v0.3.10

func ExpandVars(template, username string) string

ExpandVars rewrites Dovecot %-variables against a username.

%u → full username                  (alice@example.com)
%n → local part (before @)          (alice)
%d → domain    (after @)            (example.com)
%%   → literal %

Unknown sequences pass through unchanged. A bare trailing % is preserved.

func FormatAttrEntry added in v1.19.0

func FormatAttrEntry(scope AttrScope, attrName string) string

FormatAttrEntry returns the wire-format entry name for a (scope, attrName) pair: the inverse of ParseAttrEntry. Used when iterating dict keys back into a GETMETADATA response map.

func IsValidRight added in v1.39.0

func IsValidRight(r rune) bool

IsValidRight reports whether r is one of the 11 RFC 4314 rights. The obsolete 'c' / 'd' codes are NOT valid Right runes — they are only tolerated on input and expanded by ParseRights.

func ParseMailLocationMods added in v1.59.0

func ParseMailLocationMods(loc string) map[string]string

ParseMailLocationMods parses the modifier section of a Dovecot mail location string of the form "driver:path:KEY1=v1:KEY2=v2". Returns a map of uppercase modifier keys to their raw (unexpanded) values. Returns nil when the string has fewer than three colon-separated segments (i.e. no modifiers present).

func ServerAttrKey added in v1.19.0

func ServerAttrKey(scope AttrScope, inboxGUID [16]byte, attrName string) string

ServerAttrKey returns the dict key for a server-scope attribute.

Server-scope attributes (those whose IMAP mailbox name is empty) are stored under INBOX's GUID with the serverScopeVendorPrefix added so they cannot collide with same-named INBOX mailbox attributes.

func ServerAttrPrefix added in v1.19.0

func ServerAttrPrefix(scope AttrScope, inboxGUID [16]byte) string

ServerAttrPrefix returns the iteration path for server-scope attributes — the equivalent of AttrPrefix for "" (empty) mailbox.

func SharedAttrKey added in v1.21.0

func SharedAttrKey(scope AttrScope, folderGUID [16]byte, accessingUser, attrName string) string

SharedAttrKey returns the dict key for a per-folder attribute on a shared or public namespace mailbox. For the priv/ scope the key embeds a per-accessing-user dimension (SHA-256 hash of the username, first 16 hex chars) so users cannot see each other's private annotations on the same shared folder. The shared/ scope remains a single value visible to everyone.

Hashing avoids embedding raw usernames (which may contain '/' or '%' that would clash with dict path semantics) and gives a stable, case-sensitive identifier without escaping.

func SharedAttrPrefix added in v1.21.0

func SharedAttrPrefix(scope AttrScope, folderGUID [16]byte, accessingUser string) string

SharedAttrPrefix returns the iteration path for a shared/public namespace folder. Mirrors SharedAttrKey: priv/ scope is per-user (key includes the accessingUser hash subdir), shared/ scope is global to the folder.

func TrimAttrPrefix added in v1.19.0

func TrimAttrPrefix(key, prefix string) string

TrimAttrPrefix extracts the attrName from a full dict key, given the scope+guid prefix the iterator used. Returns "" if key does not lie under prefix (defensive — callers should pre-filter).

Types

type ACL added in v1.39.0

type ACL []Entry

ACL is the ordered set of entries from a dovecot-acl file. Order is preserved on read but is semantically insignificant — Negative=true entries subtract from any matching positive regardless of position. Sorted() returns a deterministic ordering for stable on-disk writes.

func ParseACL added in v1.39.0

func ParseACL(r io.Reader) (ACL, error)

ParseACL reads a full dovecot-acl file body. Comment and blank lines are skipped; the first malformed entry aborts parsing with the 1-based line number annotated.

Scanner buffer is bumped to 1 MiB so an oversized line surfaces as a scanner error rather than silent truncation.

func ParseACLString added in v1.39.0

func ParseACLString(s string) (ACL, error)

ParseACLString is a convenience wrapper for tests and CLI input.

func (ACL) Effective added in v1.39.0

func (acl ACL) Effective(user string, groups []string, isOwner bool) Rights

Effective resolves the effective rights an authenticated user has on this mailbox's stored ACL.

Semantics (RFC 4314 §3.5):

  • isOwner == true: returns FullRights regardless of stored entries (Dovecot personal-namespace auto-grant).
  • else: union of positive entries whose Identifier matches the accessing user — anyone, authenticated, user=<user>, group=<g> (when <g> is in groups), group-override=<g> — minus negative entries matching the same identifiers.

groups is the list of supplementary groups the user belongs to, sourced from the userdb `groups=` extra field. Pass nil or empty when no group membership is configured — group= entries have no effect.

Identifier type priority (higher tier replaces lower when matched):

  1. anyone / authenticated / group= → base tier
  2. user= → replaces if any user= entry matches
  3. group-override= → replaces if any group-override= matches

A nil ACL with isOwner == false yields the empty rights set — no implicit grant for non-owners is the RFC 4314 default.

func (ACL) Sorted added in v1.39.0

func (acl ACL) Sorted() ACL

Sorted returns a copy with entries in deterministic order: identifiers grouped by Type (anyone, authenticated, owner, user=, group=, group-override=) and then alphabetically by Name; within the same identifier, positive entries precede negative.

func (ACL) String added in v1.39.0

func (acl ACL) String() string

String returns the full dovecot-acl file encoding — one entry per line, each line LF-terminated. Empty ACL serialises to "" (no trailing newline) so an empty file is distinguishable from a single-empty-entry file.

type AttrScope added in v1.19.0

type AttrScope int

AttrScope distinguishes per-user-private from per-resource-shared attributes.

const (
	AttrPrivate AttrScope = iota
	AttrShared
)

func ParseAttrEntry added in v1.19.0

func ParseAttrEntry(entry string) (AttrScope, string, error)

ParseAttrEntry splits a wire-format METADATA entry name like "/private/comment" into (scope, attrName). The attrName is everything after the scope prefix, with the leading slash dropped:

/private/comment           → (AttrPrivate, "comment")
/shared/admin              → (AttrShared,  "admin")
/private/vendor/yarilo/abc → (AttrPrivate, "vendor/yarilo/abc")

Returns an error if the entry does not start with /private/ or /shared/, or is otherwise malformed. The fork-side ValidateMetadataEntry already catches most of this on parse; we re-check here so callers that bypass the wire layer (tests, scripted scaffolding) still get the same guarantees.

type Entry added in v1.39.0

type Entry struct {
	Identifier Identifier
	Rights     Rights
	Negative   bool
}

Entry is one parsed line of a dovecot-acl file. Negative=true entries strip Rights at evaluation time rather than granting them (RFC 4314 §3.5). Negatives are kept as separate entries — fold-in happens in the evaluator, not here.

func ParseEntry added in v1.39.0

func ParseEntry(line string) (Entry, bool, error)

ParseEntry parses one line of a dovecot-acl file:

[-]<identifier><WS><letters>

Whitespace-only and '#'-prefixed lines return (Entry{}, false, nil) — the caller skips them. Trailing comments are not supported, to match dovecot-2.4/src/plugins/acl/acl-backend-vfile.c:187-231.

func (Entry) String added in v1.39.0

func (e Entry) String() string

String returns the canonical wire/disk encoding of one entry without trailing newline. Empty Rights still serialises with a trailing space so `<id> ` round-trips — Dovecot reads that as "grant nothing explicitly", which differs from "no entry".

type FlagsUpdate added in v1.94.0

type FlagsUpdate struct {
	Flags    []string
	Keywords []string
}

ScanRecord is one entry produced by UserMailbox.Scan — the raw per-message info a storage driver can reconstruct from disk alone (no help from the index). Used by admin rebuild flows to regenerate the fileindex after corruption or operator request.

Fields populated per driver:

maildir: Filename, Size (from "S=" or stat), VSize (from "W="),
         InternalDate (from stat mtime), Flags (parsed from
         the ":2,FLAGS" trailer); GUID stays zero — Maildir
         filenames carry no GUID.
dbox:    Filename, GUID (from "G<hex>\n" trailer line),
         Size+VSize (from "Z<hex>" / "V<hex>"), InternalDate
         (from "R<hex>" Unix epoch); Flags empty — dbox
         delegates flag storage to the index.
mdbox:   not implemented in this phase — driver returns
         "not yet implemented" until Phase MDBOX-PROD-READY.

FlagsUpdate carries the new flag and keyword sets for one message in a batch UpdateFlagsMulti call.

type Folder

type Folder struct {
	ID            uint64
	Name          string
	UIDValidity   uint32
	NextUID       uint32
	Messages      uint32
	Unseen        uint32
	HighestModSeq uint64
	// GUID is a stable 16-byte identifier stamped at folder creation
	// time. Survives RENAME (unlike Name). Used as the key namespace
	// for per-folder metadata in pkg/dict (RFC 5464 METADATA) and as
	// the rename-stable handle for ACL state, quota counters, etc.
	GUID [16]byte
}

Folder holds index-level folder metadata.

type Identifier added in v1.39.0

type Identifier struct {
	Type IdentifierType
	Name string // user=/group=/group-override= only
}

Identifier is a parsed ACL identifier — the left side of a dovecot-acl line, without the optional '-' negativity marker.

func ParseIdentifier added in v1.39.0

func ParseIdentifier(s string) (Identifier, error)

ParseIdentifier parses the wire/disk form into an Identifier. Rejects empty Name for user=/group=/group-override=. The '-' negativity marker is handled by ParseEntry, not here.

func (Identifier) String added in v1.39.0

func (id Identifier) String() string

String returns the wire/disk form. Returns "" for IDInvalid so callers serialising an unvalidated Identifier produce a clearly malformed line rather than silent data.

type IdentifierType added in v1.39.0

type IdentifierType int

IdentifierType enumerates the six identifier kinds RFC 4314 + Dovecot recognise. Name is meaningful only for IDUser / IDGroup / IDGroupOverride. The zero value IDInvalid is never produced by ParseIdentifier — callers can use it as a sentinel.

const (
	IDInvalid IdentifierType = iota
	IDAnyone
	IDAuthenticated
	IDOwner
	IDUser
	IDGroup
	IDGroupOverride
)

type IndexBackend

type IndexBackend interface {
	OpenUser(*UserInfo) UserIndex
}

IndexBackend is the per-process factory for user-scoped index handles.

Phase 5 — multi-namespace stub:

OpenNamespace(user *UserInfo, namespace string) (UserIndex, error)
    Returns an index handle for a non-private namespace.
    Implementation: separate Backend instance rooted at the namespace index dir.

type Location added in v1.21.0

type Location struct {
	Driver string // "maildir", "sdbox" (alias: "dbox"), "mdbox"
	Path   string // expanded absolute path (varexpand applied)
}

Location is a parsed namespace storage URL: "driver:path". Currently only the "maildir" driver is supported; other drivers (dbox, mdbox) are accepted by the parser for forward-compat but must match the globally configured cfg.Storage.Mailbox driver — per-namespace driver mixing is deferred until backends gain a shared OpenNamespace dispatch.

func ParseLocation added in v1.21.0

func ParseLocation(loc string, ui *UserInfo) (Location, bool, error)

ParseLocation parses "driver:path" into Location. When loc is empty returns (Location{}, false) so callers can detect "namespace not configured for storage" and fall through to NS-1a wire-only mode. Path is %u/%n/%d/%h-expanded against ui — when ui is nil expansion is a no-op (callers without a user context, e.g. system-wide shared/public namespaces, pass nil and ship a literal absolute path).

type MailboxBackend

type MailboxBackend interface {
	OpenUser(*UserInfo) UserMailbox
}

MailboxBackend is the per-process factory for user-scoped storage handles. It holds no per-user state; all per-user state lives in UserMailbox.

Phase 5 — multi-namespace stub:

OpenNamespace(user *UserInfo, namespace string) (UserMailbox, error)
    Returns a handle for a shared or public namespace (e.g. "shared/" or "Public").
    Implementation: separate Backend instance rooted at the namespace storage dir,
    wrapped with the same UserMailbox contract. Namespace list comes from
    config.Namespaces (private/shared/public, each with its own location template).

type MessageMeta

type MessageMeta struct {
	UID          uint32
	Filename     string // backend-specific filename returned by UserMailbox.Save
	Flags        []string
	Keywords     []string
	ModSeq       uint64
	Size         uint32
	VSize        uint32
	InternalDate time.Time
	GUID         [16]byte
	CacheOffset  uint32
	// AltTier is true when the message body resides in alt (cold) storage.
	// Stored as FlagBackend (0x40) in the on-disk index record so Fetch()
	// can open the correct tier without a wasted primary-tier syscall.
	// Only meaningful for mdbox; other drivers ignore it.
	AltTier bool
}

MessageMeta holds per-message metadata stored in the index.

type Resolver added in v0.3.10

type Resolver struct {
	// Root is prepended to template-derived paths. Absolute override paths
	// from userdb skip Root entirely.
	Root string

	// HomeTemplate is the Dovecot-style template for users with no userdb
	// override. Default: "%d/%u" (virtual hosting layout).
	HomeTemplate string

	// DefaultQuotaRules are applied to every UserInfo produced by this
	// Resolver when the userdb lookup provides no per-user override.
	// Mirrors Dovecot's quota_rule setting at the global level.
	DefaultQuotaRules []string

	// DefaultVolatileDir is the cluster-wide VOLATILEDIR template applied
	// when no per-user override arrives from userdb. Supports the same
	// %u/%n/%d/%h variables as HomeTemplate. Empty disables volatile dir
	// (default). Mirrors Dovecot's VOLATILEDIR mail-location modifier at
	// the global config level.
	DefaultVolatileDir string

	// DefaultIndexDir is the cluster-wide INDEX= template applied when no
	// per-user override arrives from userdb. Supports %u/%n/%d/%h. Empty
	// keeps index files co-located with the mailbox (default). Mirrors
	// Dovecot's INDEX= mail-location modifier at the global config level.
	DefaultIndexDir string

	// DefaultControlDir is the cluster-wide CONTROL= template applied when
	// no per-user override arrives from userdb. Supports %u/%n/%d/%h.
	// Empty keeps control files co-located with the mailbox (default).
	// Mirrors Dovecot's CONTROL= mail-location modifier at the global
	// config level.
	DefaultControlDir string

	// DefaultAltDir is the cluster-wide ALT= template applied when no
	// per-user override arrives from userdb. Supports %u/%n/%d/%h.
	// Empty disables two-tier storage (default). Mirrors Dovecot's
	// ALT= mail-location modifier at the global config level.
	DefaultAltDir string
}

Resolver maps a username (+ optional userdb override) to an absolute home directory using a Dovecot-style template (%u / %n / %d expansion).

Dovecot reference:

mail_home = /var/mail/vhosts/%d/%n         (defaults shipped here)
mail_location = maildir:~/Maildir          (consumed by storage layer)

When userdb returns a non-empty home value it overrides the template outright; otherwise the template is expanded and joined with Root.

func (*Resolver) Resolve added in v0.3.10

func (r *Resolver) Resolve(username, homeOverride string) string

Resolve returns the absolute home directory for a user. An empty homeOverride falls back to expanding HomeTemplate against the username and joining with Root.

func (*Resolver) UserInfo added in v0.3.10

func (r *Resolver) UserInfo(username, homeOverride string) *UserInfo

UserInfo builds a fully-resolved UserInfo by running Resolve against the supplied username + userdb override. DefaultVolatileDir (if set) is expanded with %u/%n/%d/%h variables and stored in VolatileDir; per-user overrides can overwrite this field after the call returns.

type Rights added in v1.39.0

type Rights string

Rights is a set of RFC 4314 right letters in canonical order. Always sorted, deduped, free of obsolete codes; equality is byte-equality on the underlying string.

func MustParseRights added in v1.39.0

func MustParseRights(s string) Rights

MustParseRights panics on error. Test helper.

func ParseRights added in v1.39.0

func ParseRights(s string) (Rights, error)

ParseRights normalises a wire/disk letter string into Rights:

  • dedupes letters
  • sorts to canonical order
  • expands obsolete 'c' → 'k' and 'd' → 'te' (RFC 4314 §2.1.1)
  • rejects any other character with a descriptive error

Empty input returns the empty Rights set without error.

func (Rights) Add added in v1.39.0

func (rs Rights) Add(other Rights) Rights

Add returns rs ∪ other in canonical form.

func (Rights) Has added in v1.39.0

func (rs Rights) Has(r rune) bool

Has reports whether r is in the set.

func (Rights) HasAll added in v1.39.0

func (rs Rights) HasAll(other Rights) bool

HasAll reports whether every right in other is also in rs.

func (Rights) Remove added in v1.39.0

func (rs Rights) Remove(other Rights) Rights

Remove returns rs ∖ other in canonical form.

func (Rights) String added in v1.39.0

func (rs Rights) String() string

String returns the canonical letter string.

type ScanRecord added in v1.25.0

type ScanRecord struct {
	Filename     string
	GUID         [16]byte
	Size         uint32
	VSize        uint32
	InternalDate time.Time
	Flags        []string
}

type SeqRange

type SeqRange struct {
	From, To uint32 // inclusive; To==0 means '*'
}

type SeqSet

type SeqSet []SeqRange

SeqSet is a set of UIDs or sequence numbers (use UID=0 for seq).

type UserIndex added in v0.3.10

type UserIndex interface {
	OpenFolder(folder string, uidValidity uint32) (*Folder, error)
	SaveFolder(f *Folder) error
	AppendMessage(folderID uint64, m *MessageMeta) error
	// AllocateUID atomically reserves and persists the folder's next UID
	// under the cross-process mailbox lock. The caller passes the UID to
	// UserMailbox.Save, then records the full MessageMeta via AppendMessage.
	//
	// If the caller fails between AllocateUID and AppendMessage the UID
	// is burnt — matches Dovecot's "uid hole" tolerance. Periodic rebuild
	// reconciles state by scanning the on-disk tree.
	AllocateUID(folderID uint64) (uint32, error)
	// AllocateUIDWithModSeq atomically reserves the folder's next UID and
	// pre-allocates the next modseq value in one lock/reload/flush cycle,
	// replacing the separate AllocateUID + NextModSeq calls in Append.
	AllocateUIDWithModSeq(folderID uint64) (uid uint32, modseq uint64, err error)
	// AllocateAndAppend assigns a UID and records the message in a single
	// lock/reload/flush cycle — the go-imap appendBytes pattern applied to
	// persistent storage. m.UID and m.ModSeq are filled in by the call;
	// all other fields must be set by the caller. m.Filename must already
	// be known (box.Save completes before this call). Replaces the two-step
	// AllocateUID(WithModSeq) + AppendMessage pattern that caused APPEND stalls.
	AllocateAndAppend(folderID uint64, m *MessageMeta) error
	UpdateFlags(folderID uint64, uid uint32, flags, keywords []string) error
	// UpdateFlagsMulti replaces flags+keywords for a batch of UIDs in a
	// single lock/reload/flush cycle. Returns the new modseq per UID.
	UpdateFlagsMulti(folderID uint64, updates map[uint32]FlagsUpdate) (map[uint32]uint64, error)
	// SetAltTier sets or clears the AltTier marker (FlagBackend) for every
	// message in folderID whose Filename matches one of the supplied names.
	// Called by the altmove API after physically relocating mdbox m.<N> files
	// so subsequent Fetch calls can skip the primary-tier open() attempt.
	// Operates under the cross-process mailbox lock for the folder.
	SetAltTier(folderID uint64, filenames []string, altTier bool) error
	GetMessages(folderID uint64, uids SeqSet) ([]*MessageMeta, error)
	ExpungeMessage(folderID uint64, uid uint32) error
	NextModSeq(folderID uint64) (uint64, error)
	// Vanished returns UIDs expunged from folderID with modseq strictly
	// greater than sinceModSeq. Drives QRESYNC (RFC 7162) SELECT and
	// UID FETCH (CHANGEDSINCE N VANISHED) responses.
	Vanished(folderID uint64, sinceModSeq uint64) ([]uint32, error)
	Keywords(folderID uint64) ([]string, error)
	// RenameFolder renames oldName to newName in the index.
	// Called by IMAP RENAME immediately after UserMailbox.Rename succeeds.
	RenameFolder(oldName, newName string) error
	// GetPOP3UIDLs loads saved POP3 UIDLs for a folder (uid → uidl string).
	// Returns an empty map when no saved UIDLs exist yet.
	GetPOP3UIDLs(folderID uint64) (map[uint32]string, error)
	// SavePOP3UIDLs persists POP3 UIDLs so subsequent sessions use stable values.
	SavePOP3UIDLs(folderID uint64, uidls map[uint32]string) error
	// ResetFolder atomically replaces the on-disk record set for
	// folderID with the supplied messages. Preserves UIDVALIDITY
	// and the folder GUID; bumps NextUID past max(records.UID);
	// HighestModSeq advances by one so QRESYNC clients invalidate
	// their caches. Drives the admin rebuild flow. Caller has
	// already taken the cross-process mailbox lock and made a
	// .bak of the old base file.
	ResetFolder(folderID uint64, records []*MessageMeta) error
	// OptimizeIndex compacts the .index.log overlay into the base
	// .index file. Returns a no-op nil when there is nothing to
	// compact. Takes the same X lock as a normal write.
	OptimizeIndex(folderID uint64) error
	Close() error
}

UserIndex is a per-session, per-user index handle. All folder IDs are local to this handle — they must not be shared across handles.

type UserInfo added in v0.3.10

type UserInfo struct {
	// Username is the canonical login (typically full email). Used for
	// per-user concurrency counters, log lines, and message-ID Received
	// headers — never as a storage path on its own.
	Username string

	// Home is the absolute filesystem root for the user's mailbox tree.
	// Resolved from either userdb.home (override) or the storage.mail_home
	// template expanded against the username. See Resolver.
	Home string

	// VolatileDir, when non-empty, redirects volatile index artefacts
	// (Recreate tmp files) to a local path instead of the NFS-backed
	// index directory. Mirrors Dovecot's VOLATILEDIR mail-location
	// modifier. Template vars (%u/%n/%d/%h) are already expanded by the
	// time this field is populated.
	VolatileDir string

	// IndexDir, when non-empty, redirects all per-folder index files
	// (yarilo.index*, yarilo-acl) to a separate directory tree. The
	// mailbox data (Maildir cur/new/tmp) stays under Home; only index
	// state moves. Mirrors Dovecot's INDEX= mail-location modifier.
	// Template vars (%u/%n/%d/%h) are already expanded.
	IndexDir string

	// ControlDir, when non-empty, redirects per-folder control files
	// (yarilo-uidlist, subscriptions) to a separate directory tree.
	// The mailbox data and index files are unaffected. Mirrors Dovecot's
	// CONTROL= mail-location modifier.
	// Template vars (%u/%n/%d/%h) are already expanded.
	ControlDir string

	// AltDir, when non-empty, enables two-tier maildir storage. Messages
	// that have been cold-tiered (via altmove) live under AltDir instead
	// of Home. Reads check both primary (Home) and alt; writes always go
	// to the primary tier. Mirrors Dovecot's ALT= mail-location modifier.
	// Template vars (%u/%n/%d/%h) are already expanded.
	AltDir string

	// Groups is the list of supplementary groups the user belongs to,
	// sourced from the userdb `groups=` extra field (comma-separated).
	// ACL evaluation matches these against `group=<name>` and
	// `group-override=<name>` entries. Empty when not configured —
	// group= ACL entries have no effect in that case.
	Groups []string

	// QuotaRules is the list of per-user quota rules sourced from the
	// userdb `quota_rule=` extra field. Format: `*:storage=5G` or
	// `*:messages=100000`. Empty means no quota limit.
	QuotaRules []string

	// SessionID is the IMAP/POP3 session identifier assigned by the login
	// proxy. Included in the yarilo-locks owner string for BUSY diagnostics.
	// Empty for LMTP and other non-session contexts.
	SessionID string
}

UserInfo carries the per-session storage identity for a user. The fields are resolved once at session start (after passdb/userdb lookup) and passed to MailboxBackend.OpenUser / IndexBackend.OpenUser. Backends MUST use Home directly and not re-derive a path from Username — UserInfo is the contract for "where this user's data lives".

Only Username and Home are required for the current phase. The commented fields below are placeholders — adding a field here is backward-compatible (all callers pass *UserInfo by pointer; OpenUser signatures do not change).

type UserMailbox added in v0.3.10

type UserMailbox interface {
	Init() error
	Create(folder string) error
	Delete(folder string) error
	Rename(oldName, newName string) error
	Save(folder string, r io.Reader, uid uint32, size int64, flags []string) (string, error)
	// Fetch returns a reader for the message body. altTier hints that the
	// message lives in alt (cold) storage so the driver can open it directly
	// without trying the primary path first. The hint is set from
	// MessageMeta.AltTier which is persisted in the index as FlagBackend.
	Fetch(folder, filename string, altTier bool) (io.ReadCloser, error)
	Remove(folder, filename string) error
	List(folder string) ([]*MessageMeta, error)
	FolderExists(folder string) (bool, error)
	ListFolders() ([]string, error)
	// Scan walks the on-disk representation of folder and yields
	// every visible message as a ScanRecord. Used by the admin
	// rebuild flow to regenerate the fileindex independently of
	// whatever the index currently believes. Returns
	// (nil, fmt.Errorf("driver/scan: not yet implemented"))
	// from drivers that have not implemented disk-scan yet.
	Scan(folder string) ([]ScanRecord, error)
	Close() error
}

UserMailbox is a per-session, per-user storage handle bound to a single UserInfo at creation time. Mirrors Dovecot's struct mail_storage (mail-storage-private.h:138).

Init MUST be called before any other method — it creates the on-disk directory structure. Callers that open a handle but never call Init will see errors from the underlying filesystem operations.

Save takes the assigned UID as a parameter. Drivers that encode the UID in the on-disk filename (sdbox: u.<uid>; mdbox: map_uid bookkeeping) use it directly; Maildir ignores the UID for its filename but writes the uid→filename mapping into the dovecot-uidlist sidecar inline. The canonical caller flow is:

uid     := idx.AllocateUID(folderID)
filename := box.Save(folder, r, uid, size, flags)
idx.AppendMessage(folderID, &MessageMeta{UID: uid, Filename: filename, ...})

If Save fails after AllocateUID, the UID is burnt (matches Dovecot's behaviour — the index simply skips the hole on next scan).

Close releases any open file descriptors held by the handle.

Jump to

Keyboard shortcuts

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