engine

package
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 65 Imported by: 0

Documentation

Overview

Package engine keeps each account's local copy of a mail provider: the folders, the messages with their flags, and a cache of the messages clients read. A sync engine per account keeps the copy current, whether or not a client is connected, and a queue takes the changes clients make to the provider. The client protocols (imapproxy, jmapproxy) serve from it, and the providers (IMAP, JMAP) feed it.

Clients log in with their provider credentials. The engine keeps them in memory only, while it needs them to reach the provider.

Index

Constants

View Source
const (
	KindEmail   = "Email"
	KindMailbox = "Mailbox"
)

The kinds of object the log stamps.

View Source
const FetchBatch = 25

FetchBatch is how many messages one upstream FETCH of full bodies asks for.

View Source
const PreviewLen = 256

PreviewLen is how long a preview may be, in characters (RFC 8621 section 4.1.4: up to 256).

Variables

View Source
var (
	// ErrNoAccount means no provider serves the login.
	ErrNoAccount = errors.New("no account matches the login")
	// ErrNoMailbox means the folder does not exist, or cannot hold
	// messages.
	ErrNoMailbox = errors.New("no such mailbox")
)
View Source
var (
	// SyncPoll is how often the other folders are checked, and INBOX on
	// providers without IDLE.
	SyncPoll = time.Minute
	// SyncPollNotified replaces SyncPoll for a provider that reports
	// every change itself (watchConn.Notifies).
	SyncPollNotified = 15 * time.Minute
	// SyncListEvery is how often the folder list is read again.
	SyncListEvery = 10 * time.Minute
)
View Source
var ErrCannotCalculateChanges = errors.New("cannot calculate changes")

ErrCannotCalculateChanges means the log cannot answer from a state: it is from another epoch, or older than the log keeps.

View Source
var (
	ErrLoginRejected = errors.New("login rejected")
)
View Source
var ErrNoCredentials = errors.New("no credentials for the account")

ErrNoCredentials means no client has logged in since the provider last rejected the password, or since the proxy started.

View Source
var ErrNoSearch = errNoSearch

ErrNoSearch means the provider cannot search: read the messages.

View Source
var ErrNoSending = errors.New("the provider does not send through the engine")

ErrNoSending means the account's provider has no way to send through the engine: an IMAP provider's mail goes to its submission server.

View Source
var ErrSendRefused = errors.New("the provider refused the message")

ErrSendRefused means the provider refused to send a message.

View Source
var ErrTransform = errors.New("message could not be stored")

ErrTransform means the Transform refused a message a client stores.

View Source
var NewMailWait = 8 * time.Second

NewMailWait is the longest a resync waits for new mail's download before it tells clients. 0 does not wait.

View Source
var PieceWhole int64 = 32 << 10

PieceWhole is the size up to which a message is read whole when a client asks for a piece of it.

View Source
var (
	// ReadAheadCount is how many messages one read-ahead downloads.
	ReadAheadCount = 100
)
View Source
var SelectFresh = 5 * time.Second

SelectFresh is how old a folder's state may be when a client opens it before the engine checks the provider again.

View Source
var SelectWaits = false

SelectWaits makes opening a folder synced before wait for the check; tests set it. Otherwise the client gets the engine's state at once, and what the check finds reaches it as updates, as for any change at the provider. A client that switches folders often, as Apple Mail does, would otherwise wait on the provider every time.

Functions

func ApplyFlags

func ApplyFlags(flags []imap.Flag, flagOp imap.StoreFlagsOp, change []imap.Flag) []imap.Flag

ApplyFlags returns flags after a STORE. \Recent is the server's.

func BodyStructure

func BodyStructure(raw []byte) imap.BodyStructure

BodyStructure describes raw.

func CanonicalName

func CanonicalName(name string) string

CanonicalName spells INBOX in capitals (RFC 9051 section 5.1).

func DecodeHeader

func DecodeHeader(v string) string

DecodeHeader decodes RFC 2047 encoded words in a header value.

func DecodePart

