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}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct MessageFrom {
23 pub name: String,
24 pub address: String,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct FullMessage {
33 pub account: String,
34 pub id: String,
35 pub from: MessageFrom,
36 pub to: Vec<MessageFrom>,
37 pub cc: Vec<MessageFrom>,
38 pub bcc: Vec<MessageFrom>,
39 pub subject: String,
40 pub received_at: chrono::DateTime<chrono::Utc>,
41 pub sent_at: chrono::DateTime<chrono::Utc>,
42 pub is_read: bool,
43 pub body_content_type: BodyContentType,
44 pub body_content: String,
45 pub has_attachments: bool,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum BodyContentType {
51 Text,
52 Html,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub struct Attachment {
59 pub id: String,
61 pub name: String,
62 pub content_type: String,
63 pub size_bytes: u64,
64 pub is_inline: bool,
65 pub content_id: Option<String>,
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn message_roundtrips_through_json() {
74 let m = Message {
75 account: "a@b.com".into(),
76 id: "id-1".into(),
77 from: MessageFrom {
78 name: "Maria Lindberg".into(),
79 address: "maria@mklab.se".into(),
80 },
81 subject: "Quarterly numbers".into(),
82 received_at: DateTime::parse_from_rfc3339("2026-05-13T22:00:00Z")
83 .unwrap()
84 .to_utc(),
85 is_read: false,
86 preview: "Hi, attached are…".into(),
87 };
88 let json = serde_json::to_string(&m).unwrap();
89 let m2: Message = serde_json::from_str(&json).unwrap();
90 assert_eq!(m, m2);
91 }
92
93 #[test]
94 fn full_message_roundtrips_through_json() {
95 let m = FullMessage {
96 account: "u@e.com".into(),
97 id: "graph-id".into(),
98 from: MessageFrom {
99 name: "Maria".into(),
100 address: "maria@mklab.se".into(),
101 },
102 to: vec![MessageFrom {
103 name: "Kristofer".into(),
104 address: "kristofer@mklab.se".into(),
105 }],
106 cc: vec![],
107 bcc: vec![],
108 subject: "Hi".into(),
109 received_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T22:00:00Z")
110 .unwrap()
111 .to_utc(),
112 sent_at: chrono::DateTime::parse_from_rfc3339("2026-05-14T21:59:30Z")
113 .unwrap()
114 .to_utc(),
115 is_read: false,
116 body_content_type: BodyContentType::Html,
117 body_content: "<p>Hello</p>".into(),
118 has_attachments: true,
119 };
120 let json = serde_json::to_string(&m).unwrap();
121 let m2: FullMessage = serde_json::from_str(&json).unwrap();
122 assert_eq!(m, m2);
123 }
124
125 #[test]
126 fn attachment_roundtrips_through_json() {
127 let a = Attachment {
128 id: "att-1".into(),
129 name: "report.pdf".into(),
130 content_type: "application/pdf".into(),
131 size_bytes: 12345,
132 is_inline: false,
133 content_id: None,
134 };
135 let json = serde_json::to_string(&a).unwrap();
136 let a2: Attachment = serde_json::from_str(&json).unwrap();
137 assert_eq!(a, a2);
138 }
139
140 #[test]
141 fn body_content_type_serializes_lowercase() {
142 assert_eq!(
143 serde_json::to_string(&BodyContentType::Html).unwrap(),
144 "\"html\""
145 );
146 assert_eq!(
147 serde_json::to_string(&BodyContentType::Text).unwrap(),
148 "\"text\""
149 );
150 }
151}