botserver/docs/STALWART_API_MAPPING.md
Rodrigo Rodriguez (Pragmatismo) 26f7643f5c feat(auth): Add OAuth login for Google, Discord, Reddit, Twitter, Microsoft, Facebook
- Create core/oauth module with OAuthProvider enum and shared types
- Implement providers.rs with auth URLs, token exchange, user info endpoints
- Add routes for /auth/oauth/providers, /auth/oauth/{provider}, and callbacks
- Update login.html with OAuth button grid and dynamic provider loading
- Add OAuth config settings to config.csv with setup documentation and links
- Uses HTMX for login form, minimal JS for OAuth provider visibility
2025-12-04 22:53:40 -03:00

527 lines
No EOL
18 KiB
Markdown

# Stalwart API Mapping for General Bots
**Version:** 6.1.0
**Purpose:** Map Stalwart native features vs General Bots custom tables
---
## Overview
Stalwart Mail Server provides a comprehensive REST Management API. Many email features that we might implement in our database are already available natively in Stalwart. This document maps what to use from Stalwart vs what we manage ourselves.
---
## API Base URL
```
https://{stalwart_host}:{port}/api
```
Default ports:
- HTTP: 8080
- HTTPS: 443
---
## Feature Mapping
### ✅ USE STALWART API (Do NOT create tables)
| Feature | Stalwart Endpoint | Notes |
|---------|------------------|-------|
| **User/Account Management** | `GET/POST/PATCH/DELETE /principal/{id}` | Create, update, delete email accounts |
| **Email Queue** | `GET /queue/messages` | List queued messages for delivery |
| **Queue Status** | `GET /queue/status` | Check if queue is running |
| **Queue Control** | `PATCH /queue/status/start` `PATCH /queue/status/stop` | Start/stop queue processing |
| **Reschedule Delivery** | `PATCH /queue/messages/{id}` | Retry failed deliveries |
| **Cancel Delivery** | `DELETE /queue/messages/{id}` | Cancel queued message |
| **Distribution Lists** | `POST /principal` with `type: "list"` | Mailing lists are "principals" |
| **DKIM Signatures** | `POST /dkim` | Create DKIM keys per domain |
| **DNS Records** | `GET /dns/records/{domain}` | Get required DNS records |
| **Spam Training** | `POST /spam-filter/train/spam` `POST /spam-filter/train/ham` | Train spam filter |
| **Spam Classification** | `POST /spam-filter/classify` | Test spam score |
| **Telemetry/Metrics** | `GET /telemetry/metrics` | Server metrics for monitoring |
| **Live Metrics** | `GET /telemetry/metrics/live` | Real-time metrics (WebSocket) |
| **Logs** | `GET /logs` | Query server logs |
| **Traces** | `GET /telemetry/traces` | Delivery traces |
| **Live Tracing** | `GET /telemetry/traces/live` | Real-time tracing |
| **DMARC Reports** | `GET /reports/dmarc` | Incoming DMARC reports |
| **TLS Reports** | `GET /reports/tls` | TLS-RPT reports |
| **ARF Reports** | `GET /reports/arf` | Abuse feedback reports |
| **Troubleshooting** | `GET /troubleshoot/delivery/{recipient}` | Debug delivery issues |
| **DMARC Check** | `POST /troubleshoot/dmarc` | Test DMARC/SPF/DKIM |
| **Settings** | `GET/POST /settings` | Server configuration |
| **Undelete** | `GET/POST /store/undelete/{account_id}` | Recover deleted messages |
| **Account Purge** | `GET /store/purge/account/{id}` | Purge account data |
| **Encryption Settings** | `GET/POST /account/crypto` | Encryption-at-rest |
| **2FA/App Passwords** | `GET/POST /account/auth` | Authentication settings |
### ⚠️ USE BOTH (Stalwart + Our Tables)
| Feature | Stalwart | Our Table | Why Both? |
|---------|----------|-----------|-----------|
| **Auto-Responders** | Sieve scripts via settings | `email_auto_responders` | We store UI config, sync to Stalwart Sieve |
| **Email Rules/Filters** | Sieve scripts | `email_rules` | We store UI-friendly rules, compile to Sieve |
| **Shared Mailboxes** | Principal with shared access | `shared_mailboxes` | We track permissions, Stalwart handles access |
### ✅ USE OUR TABLES (Stalwart doesn't provide)
| Feature | Our Table | Why? |
|---------|-----------|------|
| **Global Email Signature** | `global_email_signatures` | Bot-level branding, not in Stalwart |
| **User Email Signature** | `email_signatures` | User preferences, append before send |
| **Scheduled Send** | `scheduled_emails` | We queue and release at scheduled time |
| **Email Templates** | `email_templates` | Business templates with variables |
| **Email Labels** | `email_labels`, `email_label_assignments` | UI organization, not IMAP folders |
| **Email Tracking** | `sent_email_tracking` (existing) | Open/click tracking pixels |
---
## Stalwart API Integration Code
### Client Setup
```rust
// src/email/stalwart_client.rs
pub struct StalwartClient {
base_url: String,
auth_token: String,
http_client: reqwest::Client,
}
impl StalwartClient {
pub fn new(base_url: &str, token: &str) -> Self {
Self {
base_url: base_url.to_string(),
auth_token: token.to_string(),
http_client: reqwest::Client::new(),
}
}
async fn request<T: DeserializeOwned>(&self, method: Method, path: &str, body: Option<Value>) -> Result<T> {
let url = format!("{}{}", self.base_url, path);
let mut req = self.http_client.request(method, &url)
.header("Authorization", format!("Bearer {}", self.auth_token));
if let Some(b) = body {
req = req.json(&b);
}
let resp = req.send().await?;
let data: ApiResponse<T> = resp.json().await?;
Ok(data.data)
}
}
```
### Queue Monitoring (for Analytics Dashboard)
```rust
impl StalwartClient {
/// Get email queue status for monitoring dashboard
pub async fn get_queue_status(&self) -> Result<QueueStatus> {
let status: bool = self.request(Method::GET, "/api/queue/status", None).await?;
let messages: QueueList = self.request(Method::GET, "/api/queue/messages?limit=100", None).await?;
Ok(QueueStatus {
is_running: status,
total_queued: messages.total,
messages: messages.items,
})
}
/// Get queued message details
pub async fn get_queued_message(&self, message_id: &str) -> Result<QueuedMessage> {
self.request(Method::GET, &format!("/api/queue/messages/{}", message_id), None).await
}
/// Retry failed delivery
pub async fn retry_delivery(&self, message_id: &str) -> Result<bool> {
self.request(Method::PATCH, &format!("/api/queue/messages/{}", message_id), None).await
}
/// Cancel queued message
pub async fn cancel_delivery(&self, message_id: &str) -> Result<bool> {
self.request(Method::DELETE, &format!("/api/queue/messages/{}", message_id), None).await
}
/// Stop all queue processing
pub async fn stop_queue(&self) -> Result<bool> {
self.request(Method::PATCH, "/api/queue/status/stop", None).await
}
/// Resume queue processing
pub async fn start_queue(&self) -> Result<bool> {
self.request(Method::PATCH, "/api/queue/status/start", None).await
}
}
```
### Account/Principal Management
```rust
impl StalwartClient {
/// Create email account
pub async fn create_account(&self, email: &str, password: &str, display_name: &str) -> Result<u64> {
let body = json!({
"type": "individual",
"name": email.split('@').next().unwrap_or(email),
"emails": [email],
"secrets": [password],
"description": display_name,
"quota": 0,
"roles": ["user"]
});
self.request(Method::POST, "/api/principal", Some(body)).await
}
/// Create distribution list
pub async fn create_distribution_list(&self, name: &str, email: &str, members: Vec<String>) -> Result<u64> {
let body = json!({
"type": "list",
"name": name,
"emails": [email],
"members": members,
"description": format!("Distribution list: {}", name)
});
self.request(Method::POST, "/api/principal", Some(body)).await
}
/// Get account details
pub async fn get_account(&self, account_id: &str) -> Result<Principal> {
self.request(Method::GET, &format!("/api/principal/{}", account_id), None).await
}
/// Update account
pub async fn update_account(&self, account_id: &str, updates: Vec<AccountUpdate>) -> Result<()> {
let body: Vec<Value> = updates.iter().map(|u| json!({
"action": u.action,
"field": u.field,
"value": u.value
})).collect();
self.request(Method::PATCH, &format!("/api/principal/{}", account_id), Some(json!(body))).await
}
/// Delete account
pub async fn delete_account(&self, account_id: &str) -> Result<()> {
self.request(Method::DELETE, &format!("/api/principal/{}", account_id), None).await
}
}
```
### Sieve Rules (Auto-Responders & Filters)
```rust
impl StalwartClient {
/// Set vacation/out-of-office auto-responder via Sieve
pub async fn set_auto_responder(&self, account_id: &str, config: &AutoResponderConfig) -> Result<()> {
let sieve_script = self.generate_vacation_sieve(config);
let updates = vec![json!({
"type": "set",
"prefix": format!("sieve.scripts.{}.vacation", account_id),
"value": sieve_script
})];
self.request(Method::POST, "/api/settings", Some(json!(updates))).await
}
fn generate_vacation_sieve(&self, config: &AutoResponderConfig) -> String {
let mut script = String::from("require [\"vacation\", \"variables\"];\n\n");
if let Some(start) = &config.start_date {
script.push_str(&format!("# Active from: {}\n", start));
}
if let Some(end) = &config.end_date {
script.push_str(&format!("# Active until: {}\n", end));
}
script.push_str(&format!(
r#"vacation :days 1 :subject "{}" "{}";"#,
config.subject.replace('"', "\\\""),
config.body_plain.replace('"', "\\\"")
));
script
}
/// Set email filter rule via Sieve
pub async fn set_filter_rule(&self, account_id: &str, rule: &EmailRule) -> Result<()> {
let sieve_script = self.generate_filter_sieve(rule);
let updates = vec![json!({
"type": "set",
"prefix": format!("sieve.scripts.{}.filter_{}", account_id, rule.id),
"value": sieve_script
})];
self.request(Method::POST, "/api/settings", Some(json!(updates))).await
}
fn generate_filter_sieve(&self, rule: &EmailRule) -> String {
let mut script = String::from("require [\"fileinto\", \"reject\", \"vacation\"];\n\n");
// Generate conditions
for condition in &rule.conditions {
match condition.field.as_str() {
"from" => script.push_str(&format!(
"if header :contains \"From\" \"{}\" {{\n",
condition.value
)),
"subject" => script.push_str(&format!(
"if header :contains \"Subject\" \"{}\" {{\n",
condition.value
)),
_ => {}
}
}
// Generate actions
for action in &rule.actions {
match action.action_type.as_str() {
"move" => script.push_str(&format!(" fileinto \"{}\";\n", action.value)),
"delete" => script.push_str(" discard;\n"),
"mark_read" => script.push_str(" setflag \"\\\\Seen\";\n"),
_ => {}
}
}
if rule.stop_processing {
script.push_str(" stop;\n");
}
script.push_str("}\n");
script
}
}
```
### Telemetry & Monitoring
```rust
impl StalwartClient {
/// Get server metrics for dashboard
pub async fn get_metrics(&self) -> Result<Metrics> {
self.request(Method::GET, "/api/telemetry/metrics", None).await
}
/// Get server logs
pub async fn get_logs(&self, page: u32, limit: u32) -> Result<LogList> {
self.request(
Method::GET,
&format!("/api/logs?page={}&limit={}", page, limit),
None
).await
}
/// Get delivery traces
pub async fn get_traces(&self, trace_type: &str, page: u32) -> Result<TraceList> {
self.request(
Method::GET,
&format!("/api/telemetry/traces?type={}&page={}&limit=50", trace_type, page),
None
).await
}
/// Get specific trace details
pub async fn get_trace(&self, trace_id: &str) -> Result<Vec<TraceEvent>> {
self.request(Method::GET, &format!("/api/telemetry/trace/{}", trace_id), None).await
}
/// Get DMARC reports
pub async fn get_dmarc_reports(&self, page: u32) -> Result<ReportList> {
self.request(Method::GET, &format!("/api/reports/dmarc?page={}&limit=50", page), None).await
}
/// Get TLS reports
pub async fn get_tls_reports(&self, page: u32) -> Result<ReportList> {
self.request(Method::GET, &format!("/api/reports/tls?page={}&limit=50", page), None).await
}
}
```
### Spam Filter
```rust
impl StalwartClient {
/// Train message as spam
pub async fn train_spam(&self, raw_message: &str) -> Result<()> {
self.http_client
.post(&format!("{}/api/spam-filter/train/spam", self.base_url))
.header("Authorization", format!("Bearer {}", self.auth_token))
.header("Content-Type", "message/rfc822")
.body(raw_message.to_string())
.send()
.await?;
Ok(())
}
/// Train message as ham (not spam)
pub async fn train_ham(&self, raw_message: &str) -> Result<()> {
self.http_client
.post(&format!("{}/api/spam-filter/train/ham", self.base_url))
.header("Authorization", format!("Bearer {}", self.auth_token))
.header("Content-Type", "message/rfc822")
.body(raw_message.to_string())
.send()
.await?;
Ok(())
}
/// Classify message (get spam score)
pub async fn classify_message(&self, message: &SpamClassifyRequest) -> Result<SpamClassifyResult> {
self.request(Method::POST, "/api/spam-filter/classify", Some(json!(message))).await
}
}
```
---
## Monitoring Dashboard Integration
### Endpoints to Poll
| Metric | Endpoint | Poll Interval |
|--------|----------|---------------|
| Queue Size | `GET /queue/messages` | 30s |
| Queue Status | `GET /queue/status` | 30s |
| Server Metrics | `GET /telemetry/metrics` | 60s |
| Recent Logs | `GET /logs?limit=100` | 60s |
| Delivery Traces | `GET /telemetry/traces?type=delivery.attempt-start` | 60s |
| Failed Deliveries | `GET /queue/messages?filter=status:failed` | 60s |
### WebSocket Endpoints (Real-time)
| Feature | Endpoint | Token Endpoint |
|---------|----------|----------------|
| Live Metrics | `ws://.../telemetry/metrics/live` | `GET /telemetry/live/metrics-token` |
| Live Traces | `ws://.../telemetry/traces/live` | `GET /telemetry/live/tracing-token` |
---
## Tables to REMOVE from Migration
Based on this mapping, these tables are **REDUNDANT** and should be removed:
```sql
-- REMOVE: Stalwart handles distribution lists via principals
-- DROP TABLE IF EXISTS distribution_lists;
-- KEEP: We need this for UI config, but sync to Stalwart Sieve
-- email_auto_responders (KEEP but add stalwart_sieve_id column)
-- KEEP: We need this for UI config, but sync to Stalwart Sieve
-- email_rules (KEEP but add stalwart_sieve_id column)
```
---
## Migration Updates Needed
The current `6.1.0_enterprise_suite` migration already correctly:
1. ✅ Keeps `global_email_signatures` - Stalwart doesn't have this
2. ✅ Keeps `email_signatures` - User preference, not in Stalwart
3. ✅ Keeps `scheduled_emails` - We manage scheduling
4. ✅ Keeps `email_templates` - Business feature
5. ✅ Keeps `email_labels` - UI organization
6. ✅ Has `stalwart_sieve_id` in `email_auto_responders` - For sync
7. ✅ Has `stalwart_sieve_id` in `email_rules` - For sync
8. ✅ Has `stalwart_account_id` in `shared_mailboxes` - For sync
The `distribution_lists` table could potentially be removed since Stalwart handles lists as principals, BUT we may want to keep it for:
- Caching/faster lookups
- UI metadata not stored in Stalwart
- Offline resilience
**Recommendation**: Keep `distribution_lists` but sync with Stalwart principals.
---
## Sync Strategy
### On Create (Our DB → Stalwart)
```rust
async fn create_distribution_list(db: &Pool, stalwart: &StalwartClient, list: NewDistributionList) -> Result<Uuid> {
// 1. Create in Stalwart first
let stalwart_id = stalwart.create_distribution_list(
&list.name,
&list.email_alias,
list.members.clone()
).await?;
// 2. Store in our DB with stalwart reference
let id = db.insert_distribution_list(DistributionList {
name: list.name,
email_alias: list.email_alias,
members_json: serde_json::to_string(&list.members)?,
stalwart_principal_id: Some(stalwart_id.to_string()),
..Default::default()
}).await?;
Ok(id)
}
```
### On Update (Sync both)
```rust
async fn update_distribution_list(db: &Pool, stalwart: &StalwartClient, id: Uuid, updates: ListUpdates) -> Result<()> {
// 1. Get current record
let list = db.get_distribution_list(&id).await?;
// 2. Update Stalwart if we have a reference
if let Some(stalwart_id) = &list.stalwart_principal_id {
stalwart.update_principal(stalwart_id, updates.to_stalwart_updates()).await?;
}
// 3. Update our DB
db.update_distribution_list(&id, updates).await?;
Ok(())
}
```
### On Delete (Both)
```rust
async fn delete_distribution_list(db: &Pool, stalwart: &StalwartClient, id: Uuid) -> Result<()> {
let list = db.get_distribution_list(&id).await?;
// 1. Delete from Stalwart
if let Some(stalwart_id) = &list.stalwart_principal_id {
stalwart.delete_principal(stalwart_id).await?;
}
// 2. Delete from our DB
db.delete_distribution_list(&id).await?;
Ok(())
}
```
---
## Summary
| Category | Use Stalwart | Use Our Tables | Use Both |
|----------|-------------|----------------|----------|
| Account Management | ✅ | | |
| Email Queue | ✅ | | |
| Queue Monitoring | ✅ | | |
| Distribution Lists | | | ✅ |
| Auto-Responders | | | ✅ |
| Email Rules/Filters | | | ✅ |
| Shared Mailboxes | | | ✅ |
| Email Signatures | | ✅ | |
| Scheduled Send | | ✅ | |
| Email Templates | | ✅ | |
| Email Labels | | ✅ | |
| Email Tracking | | ✅ | |
| Spam Training | ✅ | | |
| Telemetry/Logs | ✅ | | |
| DMARC/TLS Reports | ✅ | | |