func DecodePart(body []byte, encoding string) ([]byte, error)

DecodePart decodes a part's body with its Content-Transfer-Encoding.

func EmailID

func EmailID(key string) string

EmailID is the JMAP ID of the email with cache key key.

func HasAttr

func HasAttr(attrs []imap.MailboxAttr, a imap.MailboxAttr) bool

func HasFlag

func HasFlag(flags []imap.Flag, f imap.Flag) bool

func Match

func Match(m MatchMessage, raw []byte, c *imap.SearchCriteria) bool

Match reports whether a message matches c. raw is the message clients see; it is needed only if NeedsBody(c). The matching follows go-imap's imapmemserver (MIT licensed).

func NeedsBody

func NeedsBody(c *imap.SearchCriteria) bool

NeedsBody reports whether c looks at anything but flags, dates and numbers.

func NonNil

func NonNil(flags []imap.Flag) []imap.Flag

NonNil returns flags, or an empty list for nil.

func PartEncoding

func PartEncoding(bs imap.BodyStructure, part []int) (string, bool)

PartEncoding returns the Content-Transfer-Encoding of a part in a body structure, and whether the part is there.

func Preview

func Preview(raw []byte) string

Preview returns the start of a message's text, whitespace collapsed: the first text/plain part that is not an attachment, or else the first text/html part without its markup.

func SetOpHook

func SetOpHook(h func(kinds []string) error)

SetOpHook makes h run before each group of queued changes goes to the provider, with their kinds ("flags", "copy", "move", "expunge"). An error from h counts as the provider's. nil removes the hook.

func WithAuthResult added in v0.0.5

func WithAuthResult(ctx context.Context, r AuthResult) context.Context

WithAuthResult returns ctx carrying r, for Transform tests.

Types

type Account

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

Account is one provider Account.

func (*Account) Append

func (a *Account) Append(name string, data []byte, opts *imap.AppendOptions) (*Message, error)

Append stores a message in a folder at the provider, after the Transform turns it into the message the provider stores, and syncs the folder. It returns the message clients see, or nil if the provider did not say which it is.

func (*Account) AuthState added in v0.0.5

func (a *Account) AuthState() AuthState

AuthState returns the account's stamp state.

func (*Account) Backfilling

func (a *Account) Backfilling() bool

Backfilling reports whether a backfill runs.

func (*Account) Capacity added in v0.0.4

func (a *Account) Capacity() int

Capacity returns the requests the account's provider takes at once now, 0 for no known limit.

func (*Account) Changed

func (a *Account) Changed() <-chan struct{}

Changed returns a channel that is closed at the next change (lock held).

func (*Account) Changes

func (a *Account) Changes(kind, since string, max int) (*Changes, error)

Changes returns what changed in objects of kind (KindEmail, KindMailbox) since state, at most max (0 for no limit). It returns ErrCannotCalculateChanges for a state it cannot answer from (lock held).

func (*Account) Copy

func (a *Account) Copy(msgs []*Message, in, dest string) ([]Copied, uint32, error)

Copy copies msgs to folder dest. Messages no longer in folder in are skipped; with in "", those expunged are. It returns the copies, and the UIDVALIDITY of dest.

func (*Account) CreateFolder

func (a *Account) CreateFolder(name string, opts *imap.CreateOptions) error

func (*Account) DeleteFolder

func (a *Account) DeleteFolder(name string) error

func (*Account) Delim

func (a *Account) Delim() rune

Delim returns the hierarchy delimiter (lock held).

func (*Account) Download

func (a *Account) Download(msgs []*Message, st *FetchStats) (map[*Message][]byte, error)

Download returns the messages clients see, from the cache or by fetching and transforming them. A message the provider no longer has is missing from the result. st, if not nil, counts where they came from. It counts as a client's read (RecordReads).

func (*Account) Email

func (a *Account) Email(id string) []*Message

Email returns the messages clients see that make the email with JMAP ID id (EmailID): the same message in one or more folders. nil if there are none (lock held).

func (*Account) EmailIDs

