agent_first_mail/
error.rs1use 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>;