1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
11pub struct Message {
12 pub account: String,
13 pub id: String,
14 pub from: MessageFrom,
15 pub subject: String,
16 pub received_at: DateTime<Utc>,
17 pub is_read: bool,
18 pub preview: String,
19 #[serde(default)]
22 pub flag_status: FlagStatus,
23 #[serde(default)]
27 pub has_attachments: bool,
28 #[serde(default)]
33 pub body: String,
34 #[serde(default)]
35 pub body_content_type: BodyContentType,
36}
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "camelCase")]
44pub enum FlagStatus {
45 #[default]
46 NotFlagged,
47 Flagged,
48 Complete,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct MessageFrom {
53 pub name: String,
54 pub address: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62pub struct FullMessage {
63 pub account: String,
64 pub id: String,
65 pub from: MessageFrom,
66 pub to: Vec<MessageFrom>,
67 pub cc: Vec<MessageFrom>,
68 pub bcc: Vec<MessageFrom>,
69 pub subject: String,
70 pub received_at: chrono::DateTime<chrono::Utc>,
71 pub sent_at: chrono::DateTime<chrono::Utc>,
72 pub is_read: bool,
73 pub body_content_type: BodyContentType,
74 pub body_content: String,
75 pub has_attachments: bool,
76 #[serde(default)]
77 pub flag_status: FlagStatus,
78}
79
80#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
81#[serde(rename_all = "lowercase")]
82pub enum BodyContentType {
83 #[default]
84 Text,
85 Html,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct Attachment {
92 pub id: String,
94 pub name: String,
95 pub content_type: String,
96 pub size_bytes: u64,
97 pub is_inline: bool,
98 pub content_id: Option<String>,
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn message_roundtrips_through_json() {
107 let m = Message {
108 account: "a@b.com".into(),
109 id: "id-1".into(),
110 from: MessageFrom {
111 name: "Maria Lindberg".into(),
112 address: "maria@mklab.se".into(),
113 },
114 subject: "Quarterly numbers".into(),
115 received_at: DateTime::parse_from_rfc3339("2026-05-13T22:00:00Z")
116 .unwrap()
117 .to_utc(),
118 is_read: false,
119 preview: "Hi, attached areβ¦".into(),
120 flag_status: FlagStatus::NotFlagged,
121 has_attachments: false,
122 body: String::new(),
123 body_content_type: BodyContentType::Text,
124 };
125 let json = serde_json::to_string(&m).unwrap();
126 let m2: Message = serde_json::from_str(&json).unwrap();
127 assert_eq!(m, m2);
128 }
129
130 #[test]
131 fn full_message_roundtrips_through_json() {
132 let m = FullMessage {
133 account: "u@e.com".into(),
134 id: "graph-id".into(),
135 from: MessageFrom {
136 name: "Maria".into(),
137 address: "maria@mklab.se".into(),
138 },
139 to: vec![MessageFrom {
140 name: "Kristofer".into(),
141 address: "kristofer@mklab.se".into(),
142 }],
143 cc: vec![],
144 bcc: vec![],
145 subject: "Hi".into(),
146 received_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T22:00:00Z")
147 .unwrap()
148 .to_utc(),
149 sent_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T21:59:30Z")
150 .unwrap()
151 .to_utc(),
152 is_read: false,
153 body_content_type: BodyContentType::Html,
154 body_content: "<p>Hello</p>".into(),
155 has_attachments: true,
156 flag_status: FlagStatus::Flagged,
157 };
158 let json = serde_json::to_string(&m).unwrap();
159 let m2: FullMessage = serde_json::from_str(&json).unwrap();
160 assert_eq!(m, m2);
161 }
162
163 #[test]
164 fn attachment_roundtrips_through_json() {
165 let a = Attachment {
166 id: "att-1".into(),
167 name: "report.pdf".into(),
168 content_type: "application/pdf".into(),
169 size_bytes: 12345,
170 is_inline: false,
171 content_id: None,
172 };
173 let json = serde_json::to_string(&a).unwrap();
174 let a2: Attachment = serde_json::from_str(&json).unwrap();
175 assert_eq!(a, a2);
176 }
177
178 #[test]
179 fn body_content_type_serializes_lowercase() {
180 assert_eq!(
181 serde_json::to_string(&BodyContentType::Html).unwrap(),
182 "\"html\""
183 );
184 assert_eq!(
185 serde_json::to_string(&BodyContentType::Text).unwrap(),
186 "\"text\""
187 );
188 }
189}