func (a *Account) EmailIDs() []string

EmailIDs returns every email's JMAP ID, in no order (lock held).

func (*Account) Expunge

func (a *Account) Expunge(m *Mailbox, gone func(*Message) bool) []*Message

Expunge removes the messages of folder m that gone reports true for, and returns them. gone runs with the lock held.

func (*Account) HasPassword

func (a *Account) HasPassword() bool

HasPassword reports whether the account has a password the provider accepted.

func (*Account) HeaderEntries

func (a *Account) HeaderEntries(msgs []*Message, st *FetchStats) (out map[*Message]*HeaderEntry, rest []*Message, err error)

HeaderEntries finds the header entries of msgs: in the Store, from their cached copies (whose entries are then saved), or, for messages the proxy does not change, from the provider, which is asked for the entries alone. The messages left to answer from whole messages (downloaded and transformed, which saves their entries) are returned as rest.

func (*Account) Held

func (a *Account) Held() int

Held returns how many messages of the account are held, for tests.

func (*Account) HighestModSeq

func (a *Account) HighestModSeq(m *Mailbox) uint64

HighestModSeq is a folder's HIGHESTMODSEQ for CONDSTORE clients (lock held): the highest ModSeq of its messages, or the folder's own last change (a message added or expunged), whichever is higher. It rises with every change in the folder.

func (*Account) Lock

func (a *Account) Lock()

Lock takes the account lock, which the read accessors need.

func (*Account) Login

func (a *Account) Login(password string) error

Login checks the credentials against the provider, unless the provider already accepted the same password for this account. The first login after a restart, or after a password change, always goes to the provider. It returns ErrNoAccount, an error that wraps ErrLoginRejected, or the error that kept it from the provider.

func (*Account) Mailbox

func (a *Account) Mailbox(name string) *Mailbox

Mailbox returns a folder, or nil (lock held).

func (*Account) MailboxByID

func (a *Account) MailboxByID(id string) *Mailbox

MailboxByID finds a folder by its JMAP ID, or nil (lock held).

func (*Account) MailboxFor

func (a *Account) MailboxFor(name string) (*Mailbox, error)

MailboxFor returns the folder, checking it at the provider if its state is older than selectFresh: before returning for a folder never synced (or if SelectWaits), in the background otherwise. It returns ErrNoMailbox for a folder that does not exist or cannot hold messages, or the provider's error for one never synced.

func (*Account) Mailboxes

func (a *Account) Mailboxes() []*Mailbox

Mailboxes returns every folder, in no order (lock held).

func (*Account) Message

func (a *Account) Message(id uuid.UUID) *Message

Message returns a message by its ID, or nil (lock held). An expunged message the provider has not expunged yet has Box "".

func (*Account) ModSeq

func (a *Account) ModSeq(msg *Message) uint64

ModSeq is a message's mod-sequence for IMAP CONDSTORE clients (lock held): its email's last change in the log. A change to any copy of the email counts, so a copy's ModSeq may rise with no change of its own, which CONDSTORE allows. It is at least 1.

func (*Account) Move

func (a *Account) Move(m *Mailbox, in func(*Message) bool, dest string) ([]Copied, uint32, error)

Move moves the messages of folder m that in reports true for to folder dest, lowest UID first. in runs with the lock held. It returns the moved messages (From and To are the same message), and the UIDVALIDITY of dest.

func (*Account) NoteRead

func (a *Account) NoteRead(name string, uids []imap.UID)

NoteRead tells read-ahead that a client read messages of a folder.

func (*Account) Pieces

func (a *Account) Pieces(msgs []*Message, secs []Section, encodings func(*Message, Section) string, st *FetchStats) (map[*Message]map[string][]byte, error)

Pieces gets sections of direct messages: from the cache, or from the provider, which is asked for just them, and they are cached. A message the provider cannot give pieces of (a JMAP provider, a queued copy) is left out of the result: the caller reads it whole. Sections are the same for every message, except their Encoding, which encodings gives by message (for Binary sections; "" when unknown).

