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