Skip to main content

agent_first_mail/
error.rs

1use serde_json::{json, Value};
2use std::fmt::{Display, Formatter};
3
4#[derive(Debug, Clone, PartialEq)]
5pub struct AppError {
6    pub error_code: &'static str,
7    pub message: String,
8    pub retryable: bool,
9    pub hint: Option<String>,
10    pub details: Option<Value>,
11}
12
13impl AppError {
14    pub fn new(error_code: &'static str, message: impl Into<String>) -> Self {
15        Self {
16            error_code,
17            message: message.into(),
18            retryable: false,
19            hint: None,
20            details: None,
21        }
22    }
23
24    pub fn retryable(error_code: &'static str, message: impl Into<String>) -> Self {
25        Self {
26            error_code,
27            message: message.into(),
28            retryable: true,
29            hint: None,
30            details: None,
31        }
32    }
33
34    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
35        self.hint = Some(hint.into());
36        self
37    }
38
39    pub fn with_details(mut self, details: Value) -> Self {
40        self.details = Some(details);
41        self
42    }
43
44    pub fn io(context: &str, err: &std::io::Error) -> Self {
45        Self::new("store_error", format!("{context}: {err}"))
46    }
47
48    pub fn json(context: &str, err: &serde_json::Error) -> Self {
49        Self::new("store_error", format!("{context}: {err}"))
50    }
51
52    pub fn to_value(&self) -> serde_json::Value {
53        let mut value = json!({
54            "code": "error",
55            "error_code": self.error_code,
56            "error": self.message,
57            "retryable": self.retryable,
58            "trace": {"duration_ms": 0}
59        });
60        if let Value::Object(map) = &mut value {
61            if let Some(hint) = self
62                .hint
63                .as_deref()
64                .or_else(|| default_hint(self.error_code))
65            {
66                map.insert("hint".to_string(), json!(hint));
67            }
68            if let Some(details) = &self.details {
69                map.insert("details".to_string(), details.clone());
70            }
71        }
72        value
73    }
74}
75
76fn default_hint(error_code: &str) -> Option<&'static str> {
77    match error_code {
78        "invalid_request" => {
79            Some("Run the nearest `afmail ... --help` command and retry with the documented action-first syntax.")
80        }
81        "reason_required" => Some("Repeat the command with `--reason TEXT`, or change `audit.reason_mode` if this workspace should not require reasons."),
82        "confirm_required" => Some("Review the dry-run or status output, then rerun with `--confirm` when you want to apply changes."),
83        "transaction_incomplete" => Some("Run `afmail doctor` to inspect incomplete transactions before making more workspace changes."),
84        "config_missing" => Some("Run `afmail config show` and set the missing config key before retrying."),
85        "config_invalid" => Some("Run `afmail config show`, fix `.afmail/config.json`, or update the key with `afmail config set`."),
86        "unknown_mailbox_id" => Some("Run `afmail config show` to inspect configured mailbox ids, then retry with one of those ids."),
87        "workspace_locked" => Some("Wait for the running afmail command to finish, then retry."),
88        "case_not_found" => Some("Check the case ref and whether it is active or archived with `afmail status` and `afmail archive list cases`."),
89        "archive_not_found" | "archive_entry_not_found" => {
90            Some("Check archive refs with `afmail archive list messages`, then retry with the correct ref and message id.")
91        }
92        "draft_not_found" => Some("List the case drafts directory or recreate the draft with `afmail case draft reply` / `afmail case draft new`."),
93        "draft_invalid" => Some("Fix the draft markdown/frontmatter, then run `afmail case draft validate CASE_REF DRAFT_NAME`."),
94        "imap_connect_failed" | "imap_greeting_failed" | "imap_login_failed" | "imap_tls_failed"
95        | "imap_select_failed" | "imap_fetch_failed" | "imap_search_failed" | "imap_list_failed"
96        | "imap_capability_failed" | "imap_create_failed" | "imap_append_failed"
97        | "imap_move_failed" | "imap_store_failed" | "imap_uid_missing" => {
98            Some("Check IMAP config with `afmail config show`; use `afmail remote test` and `afmail remote folders` to diagnose.")
99        }
100        "smtp_connect_failed" | "smtp_send_failed" => {
101            Some("Check SMTP config with `afmail config show`; preview queued mail with `afmail push --dry-run` before retrying.")
102        }
103        _ => None,
104    }
105}
106
107impl Display for AppError {
108    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109        write!(f, "{}: {}", self.error_code, self.message)
110    }
111}
112
113impl std::error::Error for AppError {}
114
115pub type Result<T> = std::result::Result<T, AppError>;