func (*Account) Prefetch

func (a *Account) Prefetch(msgs []*Message, background bool) error

Prefetch downloads msgs into the cache, as the engine does ahead of clients; background as read-ahead and the backfill do.

func (*Account) Queued

func (a *Account) Queued() int

Queued returns how many changes wait for the provider.

func (*Account) RecordReads added in v0.0.3

func (a *Account) RecordReads(msgs []*Message)

RecordReads records that a client read msgs whole: the backfill downloads a message read within the Store's Prune age again if the cache loses it. Download records its own; a caller that reads the Cache directly records them here.

func (*Account) RenameFolder

func (a *Account) RenameFolder(name, newName string, opts *imap.RenameOptions) error

RenameFolder renames a folder and the folders under it. Their messages keep their UIDs.

func (*Account) Running

func (a *Account) Running() bool

Running reports whether the account's engine runs.

func (*Account) SearchAtProvider

func (a *Account) SearchAtProvider(msgs []*Message, c *imap.SearchCriteria) (map[*Message]bool, error)

SearchAtProvider asks the provider which of msgs match c, for messages the proxy does not change: the provider has the same text. It returns ErrNoSearch if the provider cannot search; then the caller reads the messages.

func (*Account) Send

func (a *Account) Send(ctx context.Context, from string, to []string, data []byte) error

Send sends a message through the account's provider, for providers that send without a submission server (JMAP, ActiveSync). It needs credentials the provider accepted (Login). An ActiveSync provider keeps a copy in the Sent folder; a JMAP provider does not.

func (*Account) SetCredentials

func (a *Account) SetCredentials(password string, u Upstream)

SetCredentials records credentials as if the provider accepted them, and starts the engine.

func (*Account) SetFlags

func (a *Account) SetFlags(msgs []*Message, in string, flagOp imap.StoreFlagsOp, flags []imap.Flag, source *imapserver.SessionTracker)

SetFlags changes the flags of msgs and queues the change. Messages no longer in folder in are skipped; with in "", those expunged are. Every view but source hears of the change through its mailbox tracker; source, if not nil, reports it itself.

func (*Account) Since

func (a *Account) Since() time.Time

Since returns the account's cutoff: messages that arrived at or after it are new. Zero before the account's first sync.

func (*Account) State

func (a *Account) State() string

State returns the account's state: it changes whenever anything a client sees does (lock held).

func (*Account) String

func (a *Account) String() string

func (*Account) Subscribe

func (a *Account) Subscribe(name string, on bool) error

func (*Account) Sweeps

func (a *Account) Sweeps() int

Sweeps returns how many full syncs of every folder completed.

func (*Account) SyncNow

func (a *Account) SyncNow(name string) error

SyncNow brings a folder up to date with the provider.

func (*Account) Unlock

func (a *Account) Unlock()

Unlock releases the account lock.

func (*Account) User

func (a *Account) User() string

User returns the login name, lowercased.

type AuthAction added in v0.0.5

type AuthAction string

AuthAction is what happens to a new message.

const (
	AuthNone   AuthAction = ""       // shown
	AuthJunk   AuthAction = "junk"   // moved to the folder with \Junk
	AuthTrash  AuthAction = "trash"  // moved to the folder with \Trash
	AuthDelete AuthAction = "delete" // deleted at the provider
)

type AuthMode added in v0.0.5

type AuthMode string

AuthMode is how hard the engine acts on the provider's verdicts.

const (
	// AuthStandard follows the DMARC policy (reject deletes, quarantine
	// junks). Without a DMARC pass, SPF fail deletes and softfail junks.
	AuthStandard AuthMode = "standard"
	// AuthStrict deletes any SPF fail or softfail, and junks any DMARC
	// failure.
	AuthStrict AuthMode = "strict"
)

type AuthResult added in v0.0.5

