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 #[serde(default)]
16 pub conversation_id: String,
17 pub from: MessageFrom,
18 pub subject: String,
19 pub received_at: DateTime<Utc>,
20 pub is_read: bool,
21 pub preview: String,
22 #[serde(default)]
25 pub flag_status: FlagStatus,
26 #[serde(default)]
30 pub has_attachments: bool,
31 #[serde(default)]
36 pub body: String,
37 #[serde(default)]
38 pub body_content_type: BodyContentType,
39 #[serde(default)]
41 pub to: Vec<MessageFrom>,
42 #[serde(default)]
43 pub cc: Vec<MessageFrom>,
44 #[serde(default)]
46 pub is_invite: bool,
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub enum FlagStatus {
56 #[default]
57 NotFlagged,
58 Flagged,
59 Complete,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct MessageFrom {
64 pub name: String,
65 pub address: String,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct FullMessage {
74 pub account: String,
75 pub id: String,
76 #[serde(default)]
78 pub conversation_id: String,
79 pub from: MessageFrom,
80 pub to: Vec<MessageFrom>,
81 pub cc: Vec<MessageFrom>,
82 pub bcc: Vec<MessageFrom>,
83 pub subject: String,
84 pub received_at: chrono::DateTime<chrono::Utc>,
85 pub sent_at: chrono::DateTime<chrono::Utc>,
86 pub is_read: bool,
87 pub body_content_type: BodyContentType,
88 pub body_content: String,
89 pub has_attachments: bool,
90 #[serde(default)]
91 pub flag_status: FlagStatus,
92 #[serde(default)]
94 pub is_invite: bool,
95 #[serde(default)]
97 pub event_id: Option<String>,
98 #[serde(default)]
100 pub is_draft: bool,
101}
102
103#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "lowercase")]
105pub enum BodyContentType {
106 #[default]
107 Text,
108 Html,
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct Attachment {
115 pub id: String,
117 pub name: String,
118 pub content_type: String,
119 pub size_bytes: u64,
120 pub is_inline: bool,
121 pub content_id: Option<String>,
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127
128 #[test]
129 fn message_roundtrips_through_json() {
130 let m = Message {
131 conversation_id: String::new(),
132 account: "a@b.com".into(),
133 id: "id-1".into(),
134 from: MessageFrom {
135 name: "Maria Lindberg".into(),
136 address: "maria@mklab.se".into(),
137 },
138 subject: "Quarterly numbers".into(),
139 received_at: DateTime::parse_from_rfc3339("2026-05-13T22:00:00Z")
140 .unwrap()
141 .to_utc(),
142 is_read: false,
143 preview: "Hi, attached are…".into(),
144 flag_status: FlagStatus::NotFlagged,
145 has_attachments: false,
146 body: String::new(),
147 body_content_type: BodyContentType::Text,
148 to: vec![],
149 cc: vec![],
150 is_invite: false,
151 };
152 let json = serde_json::to_string(&m).unwrap();
153 let m2: Message = serde_json::from_str(&json).unwrap();
154 assert_eq!(m, m2);
155 }
156
157 #[test]
158 fn full_message_roundtrips_through_json() {
159 let m = FullMessage {
160 conversation_id: String::new(),
161 account: "u@e.com".into(),
162 id: "graph-id".into(),
163 from: MessageFrom {
164 name: "Maria".into(),
165 address: "maria@mklab.se".into(),
166 },
167 to: vec![MessageFrom {
168 name: "Kristofer".into(),
169 address: "kristofer@mklab.se".into(),
170 }],
171 cc: vec![],
172 bcc: vec![],
173 subject: "Hi".into(),
174 received_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T22:00:00Z")
175 .unwrap()
176 .to_utc(),
177 sent_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T21:59:30Z")
178 .unwrap()
179 .to_utc(),
180 is_read: false,
181 body_content_type: BodyContentType::Html,
182 body_content: "<p>Hello</p>".into(),
183 has_attachments: true,
184 flag_status: FlagStatus::Flagged,
185 is_invite: true,
186 event_id: Some("event-1".into()),
187 is_draft: false,
188 };
189 let json = serde_json::to_string(&m).unwrap();
190 let m2: FullMessage = serde_json::from_str(&json).unwrap();
191 assert_eq!(m, m2);
192 }
193
194 #[test]
195 fn attachment_roundtrips_through_json() {
196 let a = Attachment {
197 id: "att-1".into(),
198 name: "report.pdf".into(),
199 content_type: "application/pdf".into(),
200 size_bytes: 12345,
201 is_inline: false,
202 content_id: None,
203 };
204 let json = serde_json::to_string(&a).unwrap();
205 let a2: Attachment = serde_json::from_str(&json).unwrap();
206 assert_eq!(a, a2);
207 }
208
209 #[test]
210 fn body_content_type_serializes_lowercase() {
211 assert_eq!(
212 serde_json::to_string(&BodyContentType::Html).unwrap(),
213 "\"html\""
214 );
215 assert_eq!(
216 serde_json::to_string(&BodyContentType::Text).unwrap(),
217 "\"text\""
218 );
219 }
220}