- Go 73.5%
- JavaScript 18.5%
- Python 2.9%
- HTML 2.7%
- CSS 2.2%
| .forgejo/workflows | ||
| cmd | ||
| docs | ||
| internal | ||
| packaging/aur/davar-bin | ||
| playwright | ||
| release-notes | ||
| scripts | ||
| tools | ||
| .gitignore | ||
| .golangci.yml | ||
| .pre-commit-config.yaml | ||
| AGENTS.md | ||
| CHANGELIST.md | ||
| CODE_REVIEW.md | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| Makefile | ||
| package-lock.json | ||
| package.json | ||
| playwright.config.js | ||
| README.md | ||
| requirements_and_spec.md | ||
| REVIEW.md | ||
| SECURITY.md | ||
| TESTS.md | ||
| TODO.md | ||
| WEB_UI_DESIGN.md | ||
davard
Davar is a local authenticated mail daemon and CLI. The current implementation
supports IMAP/SMTP account configuration and connection checks, incremental
mail synchronization after an initial full-history download, reading locally stored raw MIME, message listing filters,
and SMTP delivery. davard also serves an embedded htmx browser UI
on the same HTTP listener; folder SSE powers live browser updates. systemd
stop/status management remains unimplemented. davarctl service systemd enable installs, enables, and starts
the per-user unit. See the API contract for current REST
behavior and TODO.md for remaining work.
For agentic development, start with AGENTS.md and the development guide. The requirements are the original proposal; older decisions, plans and milestone reports are historical snapshots and may describe superseded behavior.
SQLite performance and diagnostics
Davar keeps one SQLite connection with WAL and full durability. Bounded planner statistics maintenance runs at startup and daily. Folder listings use saved effective membership/unread projections after initialization, with exact fallback while initializing. Counts change transactionally; full verification runs daily. FTS work uses a durable pending queue and resumable old-cache backfill, so an already indexed cache does not require idle full-message scans.
davard --slow-operation-threshold 100ms
# More sensitive warnings and cumulative histogram/pool diagnostics:
davard --debug --slow-operation-threshold 25ms
# Disable slow-operation warnings (normal request logs remain):
davard --slow-operation-threshold 0
The threshold defaults to 100ms; negative durations are rejected. Warnings show
an operation name, SQL fingerprint, request/account IDs where available, total
elapsed time, driver execution/iteration time, connection acquisition time,
transaction commit time, result row count and outcome. SQL, bind values and mail
content are excluded. Query elapsed time includes time a caller holds results;
transaction elapsed time includes application work while holding the connection.
Acquisition timing is available for direct store calls; canceled acquisitions
that never enter the driver appear in pool wait statistics, not SQL summaries.
--debug emits cumulative bounded histogram p50/p95/p99 upper bounds and pool
wait totals once per minute. These bucket estimates are not benchmark percentiles.
Every browser/REST response supplies a generated X-Request-ID for correlation.
See the baseline and tuning report for repeatable synthetic measurements and remaining profiling work.
First use
Start the daemon, then initialize from another terminal:
davard
davarctl init
davarctl status
davarctl init creates full-access and read-only users atomically, prints both
keys exactly once over the same-UID Unix socket, and saves the full-access key
to the private client config. Keep both keys in a password manager. Rotate
either key with davarctl user full-access rotate_password or
davarctl user read-only rotate_password. davarctl user list shows existing
built-in users and roles. Full-access rotation updates
the active client config; read-only rotation leaves it alone. If a full-access
key is lost, create a fresh gmd_<16 lowercase hex>_<base64url secret> key
using a cryptographically secure random source, save it privately, run
davarctl key hash with that key on stdin, stop the daemon, back up its
database, apply the printed SQL, update the private client config, and restart.
The command hashes the supplied key; it does not retrieve an existing key.
An older initialized database creates its built-in reader on its first
full-access read-only-user rotation without changing the full-access key.
The daemon stores its master key in
$XDG_CONFIG_HOME/davar/daemon.json (default ~/.config/davar/daemon.json);
the CLI stores its API key in $XDG_CONFIG_HOME/davar/client.json (default
~/.config/davar/client.json). Both are mode 0600 in a mode 0700 directory.
Legacy default configs under gomail or gomaild are moved on first use when safe. SQLite
state remains under XDG state and raw blobs under XDG data. Use --host, --config, and --db on davard to
override startup settings. Client host/key settings follow CLI, environment,
client JSON config, and compiled default precedence.
Use davard --help and davarctl --help for the aligned command and global
flag references. -d/--debug enables debug-level diagnostics without logging
API keys or credential request bodies. Normal logging includes account creation,
sync queue/start/completion, folder download progress (once per 25-UID batch),
listener readiness, and delivery outcomes. Unchanged cached-folder passes,
configuration paths, client HTTP traces and repeated connectivity waits require
debug mode. Retry warnings and errors remain visible without it.
Generate shell support with
davarctl completion bash or davarctl completion zsh. davarctl skill
prints an agent-ready Markdown skill document to stdout.
The davar-bin AUR package installs both completions system-wide under
/usr/share/bash-completion/completions and /usr/share/zsh/site-functions.
davarctl requests plain-text API responses by default; use --output json
to request and print the REST JSON representation unchanged. The API selects
JSON or plain text from the request's Accept header. JSON request bodies keep
Content-Type: application/json.
Account and folder IDs remain in their documented positions, and global client
options such as --output can be placed before or after a command. Bash and zsh
completion use the same command and flag definitions as CLI parsing and help.
Run davarctl spec > openapi.json to export the daemon's OpenAPI 3.1 JSON
document. The spec command always prints JSON and requires the configured API
key. The same document is available at authenticated GET /api/v1/spec.
doctor combines several API checks, so it requests structured data internally
and formats the aggregate according to --output.
Embedded browser UI
A shared banner spans every browser app: the Davar logo, the workspace switcher, the search pill, and the sign-out control always appear in the same place. Mail is the default workspace; Contacts keeps the same banner without a top search pill - the directory search pill lives in the Contacts list pane below its icon bar. The workspace switcher stays immediately right of the logo; at widths of 1024px or less workspace and creation labels collapse to icons but keep accessible names, and the Mail search pill moves to its own full-width row so the switcher is never centered or pushed to the right edge. A Calendar switch sits between Mail and Tasks, opens /calendar, and starts a local, principal-owned calendar requiring no email account. It uses the shared header search pill for literal event search and a Mail-style sidebar with calendar visibility, colors, default-calendar selection and local calendar add/edit/delete rules. 3-day, week, month and agenda views render all-day, multi-day, overlapping, weekend and recurring events; elapsed time is shaded through the current time, and timed events can be dragged to another slot. Full-access users can quick-create or fully edit events in a modal, duplicate them into another writable local calendar, soft-delete with a 30-second Undo, and manage local calendars; read-only users can browse and search without mutation controls. Calendar visibility, default view, week start and the 30/60-minute default meeting duration are browser-local preferences. Dated task markers are shown from Davar Tasks and open the task in Tasks. Google Calendar, CalDAV, ICS, invitations, reminders and dated-task overlays are not connected in this local-first release. The Tasks switch follows Calendar and opens /tasks, a local, principal-owned workspace that requires no email account. All, Current, task-list and Trash views share Mail’s responsive three-pane layout: collapsible list navigation (with a persistent reopen control), task rows and task details. With no task selected, the details pane shows the Davar logo and Tasks shortcuts; list editing opens explicitly through List settings. Tasks search lives in the shared upper search pill. The Mail-style iconbar offers alphabetical A–Z/Z–A and due-date ordering (undated last), plus incomplete/completed/all and advanced filters. Full-access users can create/edit lists and tasks, move tasks between lists, edit metadata, complete/reopen, star, trash/restore and permanently delete with confirmation; read-only users can browse without mutation controls. Search is literal and combines with completion, status, priority, star, tag and date-range filters, supported sort orders and bounded cursor pagination. Browser-local versioned preferences remember task filters/sort and pane dimensions without storing task content or keys. Keyboard users can use / for task search, lowercase a for quick add, lowercase x to star/unstar the focused task, Delete to trash a selected task, j/k or Up/Down arrows to move between tasks, and h/l or Left/Right arrows to move between panes. Escape closes details or returns to the task list on narrow screens. Task/list changes are revision-checked and broadcast through a principal-scoped event stream; refreshes preserve unsaved edits, focus and scroll. Current date-only comparisons use the daemon host's local time zone; date-times use their explicit RFC3339 offset. Task writes reject start-after-due and mixed DATE/date-time start/due pairs; both endpoints of each date range must use the same type. Current retains an independent overdue predicate for legacy/imported rows despite the strict public-write invariant. The banner’s New contact action opens the existing contact form; Contacts shortcuts are n new, i import, and e export the active selection. Export combines selected contacts in one .vcf download (up to 1,000 IDs and 8 MiB total); a member error rejects the entire download. Re-clicking the sole selected contact clears selection and restores the logo/shortcut pane. The Tasks banner’s New task action uses material-symbols-light:add-task and focuses existing quick add; it is disabled in Trash and read-only sessions. Both creation actions sit immediately before Sign out at the banner’s right edge. The Compose button belongs to Mail only and is never shown in Contacts; it shows the mdi:email-edit-outline icon before its visible label, and at narrow widths the label is hidden while the icon-only control keeps a 40px hit target, the (n) Compose tooltip, and the Compose accessible name.
Contacts opens a two-pane directory: the left pane lists discovered contacts and the right pane shows the selected person's structured name, repeatable email addresses and phones, organization, title, birthday, notes, and observation dates. The list pane's visible header is replaced by a restrained icon bar above the directory search pill: New Contact (iconmind:user-add-duotone-thin), Import Contacts (iconmind:data-import-user-duotone-thin, wired to the hidden vCard file input), an Order menu with Alphabetical and Newest first, Move to Trash for the selected contact, Export vCard (iconmind:data-export-user-outline-thin), enabled only while exactly one contact is selected), and a filter menu with a text-sized trash icon for Show trashed contacts; the heading remains available to assistive technology as a visually-hidden heading. Clicking a row selects it in place without reloading the list, applies the highlight immediately, loads the detail through a fragment request, and updates the URL with pushState; Back/Forward restore the selection, detail, and scroll. Ctrl-click toggles a row, Shift-click selects a range, Ctrl+Enter toggles the focused row, and Shift/Ctrl+ArrowUp/Down extend or add through the list, mirroring the message list. A multi-selection leaves the detail pane empty with the Contacts shortcut legend, and Move to Trash (toolbar or right-click) applies to every selected contact, bounded and with partial-failure reporting; Delete permanently keeps its confirmation and applies to the selection, while Restore stays per row. The list loads further pages incrementally as it scrolls near the bottom, with a server-rendered Load more control as the no-JS/keyboard fallback. Before a contact is selected, the detail pane shows the Davar mark and only the shortcuts that work in Contacts - focus the directory search (Ctrl+K or /), open Settings (S), switch back to Mail (M), and show help (?) - and never advertises Mail-only actions such as Compose. Full-access users can add a person, edit the structured fields, add or remove repeatable values, and move a person to Trash; updates use the displayed revision and report stale edits instead of overwriting newer changes. The Trash view lists soft-deleted contacts with a Restore control per row; its toolbar action becomes Delete permanently and requires confirmation, and right-clicking a row offers the same Move to Trash / Restore / Delete permanently actions. Contact details omit the Move to Trash and vCard export buttons; both are list-toolbar actions now, and the detail Edit action is icon-only (material-symbols-light:person-edit-outline). Contacts can be exported as a structured vCard, and vCard 3.0/4.0 files can be imported with per-card counts, stable-UID/email matching, no implicit person merges, and preserved unknown properties. A card that matches only a contact currently in Trash is reported as failed with an explicit message rather than being silently skipped; restore or permanently delete that contact first. Contacts also supports a PNG/JPEG photo (normalized server-side, maximum 2 MiB and 20 megapixels) and a bottom pane bar with Settings, Offline, and the running version. When keys were collected from the contact's mail, the detail pane also lists each retained public key in a collapsed disclosure with its fingerprint, an Unverified label, Key ID, User IDs, observation dates, and the bounded public armor; key text is escaped and never treated as markup or trusted, and no trust, verification, or decryption controls are offered.
In the message list, the Filter menu offers Unread and Has attachment; selecting an active option again clears it. The Message options (ellipsis) menu in the reader includes Mark as unread. A sender's name and address form one control: clicking it opens a contact bubble showing the contact's name and email, with a link to the full contact record. A conversation that contains a single message is always expanded. Press b to collapse or expand the Mail sidebar.
The htmx mail UI is embedded in davard. Start the daemon normally and
open http://127.0.0.1:20114/mail in a browser. There is no separate web executable,
listener, configuration file or process. davard --host sets the listener used
by both browser and REST clients:
davard
davard --host localhost:20114
davard --help
Sign in with an existing Davar API key generated by davarctl init (also saved
in the private client configuration). The form sends the key once to the daemon;
it remains in server-side session memory and is never echoed into HTML, URLs,
browser storage or logs. Sessions expire after 12 hours; sign in again after
expiry, logout, daemon restart, a full-access key rotation, or an unauthorized
(401) browser request. A late unauthorized response from an older tab or
in-flight prefetch does not clear a newly established browser session. All
account access and permissions follow the
supplied key, and revocation/rotation takes effect on subsequent reads. Browser
views accept local loopback connections and use HttpOnly/Strict cookies;
JSON REST clients continue to authenticate with Authorization: Bearer. Key verification admits at most four Argon2 checks at once; concurrent requests wait for capacity instead of receiving a false 401.
Calendar
Calendar is a local-first calendar workspace at /calendar (shortcut A). It requires no mail account. The first visit creates a writable Personal calendar and opens Week view with Monday as the first day. Add local calendars from the sidebar; each has a name, color and optional IANA time zone. Visibility, default view and week start are stored in browser preferences. The default calendar cannot be deleted, and a populated calendar must be emptied or have its events moved before deletion.
The 3-day view shows three days; Week, Month and Agenda are also available. 3-day, week and month views show timed, all-day, multi-day, overlapping and recurring events. Weekends use a distinct surface, elapsed time is shaded through the current time, and dragging an empty slot creates an event for that range. Dragging a non-recurring timed event to another slot moves it; dragging its top/bottom bars resizes its start/end, with a live time-box preview and 15-minute snapping. Escape cancels a gesture. Recurring and all-day events are changed from the editor. Dated task markers open the task in Tasks and are never duplicated in Calendar. Click an empty time slot or New event to open the modal; the editor includes collapsed Repeat controls, separate start/end rows with independent time zones, and status, availability and visibility. Empty Location, Link and Description fields are collapsed; populated fields open automatically. New browser events default to Private, without changing existing events or REST defaults. The banner hosts the period group immediately left of the view selector as < Today [date range] >: previous, Today, the live range text, then next. The range text is hidden below 1301px so the controls keep room; Today and the arrows remain. Collapsing the sidebar retains a two-letter calendar icon rail with visibility controls; the collapse/expand control stays in the sidebar's top-right corner and is the first (only) icon in the collapsed rail. Clicking a meeting's title text opens the edit form directly (full-access), while clicking elsewhere on its block opens the read-only details. New event stays beside Sign out in the banner. Settings > Calendar sets the default 30/60-minute meeting duration (60 minutes by default). Right-click or press Shift+F10 on an event for Open, Edit, Duplicate and Delete. Duplicate opens a prefilled independent event with the same title, a new UID and a destination calendar. Delete removes the event immediately and offers Undo for 30 seconds. Recurring events support daily, weekly, monthly and yearly intervals with an occurrence count or until date; P0 edits and deletes the whole series. d selects 3 days, w Week, m Month and a Agenda (four distinct view shortcuts); A still switches to the Calendar workspace.
Search is a literal case-insensitive match over title, description, location and calendar name. The shared search pill shows results as an Agenda list; clear it to restore the calendar. Google Calendar, CalDAV, ICS import/export/subscriptions, mail invitations, browser reminders are not connected in this local-first release.
DAV REST setup (partial; no sync-runtime or UI/CLI support): the authenticated REST API can create disabled CalDAV/CardDAV connections, test unsaved read-only discovery, persist discovered collections unselected, enable/disable connections, and explicitly select supported component views. Selecting CalDAV views creates empty remote-backed Calendar/Tasks containers; it does not upload local items. Account secrets are encrypted and omitted from API responses. DAV scheduling, automatic/background sync, full conflict recovery, and CLI/Web setup remain unimplemented. DAV is a fresh-install feature: no legacy migration or compatibility conversion is provided. The database schema is initialized from one complete definition; a database created by an older incompatible build is rejected at startup rather than converted. See the API v1 contract for route details and the DAV requirements/technical specification for the full design, which is not yet implemented.
Install as a desktop app (PWA)
davard publishes an installable web app manifest at
http://localhost:20114/manifest.webmanifest and 192/512 px PNG icons under
/static/icons/. Open the UI in Chrome or Chromium, sign in, and choose
Install app from the address bar or browser menu. The installed app opens in
a standalone window without tabs or an address bar and uses the same cookie
session and API permissions as the browser UI. The manifest and icons are public
and contain no account data; installing the app never grants mail access by
itself.
The AUR package installs a Davar Mail launcher and the davar-web helper.
davar-web opens the UI as a standalone app window in the first
Chromium-family browser it finds (Chrome, Chromium, Brave, Edge, or Vivaldi).
Select a different browser with DAVAR_BROWSER=/path/to/browser or by writing
its executable name to $XDG_CONFIG_HOME/davar/browser (default
~/.config/davar/browser). Set DAVAR_WEB_URL when the daemon listens on a
non-default address:
DAVAR_BROWSER=chromium DAVAR_WEB_URL=http://localhost:20114/mail davar-web
This integration has no service worker, so the installed window still needs a
running davard, and offline reading or queued mutations are not available. A
different packaging setup can ship its own .desktop file with Exec=davar-web;
see packaging/aur/davar-bin/davar-mail.desktop.
Mail opens at /mail; the old / entry remains compatible. Link to a folder
with /mail?account=1&folder=2, or to a selected message with
/mail?account=1&folder=2&message=3 (replace these local IDs with your own).
Navigation updates the address bar, and reload/Back/Forward preserve the view;
the legacy /message path has been removed and returns 404; use /mail for message deep links. The logo is no longer a navigation
link: activating it animates the gazelle mark, respecting reduced-motion settings.
The installed PWA also starts at /mail.
The homepage opens directly into the three-pane mail client. The left sidebar
shows every accessible account expanded by default; account and folder rows
can be collapsed or expanded. An account with stored sync or Outbox trouble shows a yellow warning glyph immediately left of its name; hovering or focusing it reveals the bounded incoming (IMAP) and outgoing (SMTP) messages taken only from the cached automatic-sync error and the latest unfinished Outbox failure, without any provider probe. Read-only sessions see the same warning, since a read-only grant is not an account problem. Each account shows Inbox, a virtual Unread view, a virtual Outbox
(viewing queued SMTP submissions), Sent, Drafts, Starred, Spam and Trash in
that order when present, followed by
an expandable Folders group containing the remaining folders alphabetically.
The Outbox row shows a .folder-count-style badge with the number of unfinished
submissions for that account, kept out of the unread/hover-unread semantics and
refreshed with the existing count/status events. Right-clicking it offers the
bounded Send all messages and Move to Trash actions described with the
Outbox reader below. Browsing messages, reader
Back/Forward, list refreshes, and count/status refreshes preserve the sidebar's
scroll position, open account/folder trees, Folders expansion, focused element,
selection highlight, and collapsed state.
Unread combines cached unread messages from Inbox and custom folders, with
cursor paging and each message's real source folder retained for actions. The
Unread row shows the distinct unread count. Click Folders to expand or collapse its tree. Right-click a folder to mark its messages and descendants read or to queue a folder sync; mark-read acceptance snapshots the unread memberships and descendant unread counters clear while the worker runs. Right-click a custom folder to choose Do not sync or Sync folder: the choice applies to the folder and its current and future subtree, keeps cached messages readable, and deletes nothing on the provider. Folders excluded by that policy show the material-symbols-light:folder-off-outline icon, while Sync now remains the explicit single-folder refresh. Right-click any eligible real, selectable folder to toggle Always mark as read (a checked menuitemcheckbox): enabling marks current unread mail read locally and via a provider \Seen mutation, and automatic sync reads later arrivals in that folder without duplicating queued work; disabling only stops future marking and never restores unread. Enabled custom folders show material-symbols-light:folder-check-outline (sync-excluded and role icons take precedence). Read-only users see the control disabled. System, provider special-use, and virtual folders do not offer the custom policy control. The menu always reflects the effective inherited state from the folder API rather than the displayed folder name. Right-click a virtual Unread entry (unified or per account) or the unified Starred entry for Mark all messages as read: the whole matching view is snapshotted across every page, stars and other flags are preserved, and accounts the session may not modify are reported instead of silently skipped. An account's Starred is its provider folder, so right-clicking it keeps the existing per-folder mark-read. Custom folders can be renamed, reparented by dragging onto another folder or the Folders root, and given a newly created child folder. Folder changes enter the durable account queue and show a pending marker while the provider is offline or working. Drag opens a confirmation dialog, updates the tree immediately, and keeps the current folder selected by stable local ID. Right-click Folders to create a root folder in a modal. For display only, exactly one leading Folders/ is omitted from names; provider paths used by routing, search, and storage remain unchanged. The Folders root stays collapsed after creating a folder and has no separate disclosure chevron. Custom folder labels show only the leaf segment (for example b for a/b), while routing and stored provider paths retain the full path. Separator lines divide the unified virtual folders from account trees and adjacent accounts, including while the sidebar is collapsed. Collapsing the sidebar keeps account trees expanded at their first level, leaves Folders itself collapsed, keeps the collapsed state while messages are opened, represents a selected custom folder with the selected Folders glyph, and shows unread counts as small badges on collapsed folder icons. Drafts, Sent, and Trash never show an unread badge, because an unread count there is working state rather than an inbox signal. Folder hover details now show Name, Account, message count, and unread count. The lower sidebar shows Settings at left and the version at right; there is no
connection indicator. Settings opens in a Mail tab with Display, Privacy, and
Composer sections (System/Light/Dark starting in System), an empty Calendar tab, and a keyboard-accessible
Contacts tab stores validated CardDAV configuration (server URL, username, an
encrypted password, and an enabled switch) without contacting the server; full
CardDAV sync is not implemented yet. A clear-search icon returns to the current
folder.
The account add/edit form has Mail and Identities tabs; Test appears only on Mail. Identities lists the account's sending addresses as one-column staged rows; additions, edits and removals are applied when you Save, new accounts prefill the account email, and the backend still refuses to delete the final identity. Full-access users share one account add/edit form for adding an account or editing
its properties. It collects the email, the shared username and password, and one
Servers configuration section covering the incoming (IMAP) and outgoing
(SMTP) endpoints. Each endpoint has its own required port immediately beside its
own encryption control, and the SMTP port, encryption, and certificate always
stay SMTP-specific even when the hostname is shared. SMTP hostname and credentials
are always configurable; there is no separate-host checkbox. Username follows
email, SMTP hostname follows the incoming hostname, and SMTP username follows
the incoming username until customized; independent stored overrides are preserved.
Editing prefills safe values only: stored passwords are never sent to the browser,
and blank password fields retain current secrets (the edit-mode password
placeholder is the fixed masked ******************, never an instruction, and
the SMTP password field stays placeholder-free). Use incoming credentials
explicitly resets SMTP credentials server-side without revealing stored passwords.
Both forms use Save; Cancel, Test, and Save sit as one right-aligned group with Save last. The SMTP username and SMTP password fields have no placeholder shadows (the outgoing credentials default to the incoming ones until customized). Test checks the
current unsaved IMAP/SMTP settings without saving, syncing, or sending mail;
separate results are cleared when the form changes. Creation still follows the
existing API behavior: Save persists the account, checks connectivity, and queues
sync only when both checks pass; a failed connectivity check leaves the account
saved. Each endpoint's Custom CA policy offers an
upload control for a public PEM certificate or bundle (at most 32 KiB per
endpoint; public PEM certificates only, no private keys or client-certificate
authentication); the control appears only for the Custom CA policy, the browser
bounds the file and the server validates every block. Choosing Custom CA requires
a valid upload unless the account already has valid stored material; an unchanged
edit preserves its stored CA and pin without requiring re-upload. A bundle supplied while another policy is
selected is never attached, so it cannot persist latent trust material. A pin policy stays preserved on edit but cannot be newly selected here.
Save errors keep the typed credentials and never echo certificate or secret
material. All validation, provider defaults, and credential merge semantics stay
in the REST and CLI contracts, and the form never writes secrets into URLs, logs,
or browser storage. Full-access users open the
form from the Add account icon in the sidebar's upper-left corner or by
right-clicking an account header and choosing Edit Properties. The same
account menu offers Sync now, which queues the existing account sync
contract, and Remove account, which asks Are you sure you want to remove
account? before deleting only the local account through the REST contract.
While a queued or running sync is followed, the account header shows the same
busy spinner and accessible label. The browser reads the accepted operation from
the REST response (a duplicate folder sync is reported as the already-scheduled
operation instead of an error) and polls the account's or folder's own status
endpoint until that scope reaches a terminal state (there is no cancel control
for a queued sync, and a cancelled provider run is requeued as pending, so
the spinner keeps running while that work is still queued). Polling never starts a sync
of its own, so nothing extra runs on the provider, and the account header
follows whichever operation is actually active in the account while a folder
row stays scoped to its own folder.
Removal never deletes provider mail, resets cached data, or signs out another
account; a failed removal keeps the account, the open confirmation error, and
the current mail selection.
Outbox rows open submission status, recipients, retry timing, and safe failure
details rather than the normal message reader. Their deep links use
/outbox?account=1&submission=2; submission IDs are distinct from message IDs.
Selecting a row does not mark mail read. Send message retries an eligible
submission (failed or waiting to retry, never SMTP-accepted); Edit and
Move to Trash are available only before SMTP acceptance and in eligible
queue states. Right-clicking the sidebar Outbox row offers Send all messages
(retry only terminal failed submissions) and Move to Trash (confirmation,
then cancel every cancel-eligible unfinished submission while skipping
sending/accepted rows); both are bounded, CSRF-protected, and disabled for
read-only sessions. Unknown SMTP outcomes retain their duplicate-risk warning;
a Failed state is not silently retried by the UI.
Full-access users can Compose, Reply, Forward, mark messages unread, star or unstar selected messages, and move
selected messages to Trash, Spam/Junk, or another folder in the same account. Message actions validate and enter the daemon's durable mutation journal atomically, return an operation ID, and update cached views from desired-state overlays while provider work runs. Bulk actions apply to at most the first 100 selected messages in list order; any remaining selection is unchanged. Move actions remove rows immediately while the provider request runs; failures show the reason and reload the current list. Completed moves and deletes show a three-second bottom snackbar. Destructive move/delete requests are delayed for three seconds so Undo can cancel them; Undo restores the optimistic row state and prevents the database request. Sends and draft saves are not undoable and show a disabled Undo control. The center list supports Ctrl-click
and Shift-click selection, or Ctrl+Enter to toggle the focused message without
marking it read; its toolbar offers newest/oldest/unread-first order and an
attachment filter. Active filter and order controls show a highlighted icon and a hover label. Read/unread actions update the list immediately and persist in the background; failures restore the prior state. Hovering over a folder shows its total and unread counts (zero when empty, never a dash); Inbox uses title case, the Folders root identifies itself, and virtual Unread does not repeat an identical Unread count. When the sidebar is collapsed, a selected custom folder is represented by the selected Folders glyph, its hover reveals the real folder, and unread counts appear as small badges on folder icons. Counts are saved in the daemon and update transactionally with local message changes, including queued actions. Existing caches initialize in bounded background batches; reads use exact local counts until ready. Count-change events refresh the sidebar without new-mail notifications; when a count-only signal arrives the visible list is reconciled too, so an arrival missed during a reconnect still appears. The browser keeps one session-wide event stream for the page, so switching folders or opening a reader no longer interrupts arrivals. Read, star, move, trash and delete actions use each row's own account, which makes unified Starred work across accounts. Open message menus dismiss on the first outside click, including a click inside the message frame. Inline cid: images load through an authenticated same-origin route that serves raster image parts only; unresolved references stay blocked and remote media keeps its existing consent rules. Temporary sign-in service failures show a retry message and preserve an existing session. Hovering over a message shows the account first, then sender, To and Cc only when present, subject, date, source folder, human-readable size, and the first 200 characters of its body without marking it read. Plain-text bodies linkify URLs, email addresses, and phone numbers; a body with no readable text renders without a placeholder message. Right-clicking empty space in the message list offers Sync now for the current folder; a queued or running folder sync shows a busy spinner with an accessible label on that folder row, scoped to that folder's own status record so it never follows or clears another folder's work; the folder hover bubble also shows total and unread counts. Right-clicking a message offers read/unread, Trash, Spam,
moving to a folder, deleting, and opening in a new window. Capital D permanently deletes the focused message and lowercase t moves it to Trash; capital T switches to Tasks. Lowercase x uses the existing Star/Unstar action on the Mail selection, including multiple messages; it does nothing without a selection and never intercepts typing. The permanent-delete provider operation remains a documented limitation. Right-clicking Trash offers Empty Trash and right-clicking Spam offers Delete all. The move picker shows an expandable folder tree with icons and filters folders locally; its Create folder control remains disabled. Starred messages show a yellow star below the date in the list and reader; starring or unstarring updates the star immediately and queues a background Starred-folder sync so that view catches up. Starring from another folder also inserts the message into the visible Starred list immediately; the row remains pending until the folder sync confirms it. Unstarring in Starred immediately removes that row and restores it if saving fails. Spam is
unavailable when the account has no Spam/Junk folder. Drag the focused message or the current selection onto a sidebar folder to move it. Dropping on Trash runs the trash action, Spam runs the spam action, Starred stars the messages, and other folders move them. Clicking the already-open message or thread row again clears the selection and returns the reader to the shortcut list. Restore actions use the material-symbols-light:restore-from-trash icon; the reader no longer duplicates the list's Trash action. Folder icons and all browser toolbar/menu glyphs render from the shared icon catalog, including the composer and message context menu. The
compact Compose popup offers a single From selector containing all configured identities grouped by account (accounts and identities are sorted deterministically; identity labels include the parent account email when an account has multiple identities), To, optional Cc/Bcc fields
(revealed by the CC/BCC controls), Subject, and Body. Subject and Body may be
empty; a subject is limited to 998 characters. At least one To, Cc, or
Bcc recipient is required to send; saved drafts may omit recipients. The send
control and Send options toggle are disabled while validation fails. Closing a
compose window with content saves a draft, including one with no recipient;
the bottom-left Trash button discards without saving. The attachment button
accepts multiple files, shows the selected files, and enforces the Settings
Compose → Max attachments size total (default 1000 MB). Recipient fields accept
comma-separated addresses; empty optional Cc/Bcc fields do not add recipients. As you type a recipient, the composer offers a grey inline ghost completion for the current token (the text after the last comma) drawn only from your contacts directory, never from message headers. It appears after three characters, only when the caret is at the end of an enabled field with no selection and the token has no comma; Tab replaces just that token with the completed Name <address> and appends , and a space, while Escape dismisses the ghost without closing the composer. Hidden Cc/Bcc fields and read-only sessions never fetch or show suggestions, suggestions are debounced and stale responses never paint, and ordinary typing, recipient parsing, reply/forward prefill, Ctrl+Enter, and the existing shortcuts are unchanged. Reply addresses the sender and quotes the original
message body; Forward quotes the original message and focuses To. In the
open composer, Ctrl+Enter uses the same validation and send path as Send;
the shortcut appears in the Send tooltip and does not change message-list
Ctrl+Enter selection. Repeated keys or an in-flight send do not enqueue duplicates.
The compose heading follows the subject, showing Subject: … up to 50 characters; the header can expand to full screen or minimize to its title bar. The composer is 20% wider than the previous 520px layout. Rich/plain mode is a single toggle at the left of the framed formatting toolbar; the formatting controls sit to its right, use Unavailable in plain text tooltips in plain mode, and the empty body has no placeholder text. The body has internal padding and the compose backdrop is not blurred. Settings labels the section Composer and offers dd/mm/yyyy or mm/dd/yyyy date formatting under Display. Compose is a non-modal floating window, so the mailbox remains usable while it is open. The send control uses the send icon; its adjacent menu shows a disabled Scheduled send placeholder pending a scheduling design. Closing with Escape or the close button saves the draft immediately and closes the composer; the draft appears optimistically in Drafts and is saved in the background. A successful send closes the composer without a Queued banner or Outbox link.
Opening an unread message updates
the list immediately and marks it read in the background; already-read messages
do not cause a write. Message-list dates show only the time for today, dd Mon for dates in the current year, and the configured dd/mm/yyyy or mm/dd/yyyy form for older messages. The reader shows the full date with year and time. The details chevron omits empty To/Cc rows and includes SPF, DKIM, and DMARC evidence as Pass, Failed, or Unverified. Locally checked DKIM signatures show their domain, selector, and failure reason; provider-reported SPF/DMARC are marked Unverified unless their source is trusted. Loaded email images expose their original URL as a hover title. Opening an unread row in a Unread view marks it read and decrements the unread count immediately, but the row stays visible with read styling until another message is selected, so the list does not reflow under the reader; the retained row is released when another message is selected or the view changes. Re-clicking the open conversation or message clears the selection and reader in place, without reloading the page. The reader keeps To/Cc on one truncated line with a
chevron that opens a bubble containing the full From, To, Cc, date, and subject. The browser keeps a bounded, in-memory reader cache and
refreshes cached entries in the background. After a fresh sign-in lands on Mail,
it also warms that cache once for the first 20 messages of the unified Unread
view (idle-started, at most three concurrent reader-fragment requests, aborted on
navigation, read-only, and without any mark-read or provider sync). Folder lists use the same bounded,
in-memory cache, so revisiting a folder shows its messages immediately while
the current list loads in the background. Mail content is not stored in browser storage.
Send reports durable queue acceptance, not delivery. Unsent text stays in page
memory and is not saved as a draft; closing Compose discards it. Read-only users
see a Read Only badge and disabled write actions.
The daemon uses the standard Accept header to choose a representation. Browsers
prefer text/html; Accept: application/json selects the existing JSON REST
representation, and text/plain preserves CLI formatting. Supported shared
HTML resources include account list/detail/status/search, folder list/status,
message list and the readable message view. Quality values are respected; JSON
wins a tie with HTML, and an API request with no Accept header keeps its JSON
default. Raw mail, attachment downloads and the OpenAPI document keep their
fixed representations. At /, explicit JSON requests return authenticated daemon
status while browser requests show the mail client after sign-in.
The daemon and client global help also describe the shared listener and Accept
behavior.
For example, with an API key supplied through the existing environment variable:
curl -H 'Accept: application/json' -H "Authorization: Bearer $DAVARCTL_KEY" \
http://localhost:20114/api/v1/account
curl -H 'Accept: text/html' -H "Authorization: Bearer $DAVARCTL_KEY" \
http://localhost:20114/api/v1/account
curl -H 'Accept: application/json' -H "Authorization: Bearer $DAVARCTL_KEY" \
http://localhost:20114/
Use Settings → Display or press S to choose System, Light, or Dark; System
follows the operating system's color preference and is the default. The HTML
message reader follows
the resolved theme, including its background, text, and link colors, and the
composer's From identity selector uses the theme surface in both light and dark
instead of a white control. On desktop, navigate in the
collapsible account/folder tree, select messages in the center list, and read
them in the right pane. Drag the divider or focus it and use the left/right
arrow keys to resize the list and reader. Smaller screens keep the folder drawer and switch
between the list and reader as space requires; its toggle remains inside the
left pane. The reader print icon prints the currently rendered email content.
Page Up and Page Down scroll the focused reader by a page.
Lists default to 50 newest
messages (limit=1 through limit=100 in browser list URLs), with
read/unread/flag/attachment filters, newest/oldest/unread-first sorting in the browser (and
date/unread sorting through the REST API) and cursor-based Previous/Next pages.
Previous/Next controls appear only when the folder has more than one page.
The search pill accepts plain subject/body text or account-scoped DSL fields:
account:fixture@example.test in:"Archive/2026" from:sender@example.test to:recipient@example.test subject:"Quarterly report" attachment:yes after:2026-10-19T00:00:00+03:00.
account: narrows the search to a matching account; in: accepts a full
folder name or a unique leaf name, so in:C matches /A/B/C. Hovering the
pill shows a grid of these fields; autocomplete is planned. A missing folder
returns an empty result view without a folder-not-found error, and selecting
the current folder clears the search.
Fields combine with AND. in: includes that exact folder and descendants using
the provider's hierarchy delimiter; from: and to: match whole addresses
case-insensitively (to: checks To recipients, including a recipient among
others); subject: matches contained text. Field names are case-insensitive,
and quoted values may contain spaces. Hover the pill for syntax help. Results
highlight matching text in yellow in rows and visible message headers. body:
searches the SQLite FTS5 body index; subject and body are indexed from decoded
plain text (or derived HTML text), with attachments and scripts excluded. The
index is derived from local MIME and can be rebuilt with
davarctl search reindex. attachment:yes/no filters attachment state, while attachment:pdf and attachment:docs match PDF or document attachments; after:
and before: accept RFC 3339 timestamps with an offset (after is inclusive,
before is exclusive). The older REST search endpoint also supports
sender substring, an inclusive received-date bound, and an exact folder name;
criteria combine with AND. The reader displays escaped decoded text and
separate attachment downloads. Back returns to the original list or search.
Downloads use the configured 1 GiB attachment bound.
The browser’s top-level Unread view uses the global
GET /api/v1/account/folder/unread endpoint and shows unread mail from all
accessible Inbox and custom folders. A global Starred view uses
GET /api/v1/account/folder/starred. Each account retains its own Unread
virtual entry below its Inbox; the older per-account unread endpoint is still
available for account-scoped callers. Right-click either unified view entry, or
an account's virtual Unread entry, to mark the whole matching view read;
acceptance snapshots every page and every account the session can modify, keeps
stars and other flags, and reports accounts it cannot modify. The composer defaults to rich text
with LTR/RTL alignment, bold, italic, underline, strikethrough, and ordered or
unordered lists. It sends sanitized HTML with a generated plain-text fallback in
multipart/alternative; switching to plain text disables the formatting tools.
The “Only non-tracked” media option loads direct image resource URLs. If an image URL has a query string or fragment, it is stripped and the resulting direct URL is loaded when its path has a supported image extension.
It accepts limit, cursor, and sort=date|date_asc; each result includes the
actual folder_id for subsequent message reads and actions.
While a folder is open, the UI subscribes to an authenticated event stream for all accessible accounts. A newly committed message refreshes sidebar unread counts, and one in the open folder also refreshes the visible list. Reconnects start a fresh subscription and do not replay missed events. The browser's native notification permission prompt appears on the first interaction with the signed-in client; there is no Davar prompt in front of it. Notifications report only unread messages that arrive after the folder's first completed synchronization; initial-sync history, a rebuilt UIDVALIDITY, replayed cache entries, read messages, Sent copies, and count updates never notify, though they still refresh the view. Before a popup the client confirms the message's current effective cached read state, so a read that is already queued or applied locally also stays silent. Copies of one message cached in several folders notify once, and the UI remembers what it already announced across tabs and reloads.
Offline mode is a global daemon status. While offline, automatic IMAP synchronization, provider mutation execution, and Outbox delivery pause; reads continue from the local cache, composed mail stays in the durable Outbox, and accepted message mutations remain in the shared durable journal until the daemon resumes. The browser reports browser connectivity loss separately from this explicit daemon setting: navigator.onLine is only a hint about the browser's network connection, not proof of external Internet reachability, and browser disconnection does not stop a daemon that still has provider connectivity. On browser recovery, the UI refreshes authoritative visible list/count state and reconnects its event stream without changing explicit daemon offline mode. Browser mutations require a reachable daemon; a network failure is an error and is never reported as accepted. Legacy browser-local queued actions are detected and left untouched for review; they are not replayed and their stored CSRF values are never sent.
The Outbox list refreshes automatically (bounded five-second catch-up plus operation/reconnect updates), without a Refresh button. Ctrl/Meta/Shift and keyboard selection match normal lists; Trash cancels eligible selected submissions, retains failures, and refreshes the sidebar count. The folder action Move all messages to trash skips sending/accepted submissions and cannot retract delivered mail. Reply, Reply All and Forward icons live only in the message details pane. The list toolbar retains bulk actions and shows the distinct underlying message count at its right when a selection exists, including grouped conversations. Message/conversation rows share Contacts’ hover and selected-background treatment.
The Settings modal has Mail, Calendar (default view, week start and meeting duration), and Contacts tabs that are keyboard accessible (arrow keys move between them) and remember the last tab in the versioned UI preference store. The Mail tab keeps Display theme choices, a Privacy control for remote media, and a Compose control for the total attachment-size limit (default 1000 MB; minimum 0). Options use a fixed label column and wider controls. Never is the default and leaves per-message consent in the viewer. Always loads remote media and hides the banner, even for senders previously blocked. The tracking-stripped choice is disabled pending implementation. The Contacts tab collects the CardDAV server URL, username, password, and an enabled switch; Save CardDAV settings inside that form (not a tab-wide Save) validates the absolute http(s) URL and required username/password, encrypts the password with the daemon master key, and never returns it. A blank password preserves the stored one, and disabling keeps the secret for later. No CardDAV server is contacted. The browser stores only versioned UI preferences: theme, media policy, collapsed sidebar,
max attachment size, last settings tab, last accessible account/folder, per-folder filter/sort choices, and whether it
has requested notification permission. Real-folder context menus offer Disable notification on unread mail / Enable notification on unread mail; this browser-profile preference is scoped by principal, account and source folder, applies across tabs and unified views, and does not affect unread counts or mail synchronization. Search text is transient.
API keys, credentials, message bodies, attachments and cached mail are never
written to browser storage. Remembered external-content choices store exact
sender addresses in this browser. Use ↑/↓ or j/k to move through the list;
normal movement selects and opens the message immediately. Hold Shift to select
a range or Ctrl to add messages without marking them read.
Use →/l to enter the reader, ←/h to return to the list, and ↑/↓
or j/k to scroll the focused reader. Press ? for shortcut help. Uppercase O opens an existing Order by menu and uppercase F opens an existing Filter menu in Mail, Contacts and Tasks; these bindings do not add menus to views without them. Opening a message from virtual Unread keeps its Unread sidebar/list context, including on reload and background sidebar refresh.
Press n to compose, Ctrl+K or / to focus search, or S to open Settings. Hovering or focusing an app in the workspace switcher shows a bubble with its name and letter shortcut: (M) Mail, (C) Contacts, (T) Tasks, and (A) Calendar. Calendar uses n for a new event, t for today, d/w/m/a for views, arrows or h/j/k/l for navigation, and b for the calendar sidebar.
Press N to open the create-folder dialog for the selected custom folder (or the
account's folder root when a system or virtual folder is selected), and press C
to switch to Contacts. Press t to move a focused message to Trash and s to
move it to Spam; t
restores in Trash and s reverses Spam/Junk. With one message
focused in the list or reader, press u to toggle read/unread, f to forward,
r to reply all, R to reply, or t to move to Trash. Spam remains in the toolbar
and context menu. These letter shortcuts do not run while typing in a field or
while a dialog is open.
The empty reader also lists these shortcuts; ? opens the full help dialog.
Returning keyboard focus from the account/folder tree to the message list
resumes the previously opened or selected message when it is still present in
the current view, instead of jumping to the first row.
Folders whose current page contains repeated normalized subjects (leading
Re:, Fwd:, or Fw: stripped, case-insensitive) show conversation threads.
Each thread row shows the latest message, message count, unread state, and an
attachment indicator. Opening it marks all unread members read (bounded per
account/folder, payload capped at 100) and stacks the thread's messages
newest-first in the reader. The newest message always starts expanded even when
it is already read, and every unread message starts expanded; other read messages start collapsed. Every message can be expanded or collapsed individually. Load external on one HTML message applies to every HTML message
in that thread. Thread rows
can be dragged to a folder to move all of the thread's messages. Grouping is
subject-only and limited to the current list page. In Unread, matching
subjects are grouped across folders within each account; accounts stay separate.
Opening an Unread conversation includes read context from the account's other
folders, newest-first. Its row count and list actions apply to unread members on
the current page. Context loading is bounded to 100 messages and 2,000 search
candidates; exceeding either limit shows an error directing you to search.
Normal folder conversations remain scoped to that folder. Grouping does not use
reply-reference headers or stable provider thread IDs.
Embedded browser assets are served with Cache-Control: no-store so a rebuilt
daemon is not masked by a stale browser cache. Closing compose can save a draft;
richer draft editing and Outbox retry controls remain later work. HTML messages render in a sandboxed reader frame with message-provided scripts and active content removed. The viewer preserves allowed message structure and styling in light mode; dark mode keeps that structure with Davar colors, and the palette icon shows the original colors. Blocked external images show a disabled-picture icon; inline images show placeholders. Remote images and CSS background resources stay blocked until you choose Load external; remote stylesheets remain blocked. The original-styling view retains message inline CSS and class/id attributes inside the same sandboxed frame; treat that CSS as untrusted presentation. Image or link URLs containing embedded credentials are treated as non-remote and blocked. Checking Remember stores an allow or block choice for the exact sender address in this browser; leave it unchecked for a one-message choice. When remote media is blocked, the reader toolbar also offers an icons8:spy control immediately left of Print whose menu choices are Load tracked media (this message only) and Always load tracked media from this sender (the same per-browser sender allowlist as Remember). The control is pressed only when this message's own frame has actually loaded tracked media, so a deferred frame that was never opened stays unpressed; opening its menu, changing the theme, or reloading never requests remote media, an explicit choice applies tracked media even under Only non-tracked, and the one-shot choice is not remembered. External links open without a referrer. Blocked remote images expose their blocked URL in the hover tooltip. Attachments remain available from the reader toolbar; the duplicate attachment section below the message body has been removed. Calendar and vCard attachments also get a dedicated download control in the toolbar, ordered vCard then calendar before the blocked-media spy and Print: a single matching file is one icon button whose tooltip and accessible name are its filename, and several matching files share one menu whose entries each carry the filename tooltip. Calendar detection accepts text/calendar and application/ics media types with parameters and a case-insensitive .ics fallback; vCard detection accepts text/vcard and text/x-vcard plus a .vcf or .vcard fallback and never treats a nested message/rfc822 part as a vCard. Every control reuses the authenticated download route bound to the message's own account, folder, message, and part, so provider limits and content disposition are unchanged, and messages without matching attachments show no extra controls. Plain-text messages retain the text reader. See the web UI implementation handoff
and API contract for architecture and representation details.
Account configuration
Configure an account with explicit endpoint and credential flags:
davarctl account add \
--email person@example.com \
--password '...' \
--incoming-host imap.example.com --incoming-port 993 \
--outgoing-host smtp.example.com --outgoing-port 465
The username defaults to the email address, and the password applies to both
IMAP and SMTP. Use --username to override the shared username, or optional
--smtp-username and --smtp-password to override SMTP credentials. TLS and
sync settings have optional flags; run davarctl account add to see the full
list and which options are required.
Gmail
Enable 2-Step Verification and create an app password using Google’s app password instructions. Use that generated password with Davar. App passwords may be unavailable for some managed accounts or accounts with Advanced Protection.
davarctl account add \
--email '<account@gmail.com>' \
--password '<APP PASSWORD>' \
--incoming-host imap.gmail.com --incoming-port 993 \
--outgoing-host smtp.gmail.com --outgoing-port 587
Proton Mail through Bridge
Install and run Proton Mail Bridge, then sign in inside Bridge. Keep Bridge running on the same machine as Davar. Copy the IMAP/SMTP host, ports, username and generated password from Bridge’s account settings; the generated password is separate from your Proton login. Bridge exposes local IMAP/SMTP servers and normally uses a self-signed TLS certificate (connection details).
Export Bridge’s public certificate in PEM format. Check its names:
openssl x509 -in /path/to/bridge-cert.pem -noout -subject -ext subjectAltName
openssl x509 -in /path/to/bridge-cert.pem -noout -checkhost localhost
# If connecting to an IP address instead:
openssl x509 -in /path/to/bridge-cert.pem -noout -checkip 127.0.0.1
Use a Bridge host covered by the certificate. Import the certificate for both connections when adding the account (replace the port placeholders with the values shown in Bridge):
bridge_cert_pem=$(cat /path/to/bridge-cert.pem)
davarctl account add \
--email '<account@proton.me>' \
--username '<BRIDGE USERNAME>' --password '<BRIDGE PASSWORD>' \
--incoming-host localhost --incoming-port '<BRIDGE IMAP PORT>' \
--incoming-tls-certificate-policy custom_ca \
--incoming-tls-ca-cert-pem "$bridge_cert_pem" \
--outgoing-host localhost --outgoing-port '<BRIDGE SMTP PORT>' \
--outgoing-tls-certificate-policy custom_ca \
--outgoing-tls-ca-cert-pem "$bridge_cert_pem"
The PEM contents are stored in the account’s database configuration; Davar does
not depend on the certificate file afterward. TLS mode defaults to auto, which
uses implicit TLS on standard IMAP/SMTP ports (993/465) and attempts STARTTLS
on other ports. Auto mode is not general TLS protocol detection. For Bridge’s
SSL setting on a nonstandard port, set --incoming-tls-mode and
--outgoing-tls-mode explicitly to implicit; use starttls when Bridge
shows STARTTLS to require a successful upgrade.
If no certificate name matches the local Bridge host, use certificate pinning for both endpoints. Compute the SHA-256 of the certificate’s DER bytes:
openssl x509 -in /path/to/bridge-cert.pem -outform DER | sha256sum
Set --incoming-tls-certificate-policy pin --incoming-tls-pinned-sha256 HEX
and the corresponding --outgoing-... flags. Pinning verifies the exact
certificate and its validity period; it does not require a matching hostname.
Import the replacement certificate or update both pins if Bridge regenerates it.
After changing an existing account, run davarctl account <id> test, then
davarctl account <id> sync and inspect davarctl account <id> status.
Sync status
davarctl account list includes each account's last known sync status: not synced,
syncing, or synced; when sync fails, it shows the actual latest sync error.
Use davarctl account <id> status for operation details and the latest automatic
sync error. Account creation tests both providers immediately and queues sync
when they pass; if a test fails, the account is retained and the full provider
error is printed to stderr.
Use the returned account ID with davarctl account <id> test to repeat the
connection check and davarctl account <id> sync to queue another synchronization.
The command returns a durable incrementing integer operation ID; use
davarctl account <id> operation <operation-id> to inspect its state.
Run davarctl account <id> status to show the active or most recent sync
operation without supplying its operation ID. It also reports the last
automatic sync success and the latest automatic sync error, if any.
Message listings show the IMAP received date and are ordered newest first.
Previously cached messages without that date use their local cache timestamp.
Listings default to 20 messages. Pass --limit NUMBER to change the page size,
--limit 0 for all matching messages, or --cursor TOKEN to continue using the
returned next cursor. Text output is a table with ID, Sender, Date, Subject,
Size, Unread (Y/N), and Attachments (Y/N); Size shows the stored MIME length
rounded to whole binary units. The virtual unread listing adds a Folder
column because reads and actions must target each message's source folder. JSON
rows include size in bytes. JSON includes decoded headers and numbered attachment
metadata. Older cached mail is enriched from its existing local MIME blobs.
Each cached message also carries domain authentication evidence: independent
DKIM verification against the stored MIME and RFC 8601 provider
Authentication-Results, both persisted as derived data and exposed under
authentication in the message metadata response and via message <id> metadata in JSON output. Local DKIM verification and provider assertions stay
separate; a passing result means a domain authenticated message content, never
that the sender is a verified person or that the message is safe. Provider
results are untrusted unless the account sets
authentication.trusted_results.enabled, lists the receiving authserv_ids,
and asserts provenance_verified; generic IMAP and Proton Bridge default to
untrusted, are still retained for inspection, and never override locally
verified DKIM. Reanalyze existing mail from local blobs, without redownloading,
with davarctl security recheck [--account <id>] [--message <id>]. Inspect
aggregate analysis state with davarctl security status (or
GET /api/v1/security/status). It counts logical messages in accounts the
caller owns or that are shared with the caller by a mail.read grant;
account.read alone does not expose analysis counts. processing,
pending (including deferred retries), succeeded, and failed are mutually
exclusive and sum to total;
retrying is a subset of pending. activity is running only while
processing work exists, so it reports analysis activity rather than background
worker liveness. succeeded means analysis completed, not that DKIM, SPF, or
DMARC passed.
Inspect cached mail with davarctl account <id> folder INBOX message list; add
--filter read|unread|all|flagged|answered|deleted|seen|draft|has_attachment to filter
by cached IMAP flags (default: all). You can use the local folder ID instead
of its name, for example folder 1 message list. Use folder INBOX message <message-id> to print readable text, or append
delete to move the message to provider Trash. Local message IDs are incrementing
integers. Set or clear flags with the folder-scoped message flag action. For
example:
davarctl account <id> folder INBOX message <message-id> flag -flagged -answered -seen
Prefix a settable flag with - to clear it; read and unread set and clear
\\Seen. Create and delete folders with davarctl account <id> folder add Archive
and davarctl account <id> folder delete Archive; both return a durable
operation acceptance before provider completion. The response's operation_id
can be checked with GET /api/v1/account/<id>/mutation/<operation-id>.
List unread mail across the account with davarctl account <id> folder unread list. It reads the virtual Unread view: cached unread messages from selectable
Inbox and custom folders, excluding Sent, Drafts, Starred, Spam/Junk, Trash, and
nonselectable folders. It supports --limit (default 20; 0 means unlimited),
--cursor, and --sort date|date_asc; no provider sync is performed. Use the
Folder column or folder_id from --output json for later message reads and
actions.
davarctl account <id> folder unread list
davarctl account <id> folder unread list --sort date_asc --limit 0
davarctl account <id> folder unread list --limit 20 --cursor '<NEXT_CURSOR>'
A real folder literally named unread remains addressable with folder unread status, set, or message ...; only the list verb is the virtual view.
For several messages in one folder, the REST API accepts one collection PATCH:
curl -X PATCH -H "Authorization: Bearer $DAVARCTL_KEY" -H 'Content-Type: application/json' \
-d '{"action":"read","messages":[10,11]}' \
"http://localhost:20114/api/v1/account/<id>/folder/INBOX/message"
Use unread, star, unstar, trash, or spam for the other bulk actions. From Trash, use
restore to move mail to INBOX; from Spam/Junk, use not_spam for the same
destination. To move to a folder in the same account, use
{"action":"move","destination":"Archive","messages":[10,11]} (or use its
local folder ID). A failed operation reports which message IDs were already processed.
Folder names are stored and returned as exact Unicode text, including Hebrew,
right-to-left names, CJK, and supplementary characters. For example:
davarctl account <id> folder add 'פרויקטים/2026'
davarctl account <id> folder 'פרויקטים/2026' message list
The daemon converts names for IMAP automatically. The browser isolates each
name and message value by direction and wraps CJK text; dynamic text tables align by
display width. JSON, API text values, and SSE keep their original Unicode
without added direction marks. Davar uses IMAP modified UTF-7 and does not
negotiate UTF8=ACCEPT. MIME headers, text bodies, and RFC 2231
attachment names decode UTF-8 plus common Hebrew, Japanese, Chinese, and Korean
charsets. Unsupported charsets remain available in raw MIME and appear in
metadata warnings. The x/text decoders replace malformed legacy byte sequences
with U+FFFD; strict UTF-8 and US-ASCII validation remains in place.
Send plain-text mail with davarctl account <id> send [--from ADDRESS] --to ADDRESS --headline SUBJECT --body TEXT; --from selects one configured sender identity (and is rejected when it is not stored for that account). When omitted, the lowest stored identity ID is selected for backward compatibility; stored identities are created with the account email for new accounts. Add repeatable --to, --cc, --bcc, and --attach
flags for more recipients and files. Use --body-file PATH to read the plain-text body from a file;
exactly one of --body, --body-file, or --body-stdin may be used. A body
file supplies message text and is not sent as an attachment. --subject and
--body-stdin are also available. Add --draft to
save the composed message through IMAP in the first provider folder marked
\\Drafts instead of sending it through SMTP:
davarctl account <id> send --to person@example.com --headline "Hello" \
--body "A message from Davar."
davarctl account <id> send --from "Team <team@example.com>" \
--to person@example.com --headline "Hello" --body "A message from Davar."
davarctl account <id> send --to person@example.com --headline "Hello" \
--body-file ./message.txt
davarctl account <id> send --to person@example.com --headline "Report" \
--body "See attached." --attach ./report.pdf --attach ./summary.csv
davarctl account <id> send --to person@example.com --headline "Hello" \
--body "A draft to finish later." --draft
Successful draft creation prints Draft created. When the IMAP server returns
APPENDUID, Davar saves the draft in its local cache immediately, so it
appears in draft list without waiting for background sync. The draft commands
use the first cached folder marked \\Drafts by discovery order, including
localized folder names:
davarctl account <id> draft list [--limit 20] [--cursor TOKEN] [--sort date|unread]
davarctl account <id> draft <message-id>
davarctl account <id> draft <message-id> send
davarctl account <id> draft <message-id> delete
Draft lists default to 20 items; --limit 0 returns all cached drafts. Sending
uses the stored MIME, including HTML and attachments, and removes the saved
draft only after SMTP acceptance is durably recorded. Cleanup retries never
resubmit SMTP. Inspect the returned submission ID with outbox <submission-id>.
Use davarctl account <id> folder list to show the folder tree, then
davarctl account <id> folder INBOX message list to list cached messages.
Read one with davarctl account <id> folder INBOX message <message-id>, set or
clear flags with davarctl account <id> folder INBOX message <message-id> flag seen
or davarctl account <id> folder INBOX message <message-id> flag -flagged, and
move it to provider Trash with davarctl account <id> folder INBOX message <message-id> delete. Create or remove provider folders with
davarctl account <id> folder add Archive and
davarctl account <id> folder delete Archive.
Folder writes are durably accepted with HTTP 202 and a Location header; provider work runs in the account's FIFO mutation queue. To create a child folder, send POST /api/v1/account/<id>/folder with {"name":"Child","parent_folder_id":12}. To rename a folder leaf, PATCH /api/v1/account/<id>/folder/<folder-id> with {"name":"New leaf name"}. To move it under another folder, include its numeric parent_folder_id; null moves it to the root and omission preserves its parent. A parent-only move may omit name. These validations include cycles, collisions, special-use folders, ownership, and the provider delimiter before acceptance. Folder IDs and message memberships remain stable through rename and reparent. Add Idempotency-Key to a REST request to retry the exact intent safely; a changed intent with the same key returns 409.
To mark selected folders and their descendants read, keep the existing request shape: PATCH /api/v1/account/<id>/folder with {"action":"mark_read","folders":[12,13]}. Acceptance snapshots current unread memberships; later arrivals stay unread. The bounded worker handles large subtrees in batches. Folder create, rename/reparent, delete, and mark-read require mail.modify, except folder deletion, which requires mail.delete. Use the returned operation ID at GET /api/v1/account/<id>/mutation/<operation-id> to inspect progress and outcomes.
To make one real, selectable folder always mark new and existing unread mail read, use davarctl account <id> folder FOLDER set auto-read; set manual-read stops future automatic marking and never restores unread state. Enabling persists the preference by exact folder name (so provider folder re-discovery cannot drop it) and durably snapshots the folder's current unread mail for a provider \Seen mutation, returning the accepted operation body. Automatic sync then stores new arrivals in that folder read locally and queues a single snapshot per pass without per-message mutation spam. The preference is by name, so renaming a folder starts it disabled. Hidden, pending, and \Noselect folders are ineligible; virtual Unread/Starred views have no folder row.
Inspect cached folder metadata and sync state with
davarctl account <id> folder INBOX status (or select a folder by its local
ID). The response includes attributes, delimiter, cached total/unread counts,
sync setting, last successful folder sync, and a matching folder-specific
operation. Missing timestamps and operations are null. The command uses cached
data and does not send an IMAP STATUS request; folder list remains complete.
Update any account-add option with davarctl account <id> update --email new@example.com --incoming-host imap.new.example.com --password 'NEW APP PASSWORD'.
Only supplied fields change; --password rotates both IMAP and SMTP passwords
unless --smtp-password is also supplied. Repeatable folder rules and exclusions
replace their respective lists when supplied. Remove an account with
davarctl account delete <id>; this immediately deletes its local credentials,
mail, and cache, while leaving the provider mailbox unchanged.
CLI action examples
Search uses cached mail and combines all supplied criteria with AND. Supply
at least one of --subject, --sender, --since, or --folder. Subject and
sender use case-insensitive substring matching; --folder is an exact folder
name. --since is an inclusive received-date bound (YYYY-MM-DD in UTC, or
an RFC 3339 timestamp). Search supports the same --limit and --cursor flags.
Read decoded headers with message ID metadata, or list numbered file parts
with message ID attachment list. message ID download NUMBER saves the
decoded attachment to its sanitized filename in the current directory. Pass
--output-file PATH to choose a different path. Existing files are never
overwritten. Attachments are extracted from cached MIME, including offline.
Replace <id>, <folder>, and <message-id> with values from the list commands.
Account sending identities are manually configured mailbox addresses. Each account
starts with its account email; add another identity before deleting the last one.
Email normalization preserves the local part and applies IDNA/case normalization
to the domain. All examples use the default text output; prepend --output json
before the command for the REST JSON representation.
davarctl init
davarctl status
davarctl doctor
davarctl spec > openapi.json
davarctl skill > SKILL.md
davarctl completion bash > ~/.local/share/bash-completion/completions/davarctl
davarctl completion zsh > ~/.local/share/zsh/site-functions/_davarctl
davarctl service systemd enable
davarctl account list
davarctl account add --email me@example.com --password 'APP PASSWORD' \
--incoming-host imap.example.com --incoming-port 993 \
--outgoing-host smtp.example.com --outgoing-port 587
davarctl account <id> update --smtp-password 'NEW SMTP PASSWORD' --sync-poll-seconds 600
davarctl account <id> test
davarctl account <id> sync
davarctl account <id> status
davarctl account <id> operation <operation-id>
davarctl account <id> identity list
davarctl account <id> identity add --email alias@example.com
davarctl account <id> identity <identity-id>
davarctl account <id> identity <identity-id> update --email new-alias@example.com
davarctl account <id> identity <identity-id> delete
davarctl account <id> folder list
davarctl account <id> folder add Archive
davarctl account <id> folder <folder> message list --filter unread
davarctl account <id> folder <folder> message list --limit 5
davarctl account <id> folder <folder> message list --limit 5 --cursor '<NEXT_CURSOR>'
davarctl account <id> folder <folder> message list --limit 0
davarctl account <id> folder unread list
davarctl account <id> folder unread list --sort date_asc --limit 0
davarctl account <id> search --subject invoice --sender billing@example.com --since 2026-10-01 --folder INBOX
davarctl account <id> search --folder Archive --limit 0
davarctl account <id> folder <folder> message <message-id>
davarctl account <id> folder <folder> message <message-id> metadata
davarctl contact list --q alex --limit 50
davarctl contact list --cursor '<NEXT_CURSOR>'
davarctl contact 12
davarctl contact 12 --output vcard > alex.vcf
davarctl contact add --from-vcard ./contacts.vcf
davarctl security status
davarctl security recheck --account <id>
davarctl security recheck --account <id> --message <message-id>
davarctl account <id> folder <folder> message <message-id> attachment list
davarctl account <id> folder <folder> message <message-id> download 1
davarctl account <id> folder <folder> message <message-id> download 2 --output-file ./saved-report.pdf
davarctl account <id> folder <folder> message <message-id> flag seen flagged
davarctl account <id> folder <folder> message <message-id> flag -seen -flagged
davarctl account <id> folder <folder> message <message-id> delete
davarctl account <id> folder delete Archive
davarctl account <id> send --to recipient@example.com --headline Hello --body 'Plain text'
davarctl account <id> send --from 'Team <team@example.com>' --to recipient@example.com \
--headline Hello --body 'Plain text'
davarctl account <id> send --to recipient@example.com --cc copy@example.com \
--bcc hidden@example.com --headline Report --body-file ./message.txt \
--attach ./report.pdf --attach ./summary.csv
davarctl account <id> send --to recipient@example.com --headline Draft \
--body 'Finish later' --draft
davarctl account <id> draft list --sort unread
davarctl account <id> draft 42
davarctl account <id> draft 42 send
davarctl account <id> draft 42 delete
davarctl account <id> outbox list
davarctl account <id> outbox list --limit 20 --cursor TOKEN
davarctl account <id> outbox list --limit 0
davarctl account <id> outbox 42
davarctl account <id> outbox 42 retry
davarctl account <id> folder INBOX status
davarctl --output json account <id> folder 1 status
davarctl account delete <id>
Outbox lists use an aligned text table with ID, state, SMTP attempts, Sent-copy
attempts, creation time, next retry, unknown outcome (Y/N), and last error.
Message and Outbox listings default to 20 rows; --limit 0 returns all
remaining rows. Follow next_cursor with the same account using --cursor.
Outbox lists include unfinished submissions only. After SMTP acceptance and all
required draft cleanup and configured Sent-copy work finish, a submission
leaves the queue; inspect its compact completion status by ID. The completion
record keeps acceptance and cleanup outcome, Sent-copy completion, timestamps, attempts, account and
submission IDs, and frozen draft identity internally for deduplication; status
responses do not expose that identity. MIME, attachments, sender, and
recipients are cleared. Message lists and outbox lists print
only column labels when empty. Use
davarctl --output json account <id> outbox list for the REST JSON response.
The daemon delivers up to four outbox jobs globally, with no more than one job
active for an account; SMTP for separate accounts can progress independently.
IMAP commands and their cache updates are serialized per account, and a large
sync discovers history in bounded numeric UID ranges (normally 4,096 UID values,
expanding empty sparse windows up to 65,536) and yields between ranges and groups
of at most 25 UIDs so folder, flag, and draft operations can run between them.
Transient failures keep already committed mail; automatic sync retries with
jittered delays from five seconds to five minutes, while a failed manual sync can
be queued again. Gmail's All Mail is identified by its standard \All folder
attribute, regardless of its localized folder name. New work interrupts IDLE.
MIME processing uses two workers with a shared 2048 MiB admission budget
covering raw input and working space; each message uses a 1 GiB
limit. Waiting requests apply
backpressure instead of building an unbounded parsing queue. These defaults
are internal and have no CLI tuning flags.
Run davarctl --help, davarctl account --help, or
davarctl account <id> outbox --help or
davarctl account <id> draft --help to see the available command forms.
Normal sends are durably queued and return a submission ID before SMTP runs.
Check delivery state with davarctl account <id> outbox list or
davarctl account <id> outbox <submission-id>; retry an eligible item with
davarctl account <id> outbox <submission-id> retry. The submission ID is
separate from the RFC Message-ID and any provider-cached message ID. SMTP
acceptance means the submission server took responsibility, not that a
recipient received the message. If a connection drops after DATA and acceptance
is unknown, Davar retries automatically and logs a duplicate-risk warning.
Every new successful send ensures a read copy in the provider Sent folder,
including accounts with the legacy sent_copy setting off. Davar checks the
frozen Message-ID and reuses a provider copy before appending; an appended copy
retains Bcc recipients, while SMTP headers do not. --sent-copy remains accepted
for compatibility and no longer gates this behavior. A provider that saves its
own copy later can still race this check; existing remote duplicates are not
deleted. Sent updates refresh the UI without a new-mail notification. For a saved draft send, the source moves to Trash only after recorded
SMTP acceptance. Trash cleanup happens before Sent copy; cleanup failures remain
visible in the outbox and retry without resubmitting SMTP. A Sent-copy failure
retries only the IMAP append phase.
The REST API follows the same singular nesting, such as
/api/v1/account/<id>/folder/INBOX/message/<message-id>. Complete raw MIME is
available at the same path with /raw. Credentials are encrypted before
persistence. Provider tests and CI use isolated local fixtures; those fixtures
do not guarantee interoperability with every real provider. Use
TESTS.md to qualify real providers and record the results.
Run davarctl doctor for a text health report covering the daemon, account
connectivity and local state. Warning checks, such as plaintext TLS fallback
or a server without IMAP IDLE, are reported without failing the command; errors
produce a nonzero exit status. Use davarctl --output json doctor for the
machine-readable report.
Run the complete readiness API flow with local IMAP/SMTP fixtures:
go test ./internal/httpapi -run TestReadinessFlow -count=1
API key permissions
A read-only principal uses account.read and mail.read: account/mail reads,
status and spec are available, while mutations are blocked even for an account
it owns. doctor, security status/recheck, and global config require
administrative config.manage permission. GET /api/v1/user lists existing
built-in users for either key.
GET /api/v1/user/me returns the authenticated user as a single {id, role}
object, with Cache-Control: no-store. Both built-in key types are supported;
custom principals receive 403. The user list returns a bare array containing only
id and role; absent built-in users
are omitted. PATCH /api/v1/user/{user} rotates a built-in key for full-access
with token.manage. Invalid keys are rejected on every authenticated REST route.
curl -H "Authorization: Bearer $DAVAR_API_KEY" \
http://localhost:20114/api/v1/user
curl -H "Authorization: Bearer $DAVAR_API_KEY" \
http://localhost:20114/api/v1/user/me
curl -X PATCH -H "Authorization: Bearer $DAVAR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"action":"rotate_password"}' \
http://localhost:20114/api/v1/user/read-only
The list response is [ {"id":"full-access","role":"full-access"}, {"id":"read-only","role":"read-only"} ] when both users exist.
Use Accept: text/plain for an ID and role table.
See the authorization E2E scenarios.
Development checks
Use Go 1.27.2 and Make. Race tests also require a C compiler (for the race runtime).
Install pre-commit separately, then run make hooks-install to enable the hooks.
make ci # dependencies, format check, lint, race/coverage tests, build, scans
make spec # regenerate the OpenAPI JSON document
make spec-check # verify the checked-in document matches the REST routes
make fmt # apply Go formatting
make build # build static dist/davard and dist/davarctl
make pre-commit # run all configured hooks on tracked files
make build sets CGO_ENABLED=0 and -trimpath, so both executables are
statically linked and portable across Linux hosts. Local builds default to a
UTC timestamp version in vYYYYMMDDHHMM form; this value is printed by
davard --version and davarctl --version, and shown at the bottom of the Web
UI sidebar. Pass VERSION=vX.Y.Z to override it. Release builds use their tag.
The Makefile pins golangci-lint v2.14.0, gosec v2.29.0, and govulncheck v1.8.0.
Lint and vulnerability checks run through go run. Gosec is built from the
isolated tools module, which pins golang.org/x/tools v0.51.0 to support Go
1.27 export data without changing application dependencies. Local and CI
checks use the same tool versions.
Their first invocation requires network access; govulncheck also queries the
Go vulnerability database. CI and pre-commit both use Make targets. gosec fails
on high-severity findings, matching AnanCore; govulncheck reports reachable
vulnerabilities. Formatting checks never modify source files.
Forgejo runs .forgejo/workflows/test.yaml on pushes and pull requests, using checkout and setup-go with full GitHub action URLs. The workflow uses Go 1.27.2 and runs the same Make targets. Testing needs no secrets.
Release
Releases publish static Linux executables instead of container images. Write
the release notes in release-notes/vX.Y.Z.md, commit them, then push a
matching vX.Y.Z tag. .forgejo/workflows/release.yaml
builds linux/amd64 and linux/arm64 builds of davard and davarctl,
stamps the tag into both binaries, writes SHA256SUMS, and publishes a Forgejo
release whose body is the notes file and whose assets are the executables and
checksums. Tags must match v<major>.<minor>.<patch>. The tag is the release
source of truth; the workflow never creates or moves tags.
Folder listings retain the ASCII hierarchy and show ID: NAME [N:unread/T:total].
Counts reflect locally cached messages in that folder; N means messages without
\Seen, and parent folder counts do not include child folders.
Control a folder with davarctl account <id> folder <folder> set unsync or
set sync. The setting follows the folder's whole subtree, including
descendants discovered later; it only changes local scheduling and never
deletes provider or cached mail. Only custom folders can be controlled; system,
provider special-use, and virtual folders reject the request. Unsynced folders
show ID: NAME (unsync) without counts; their existing local messages remain
available. After enabling sync, run davarctl account <id> sync to fetch
current provider mail; account sync skips unsynced folders and their subtrees,
including a folder excluded while its pass is running: the folder stops after
the already-admitted batch and resumes the rest on the next sync.
To refresh just one folder immediately, run
davarctl account <id> folder <folder> message sync; this queues a sync for
that folder, even if automatic sync is disabled for it. An explicit
single-folder sync is not blocked by an exclusion. The command returns an
operation ID; inspect it with davarctl account <id> operation <operation-id>.
If a sync for the same account and folder is pending or running, the command
fails with sync was already scheduled, the original operation ID, and its
original UTC request time. No additional work is queued; retry after it finishes.
Retry a failed folder sync with
davarctl account <id> folder '[Gmail]/All Mail' message sync (quote folder names
containing spaces); the same command works for a localized All Mail name.
Monitor newly cached mail in a folder with davarctl account <id> folder <id> monitor.
The command prints sender and subject for messages committed while connected;
it does not start synchronization and does not replay messages from a disconnect.
Press Ctrl-C to stop it.
Message lists default to --sort date: newest received date first, with ID
descending as the tie-breaker. --sort unread groups unread messages first,
then orders each group by date descending. --filter has_attachment lists
messages with cached MIME attachment parts (including inline file parts).
davarctl account 1 folder INBOX message list --sort unread
davarctl account 1 folder INBOX message list --filter has_attachment --limit 20
# Copy the “Next cursor” value from the previous result:
davarctl account 1 folder INBOX message list --filter has_attachment --limit 20 --cursor '<TOKEN>'
A cursor continues after the last message on the previous page. Keep the same
account, folder, filter, and sort; repeat search criteria for search pagination.
Text prints Next cursor only if more results exist; JSON returns next_cursor
(or null on the final page). The limit may change between pages. New arrivals
are excluded from that traversal: omit --cursor to restart and see them.
Changes to cached received dates or read/unread status invalidate existing
cursors; restart from the first page if the server reports this. Deletions and
other flags remain live, so pagination is not a frozen mailbox snapshot.
Automated real-account CLI checks
scripts/e2e_international.py adds opt-in Hebrew, Arabic, Chinese, Japanese and
Korean folder/self-delivery checks with private browser screenshots. Supply the
authorized account at runtime; see the international E2E runbook.
make test-e2e-harness checks its safety guards without contacting providers.
scripts/e2e_cli.py is opt-in. It verifies the selected account's email, sends
only to that address, and uses unique subjects/folders. Cleanup moves its own
messages to Trash and deletes only its own temporary folders. Keep its private
reports and daemon logs outside the repository.
python3 scripts/e2e_cli.py --execute --cli "$PWD/dist/davarctl" \
--account-id 1 --account-email '<your-account-email>' \
--artifact-dir /tmp/davar-e2e --run-prefix "Davar-E2E-$(date -u +%Y%m%dT%H%M%SZ)" \
--cleanup-run-artifacts --operation-timeout 300
The IMAP client refreshes capabilities after authentication so provider-advertised MOVE, UIDPLUS, and IDLE support reflects the authenticated session.
Saved-draft sends receive a fresh RFC Message-ID, distinct from their source draft, so moving the source to Trash does not trash a delivered Gmail self-copy. The queued MIME keeps that identity for every retry. Body and attachments remain unchanged.
Account-wide sync skips folders deleted through Davar between batches instead of treating its earlier folder snapshot as current. Explicit folder sync reports a missing folder if that requested folder was deleted.
Delivery retry and SMTP connectivity
Temporary delivery failures retry indefinitely with Fibonacci delays starting at 5 seconds: 5, 5, 10, 15, 25, 40, 65, 105, 170, 275, then 300 seconds. Each delay has ±20% jitter and a hard five-minute maximum. Draft-cleanup and Sent-copy retries use the same schedule, without repeating accepted SMTP sends. Permanent SMTP rejection remains a reported failure.
A failed DNS/TCP connection pauses other queued sends to the same SMTP host and
port. When its cooldown expires, one queued message tries the connection;
other messages remain in retry_wait without increasing their attempt count.
A successful TCP connection releases the pause before TLS/authentication; TLS
and authentication errors do not mark the destination disconnected. Jobs that
wait behind a recovery probe are checked again within ten seconds. Healthy
SMTP destinations retain normal concurrency. IMAP connectivity does not gate
SMTP, because their routes and availability can differ.
davarctl account 1 outbox list
davarctl account 1 outbox 13 # state, attempts, last error, next attempt
davarctl account 1 outbox 13 retry # retry eligible work; respects connectivity cooldown
Normal logs include SMTP endpoint unavailable; delivery paused (WARN) and
SMTP endpoint connectivity recovered (INFO). Debug logs additionally include
outbox waiting for SMTP connectivity. These events carry account/submission
IDs and the endpoint. The endpoint availability hint is kept
in memory; message retry times and MIME stay durable in SQLite across restarts.
Cached IMAP mail and server-saved drafts can be rebuilt by syncing. Account
settings/credentials and API-key records currently live in SQLite, so restoring
only daemon/client JSON files requires re-adding accounts and initializing
access; pending Outbox messages require preserving the database.
Provider response limits protect the daemon from malformed or flooding mail servers. IMAP control responses have a 1 GiB wire budget, a 1 MiB line limit, and a 65,536-line limit; FETCH body bytes are additionally bounded by the caller-reserved allowance, up to 1 GiB. LIST literals and continuation bytes count toward the response budget. Greeting, APPEND and IDLE reads are bounded, and a limit failure invalidates the connection. SMTP diagnostics and submission share a 1 MiB reply budget, with at most 64 KiB per line and 4,096 lines, including replies after STARTTLS. A failed reply after DATA remains an unknown submission outcome; inspect Outbox before resending. See SECURITY.md for the assessment and remediation status.
Contacts
Davar builds one contact book per API principal from locally cached message headers. It observes From, Sender, To, Cc, Bcc when present, and Reply-To; message bodies, quoted text, and attachments are never searched for addresses. Discovery runs in a bounded durable background queue and resumes historical local-cache processing after restart without downloading mail again. The daemon starts this worker automatically. First/last-seen use the cached received/internal date, falling back to the parsed sent date and then local message creation time. A corrupt or missing local blob is retried with bounded backoff and remains visible in the queue; discovery errors do not undo synced mail.
Email identity trims surrounding whitespace, preserves local-part case/content, plus tags, and dots, and converts the domain to lower-case IDNA ASCII. Local-part equality is case-sensitive while domain equality is case-insensitive; provider-specific alias rules are not applied. A contact's first stored email spelling is retained, and a newer observed nonempty display name replaces the prior name. POST is idempotent: an existing normalized address is returned unchanged.
curl -H "Authorization: Bearer $DAVAR_API_KEY" \
'http://localhost:20114/api/v1/contacts?q=alex&limit=50'
curl -H "Authorization: Bearer $DAVAR_API_KEY" \
http://localhost:20114/api/v1/contacts/12
curl -X POST -H "Authorization: Bearer $DAVAR_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"email":"Alex@example.test","display_name":"Alex"}' \
http://localhost:20114/api/v1/contacts
GET requires mail.read; POST requires mail.modify. All operations are strictly scoped to the authenticated principal's contact book, even if that principal has grants to read another account. Listing defaults to 50 and accepts limits through 100, an optional literal case-insensitive q substring, sort=name|newest (alphabetical is the default; newest is creation time descending), trash=1 to list only soft-deleted contacts, and an opaque cursor bound to that query, sort, and trash mode. DELETE soft-deletes a contact into Trash; DELETE with permanent=1 permanently deletes it, and POST /api/v1/contacts/{id}/restore restores a trashed contact with its photo, keys, and email identities intact. A new POST returns 201 and Location; a retry returns 200 with the unchanged contact.
A contact detail response also includes public_keys: up to 128 retained collected OpenPGP public keys for that contact, each with a parser-derived fingerprint, key_id, public armor, raw uids, and unverified: true. A key is collected while synchronizing newly processed MIME only when a key User ID mailbox matches the message's unambiguous From sender; per message the daemon bounds eight keys, 128 KiB per key, and 1 MiB of aggregated armor, and per contact it retains at most 128 fingerprints, refreshing last_seen_at on repeats and evicting the oldest last-seen fingerprint (then the lowest fingerprint) once the bound is exceeded. Malformed, truncated, oversized, private, or unmatched armor is ignored without failing synchronization. Keys are read-only and unverified: they are never fetched remotely, used to encrypt, used to verify signatures, or marked trusted, and already-cached mail is not re-read only to collect keys. Keys are not shared through account grants.
davarctl contact list [--q TEXT] [--limit N] [--cursor TOKEN] uses the
same paging: the default page is 50 (1-100), and next_cursor is printed in
text or JSON so callers request further pages explicitly.
davarctl contact ID prints text by default or the REST JSON object with
--output json; --output vcard is available only on contact detail and
writes a vCard 3.0 record to stdout with CRLF endings and UTF-8-safe folding.
davarctl contact add --from-vcard PATH imports every vCard 3.0/4.0 card in a
regular file up to 16 MiB, uses each card's first FN and EMAIL, and skips
cards without an email. It validates the complete file before the first
request, reports created, existing, and skipped counts, and makes no
all-or-nothing claim if a later request fails.