type AuthResult struct {
	Status                  AuthStatus
	From                    string // lowercased; "" unless exactly one
	DKIMAligned, SPFAligned bool
	DMARC, DKIM, SPF        string // as reported; "" if none
	DKIMDomains             []string
	SPFDomain               string
	DMARCPolicy             string // on failure; "" if unreadable
	Rescued                 bool   // moved back by the user
	Learning                bool   // stamp not known yet
	// New marks mail not yet shown, which ReadJunk and the others can move
	// away. Otherwise they act as ReadRetry.
	New    bool
	Reason string // for logs
}

AuthResult is the verdict on one message.

func AuthResultFrom added in v0.0.5

func AuthResultFrom(ctx context.Context) (r AuthResult, ok bool)

AuthResultFrom returns the verdict on the message being read, if any.

type AuthState added in v0.0.5

type AuthState struct {
	Stamp []string // trusted names; empty while learning
	// Candidates are names on authShare of a full window that are not
	// learned for want of senders: the provider stamps its verdicts, but
	// its mail comes from too few domains to tell it from a forger's.
	Candidates  []string
	Examined    int
	LastStamped time.Time
}

AuthState is what the engine knows of an account's provider stamp.

type AuthStatus added in v0.0.5

type AuthStatus string

AuthStatus is the verdict on a message.

const (
	AuthPass     AuthStatus = "pass" // or rescued by the user
	AuthFail     AuthStatus = "fail"
	AuthNoPolicy AuthStatus = "none"    // no DMARC record, or permerror
	AuthUnknown  AuthStatus = "unknown" // no usable verdict
)

type Cache

type Cache struct {
	Dir      string // cache_dir
	MaxBytes int64  // memory bound
	// Plain stores chunks uncompressed, for a file system that compresses
	// them. It applies to new chunk databases: one keeps what it was made
	// with.
	Plain bool
	// The free space to leave on Dir's file system (see Trim): a
	// percentage of it, or bytes, whichever is more. Zero for neither.
	MinFreePercent float64
	MinFreeBytes   int64
	Log            *slog.Logger // for Trim; nil for none
	// Reads, if not nil, keeps the clients' reads (the Store): Trim forgets
	// those of the messages it deletes, so the backfill does not download
	// them again.
	Reads TrimReads
	// contains filtered or unexported fields
}

Cache holds messages as clients see them, so each is fetched and transformed once. Entries are keyed by message fingerprint (fingerprintKey), not by folder and UID, so a message keeps its entry when it moves, and a copy in two folders shares one.

Entries live in memory (least recently used out first) and, if Dir is set, on disk in the account's chunk database (chunkstore.go), where what messages share is stored once. The disk store is not encrypted: with a Transform that decrypts, protect Dir like the private keys.

func (*Cache) Close added in v0.0.2

func (c *Cache) Close() error

Close closes the chunk databases.

func (*Cache) Delete

func (c *Cache) Delete(account, key string) error

Delete removes a message, and its pieces, from memory and disk.

func (*Cache) DropNonFinal

func (c *Cache) DropNonFinal()

DropNonFinal forgets the results Read gave as ReadRetry, so they are read again.

func (*Cache) Get

func (c *Cache) Get(account, key string) []byte

Get returns a cached message, or nil. A read counts as use for Prune.

func (*Cache) Has

func (c *Cache) Has(account, key string) bool

Has reports whether a message is cached, without reading it.

func (*Cache) Prune

func (c *Cache) Prune(maxAge time.Duration) (int, error)

Prune deletes the messages on disk not read for maxAge, and returns how many.

func (*Cache) Put

func (c *Cache) Put(account, key string, data []byte, persist bool) error

Put stores a message. persist is false for results that may change later, such as a Transform result that is not final: those stay in memory only.

func (*Cache) Trim

func (c *Cache) Trim() (int, error)

Trim deletes the least recently read messages on disk until the free space is back above the minimum, and returns how many it deleted. A message frees only the chunks no other message has.

type Changes

type Changes struct {
	Created, Updated, Destroyed []string
	// CountsOnly says every update changed folder counts alone.
	CountsOnly bool
	// NewState is the state the changes lead to; with HasMore, a state
	// before the current one, to ask from next.
	NewState string
	HasMore  bool
}

