Standalone outbound-email service in the clarity backend: an SMTP client (the lettre crate) paired with a SQLite store-and-forward queue with retry, an append-only JSONL audit log, and an admin config UI. It is callable by any in-process producer or over HTTP; the notifications dispatcher is one such caller (clarity:backend/src-tauri/src/mail/mod.rs:1-26).
Defined in clarity:backend/src-tauri/src/mail/model.rs. MAX_RECIPIENTS = 500 (clarity:backend/src-tauri/src/mail/model.rs:10).
MailSendRequest — wire request, camelCase: to / cc / bcc: Vec<String>, subject, body, body_type (default "html"), reply_to, notification_id (clarity:backend/src-tauri/src/mail/model.rs:73-96). validate() requires non-empty subject and body, ≤500 recipients, a basic @ presence check, and addresses ≤320 chars (clarity:backend/src-tauri/src/mail/model.rs:102-130).MailSendResponse — sent, queued, message_id, accepted/rejected recipients, mail_queue_id (clarity:backend/src-tauri/src/mail/model.rs:136-154).SmtpConfigResponse — config read-back with password masked (clarity:backend/src-tauri/src/mail/model.rs:160-172).SmtpConfigUpdate — all fields optional (clarity:backend/src-tauri/src/mail/model.rs:178-190).SmtpTestRequest — test-email target (clarity:backend/src-tauri/src/mail/model.rs:196-204).QueuedMail — internal mail_queue row (clarity:backend/src-tauri/src/mail/model.rs:212-231).MailStatus — enum Pending | Sending | Sent | Failed (clarity:backend/src-tauri/src/mail/model.rs:233-260).MailAuditEntry — JSONL audit shape (clarity:backend/src-tauri/src/mail/model.rs:267-290).Filter chain assembled in clarity:backend/src-tauri/src/mail/routes.rs:38-98. All /exactapi/* routes are mounted under warp::path("exactapi") in main.rs (see API server).
| Method | Path | Auth | Purpose |
|---|---|---|---|
| POST | /exactapi/mail/send |
any authenticated user | Send mail; multipart, 50 MB cap (clarity:backend/src-tauri/src/mail/routes.rs:108-308) |
| GET | /exactapi/mail/queue/status |
admin | Queue counts by status (clarity:backend/src-tauri/src/mail/routes.rs:310-315) |
| GET | /exactapi/admin/smtp/config |
admin | Read masked SMTP config (clarity:backend/src-tauri/src/mail/routes.rs:317-335) |
| POST | /exactapi/admin/smtp/config |
admin | Hot-reload config by rewriting clarity.properties + calling init_config() (clarity:backend/src-tauri/src/mail/routes.rs:337-387) |
| POST | /exactapi/admin/smtp/test |
admin | Send a canned test email (clarity:backend/src-tauri/src/mail/routes.rs:389-430) |
The admin UI page is served outside /exactapi: GET /admin/smtp_ui returns the bundled admin_smtp.html via include_str!, registered at clarity:backend/src-tauri/src/main.rs:5020-5025 and mounted at clarity:backend/src-tauri/src/main.rs:5156.
send_email() builds a lettre Message — either singlepart, or MultiPart::mixed with disk-read attachments whose MIME type is guessed — and sends via AsyncSmtpTransport::<Tokio1Executor> (clarity:backend/src-tauri/src/mail/smtp.rs:22-134).build_transport maps the encryption setting: tls → Wrapper, starttls → Required, otherwise None; credentials are optional (clarity:backend/src-tauri/src/mail/smtp.rs:155-182).check_smtp_config() validates the effective configuration (clarity:backend/src-tauri/src/mail/smtp.rs:197-215).sender.rs runs the queue-draining task:
spawn() starts the task (clarity:backend/src-tauri/src/mail/sender.rs:19-23).run_sender loops (clarity:backend/src-tauri/src/mail/sender.rs:25-146): it sweeps the queue in batches of 5, sends each mail, marks it sent or schedules a retry, and runs a weekly attachment cleanup.Defined in clarity:backend/src-tauri/src/mail/queue.rs.
enqueue — INSERT into mail_queue (clarity:backend/src-tauri/src/mail/queue.rs:28-70).dequeue_pending — atomically flips pending → sending (clarity:backend/src-tauri/src/mail/queue.rs:78-120).mark_sent (clarity:backend/src-tauri/src/mail/queue.rs:127-141) and mark_failed (clarity:backend/src-tauri/src/mail/queue.rs:144-159).schedule_retry — exponential backoff base * 2^retry plus up to 30 s of jitter; promotes to mark_failed once retry_count >= max_retries (clarity:backend/src-tauri/src/mail/queue.rs:162-196).cleanup_old_attachments plus a 30-day row purge (clarity:backend/src-tauri/src/mail/queue.rs:205-289).count_by_status — backs the queue-status endpoint (clarity:backend/src-tauri/src/mail/queue.rs:296-325).Append-only JSONL written to {data_dir}/logs/mail-YYYY-MM-DD.log, size-rotated by truncation at logging_max_file_bytes (clarity:backend/src-tauri/src/mail/audit.rs:1-76). Loggers: log_sent (clarity:backend/src-tauri/src/mail/audit.rs:80-107), log_failed (clarity:backend/src-tauri/src/mail/audit.rs:111-138), log_queued (clarity:backend/src-tauri/src/mail/audit.rs:141-164). Logging is best-effort and never propagates errors.
main.rs initializes the store and background sender via mail::init(db) followed by mail::sender::spawn(db) (clarity:backend/src-tauri/src/main.rs:5278-5282), and mounts the routes with warp::path("exactapi").and(mail::routes::routes(db)) (clarity:backend/src-tauri/src/main.rs:5430-5431).
Loaded from clarity.properties in clarity:backend/src-tauri/src/config.rs:457-501 (see API server for the config loader). Keys are under clarity.smtp.* and clarity.mail.*.
SMTP (clarity.smtp.*):
| Key | Default | Notes |
|---|---|---|
smtp_enabled |
false |
|
smtp_host |
— | |
smtp_port |
587 |
|
smtp_username |
— | |
smtp_password |
— | env SMTP_PASSWORD overrides the properties file |
smtp_from_address |
— | |
smtp_from_name |
"Clarity Engine" |
|
smtp_encryption |
"starttls" |
one of none | starttls | tls |
smtp_timeout_seconds |
10 |
Mail queue / attachments (clarity.mail.*):
| Key | Default | Notes |
|---|---|---|
mail_queue_max_retry_count |
5 |
|
mail_queue_retry_interval_seconds |
60 |
retry backoff base |
mail_queue_sweep_interval_seconds |
30 |
|
mail_attachment_max_size_mb |
25 |
|
mail_attachment_dir |
"./mail_attachments" |
|
mail_attachment_cleanup_days |
7 |
|
mail_attachment_cleanup_size_threshold_mb |
10 |
larger files get an accelerated 3-day cleanup |
mail_rate_limit_max_per_minute |
10 |
|
mail_rate_limit_window_seconds |
60 |
See notifications (a caller), API server (config loader and route mounting), and the glossary.
Last updated: 2026-07-17 from commit 6800acc