Changes is what changed since a state.

type Copied

type Copied struct {
	From, To *Message
	// FromUID and ToUID are the UIDs before and after.
	FromUID, ToUID imap.UID
}

Copied is a message and its copy, or a message before and after a move.

type Engine

type Engine struct {
	// Route picks the provider for a login name. ok is false for a login
	// the engine does not serve, which is then rejected.
	Route   func(login string) (u Upstream, ok bool)
	Timeout time.Duration // per upstream operation
	// Transform changes messages between the provider and clients; nil
	// serves them unchanged.
	Transform Transform
	// Authentication is the sender authentication mode; "" is off.
	Authentication AuthMode
	// LookupTXT overrides DNS for DMARC lookups in tests.
	LookupTXT func(ctx context.Context, name string) ([]string, error)

	// Authorize, if set, runs after each login; an error refuses it.
	Authorize func(a *Account) error
	Cache     *Cache
	// Store keeps the mailbox state on disk across restarts; nil keeps it
	// in memory only.
	Store *Store
	// Backfill fills every folder's message list in the background after
	// a login (backfill.go). It needs a Store.
	Backfill bool
	Log      *slog.Logger
	// Name and Version identify the proxy to providers that ask (the IMAP
	// ID command).
	Name, Version string
	// contains filtered or unexported fields
}

Engine holds the accounts.

func (*Engine) Account

func (b *Engine) Account(login string) *Account

Account returns the account for login, loading its saved state.

func (*Engine) Close

func (b *Engine) Close()

Close ends every account's engine, drops every password, and waits for the read times to be written.

func (*Engine) Downloading

func (b *Engine) Downloading(key string) bool

Downloading reports whether the message with cache key key is being downloaded.

func (*Engine) Preload added in v0.0.4

func (b *Engine) Preload()

Preload loads, in the background, the saved state of every account that has some and that a route still covers, so a client's first login after a start does not wait for it. An account whose folder is named by a hash is loaded at its first login instead.

func (*Engine) Recheck

func (b *Engine) Recheck()

Recheck drops the ReadRetry results from the memory cache, so they go through Read again, and reads every held message again, in the background. It is safe to call from any goroutine at any time.

type FetchStats

type FetchStats struct {
	Memory, Disk int // from the cache
	Provider     int // fetched by this request
	Waited       int // fetched by another request, waited for
	Headers      int // answered from header entries
	Pieces       int // answered from pieces (piece.go)
}

FetchStats says where a FETCH got its messages, for the fetch trace.

type HeaderEntry

type HeaderEntry struct {
	Header    []byte
	Size      int64
	Structure imap.BodyStructure
	// Preview is the start of the message's text, for JMAP; nil if
	// unknown (an entry the provider gave without the body).
	Preview *string
}

HeaderEntry is what clients fill a message list with, kept in the Store so a client's list is answered without reading whole messages: the header, the size and the structure (which the header does not give). For a message the proxy does not change (message.direct) the provider is asked for these alone. For any other, they come from the changed message, when it is transformed or read from the disk cache.

func EntryOf

func EntryOf(raw []byte) *HeaderEntry

EntryOf makes the header entry of a whole message.

type Mailbox

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

Mailbox is the proxy's copy of one folder.

func (*Mailbox) Attrs

func (m *Mailbox) Attrs() []imap.MailboxAttr

func (*Mailbox) Delim

func (m *Mailbox) Delim() rune

func (*Mailbox) Flags

func (m *Mailbox) Flags() []imap.Flag

func (*Mailbox) ID

func (m *Mailbox) ID() string

ID returns the folder's JMAP ID, which it keeps when renamed.

func (*Mailbox) Index

func (m *Mailbox) Index(uid imap.UID) (int, bool)

Index finds a message by UID in Msgs.

func (*Mailbox) Msgs

func (m *Mailbox) Msgs() []*Message

Msgs returns the messages clients see, by UID. A sequence number is the index plus one. The caller must not change the slice.

func (*Mailbox) Name

func (m *Mailbox) Name() string

func (*Mailbox) PermanentFlags

func (m *Mailbox) PermanentFlags() []imap.Flag

func (*Mailbox) Selectable

func (m *Mailbox) Selectable() bool

func (*Mailbox) Subscribed

func (m *Mailbox) Subscribed() bool

func (*Mailbox) Synced

func (m *Mailbox) Synced() time.Time

Synced returns when the folder was last synced; zero if never.

func (*Mailbox) Tracker

func (m *Mailbox) Tracker() *imapserver.MailboxTracker

func (*Mailbox) UIDNext

func (m *Mailbox) UIDNext() imap.UID

func (*Mailbox) UIDValidity

func (m *Mailbox) UIDValidity() uint32

type MatchMessage

type MatchMessage struct {
	Seq   uint32 // sequence number in the client's view
	UID   imap.UID
	Flags []imap.Flag
	Date  time.Time // internal date
}

MatchMessage is what a search knows of a message besides its contents.

type Message

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

func (*Message) AtProvider

func (msg *Message) AtProvider() (string, imap.UID)

AtProvider returns where the provider has the message: a folder and its UID there, or "" and 0 (lock held).

func (*Message) Box

func (msg *Message) Box() string

func (*Message) Date

func (msg *Message) Date() time.Time

func (*Message) Direct

func (msg *Message) Direct() bool

Direct says the Transform leaves the message as the provider has it.

func (*Message) Flags

func (msg *Message) Flags() []imap.Flag

func (*Message) ID

func (msg *Message) ID() uuid.UUID

func (*Message) Key

func (msg *Message) Key() string

Key is the message's cache key. Copies of a message share it.

func (*Message) UID

func (msg *Message) UID() imap.UID

type ReadStatus

type ReadStatus int

ReadStatus says what to do with the result of Transform.Read.

const (
	// ReadFinal: the result is what clients see from now on. It is
	// cached on disk.
	ReadFinal ReadStatus = iota
	// ReadRetry: clients see the result, but a later Read may give
	// another (a key that is missing now). It is kept in memory only,
	// until Engine.Recheck.
	ReadRetry
	// ReadHold: clients must not see the message yet. A new message (one
	// that arrived at or after Account.Since) is held: clients do not see
	// it at all until a later Read gives ReadFinal or ReadRetry. It is
	// tried again with every sweep and on Engine.Recheck. For an older
	// message, ReadHold is ReadRetry.
	ReadHold
	// ReadJunk, ReadTrash and ReadDelete move new mail away; for older
	// mail they act as ReadRetry.
	ReadJunk
	ReadTrash
	ReadDelete
	// ReadDefer is ReadJunk, released once the stamp is known.
	ReadDefer
)

type Section

type Section struct {
	Binary    bool
	Part      []int
	Specifier imap.PartSpecifier
	Fields    []string // HEADER.FIELDS, or with NotFields HEADER.FIELDS.NOT
	NotFields bool
	Partial   *imap.SectionPartial
	// Encoding is the part's Content-Transfer-Encoding, for a Binary
	// section from a provider without BINARY.
	Encoding string
}

Section is a piece of a message: a body section, or with Binary the decoded content of a part.

func (Section) String

func (s Section) String() string

String names the section as IMAP does, with its partial range.

type Store

type Store struct {
	Dir string // cache_dir
	// contains filtered or unexported fields
}

Store keeps each account's mailbox state on disk, in one SQLite database per account: folders, messages with the proxy's IDs and UIDs and where the provider has them, the queue of changes not yet made at the provider, the header entries clients fill message lists with (headerEntry), and when clients last read each message, so a lost cache can be filled again. Never whole messages: those are in the Cache. It lets the proxy answer clients right after a restart, before the sync has caught up with the provider, and finish the queue after a shutdown.

Each account has its own database, so accounts never wait on each other's writes, and an account is removed by deleting its folder (accountDir).

func OpenStore

func OpenStore(dir string) (*Store, error)

OpenStore returns the store for the databases under dir.

func (*Store) Close

func (s *Store) Close() error

func (*Store) ForgetReads added in v0.0.3

func (s *Store) ForgetReads(dir string, keys []string) error

ForgetReads deletes the records of reads of keys in the account folder dir: Trim deleted their messages (Cache.Reads), and the backfill must not download them again.

func (*Store) HeaderKeys

func (s *Store) HeaderKeys(account string) (map[string]bool, error)

HeaderKeys returns the cache keys with a header entry.

func (*Store) Headers

func (s *Store) Headers(account string, keys []string) map[string]*HeaderEntry

Headers returns the header entries saved for keys. A read counts as use for prune.

func (*Store) NoteReadsIn added in v0.0.3

func (s *Store) NoteReadsIn(dir string, keys []string) error

NoteReadsIn records that a client read keys now, in the account folder dir: Trim kept their messages, which a client read meanwhile, after ForgetReads.

func (*Store) Prune

func (s *Store) Prune(maxAge time.Duration) (int, error)

Prune deletes header entries not read for maxAge, and records of reads older than that, in the databases open now. It returns how many header entries it deleted. Others are pruned when they are opened.

func (*Store) ReadKeys added in v0.0.3

func (s *Store) ReadKeys(account string) (map[string]bool, error)

ReadKeys returns the cache keys of the messages a client read whole.

type Transform

type Transform interface {
	// Fields lists the header fields Changes needs. The sync fetches them
	// for every message, with Message-ID.
	Fields() []string
	// Changes reports, from a message's Fields, whether Read changes it.
	// A message it does not change is served as the provider has it,
	// without downloading it whole.
	Changes(h textproto.MIMEHeader) bool
	// Read turns a message from the provider into the one clients see,
	// and says how sure the result is (ReadStatus).
	Read(ctx context.Context, raw []byte) (out []byte, status ReadStatus)
	// Write turns a message a client stores with APPEND (a Sent copy, a
	// draft) into the one the provider stores. An error refuses the
	// APPEND.
	Write(ctx context.Context, raw []byte) ([]byte, error)
}

Transform changes messages between the provider and clients, for example to decrypt mail on its way to clients and encrypt what they store. Without one, the proxy serves every message as the provider has it.

type TrimReads added in v0.0.3

type TrimReads interface {
	ForgetReads(dir string, keys []string) error
	NoteReadsIn(dir string, keys []string) error
}

TrimReads is where Trim forgets the clients' reads of the messages it deletes, by account folder (the Store).

type Upstream

type Upstream struct {
	Addr      string // IMAP: host:port
	TLS       string // IMAP: config.TLSImplicit, TLSStartTLS or TLSNone
	TLSConfig *tls.Config
	Timeout   time.Duration
	// Sources are where connections to the provider may leave from:
	// interface names or local IP addresses. IMAP keeps a count and a
	// limit for each; HTTP connections take them in turn. None: wherever
	// routing says.
	Sources []string
	// URL is a JMAP provider's session resource, such as
	// https://example.com/.well-known/jmap. It makes the provider JMAP.
	URL string
	// Bearer sends the client's password to a JMAP provider as a Bearer
	// token (an API token), not with Basic auth.
	Bearer bool
	// ActiveSync is an Exchange ActiveSync provider's endpoint, such as
	// https://example.com/Microsoft-Server-ActiveSync. It makes the
	// provider ActiveSync.
	ActiveSync string
	// MaxConns caps the connections (IMAP) or the requests at a time
	// (JMAP, ActiveSync) to the provider, counting the engine's own. 0:
	// no cap but the provider's, which an IMAP provider shows by refusing
	// a connection and a JMAP one gives in its session.
	MaxConns int
}

Upstream is a provider's server: IMAP (Addr), JMAP (URL), or ActiveSync (ActiveSync).

func (Upstream) String

func (u Upstream) String() string

String names the provider, for logs and for the state saved for it.

Jump to

Keyboard shortcuts

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