1use std::marker::PhantomData;
7
8use crate::calendar::InviteSummary;
9use thiserror::Error;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct MailAccount {
13 pub name: String,
14 pub address: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct Mailbox {
19 pub account: String,
20 pub name: String,
21 pub unread_count: usize,
22 pub total_count: usize,
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct MessageId(pub String);
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct MessageSummary {
30 pub id: MessageId,
31 pub from: String,
32 pub subject: String,
33 pub date: String,
34 pub unread: bool,
35 pub has_invite: bool,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct MessageBody {
40 pub id: MessageId,
41 pub headers: Vec<(String, String)>,
42 pub plain_text: String,
43 pub attachments: Vec<AttachmentSummary>,
44 pub invite_summary: Option<InviteSummary>,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq)]
48pub struct AttachmentSummary {
49 pub index: usize,
50 pub filename: String,
51 pub content_type: String,
52 pub size_bytes: Option<u64>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct Editing;
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct ReadyToSend;
60
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PendingConfirmation;
63
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Confirmed;
66
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct OutboundDraft<State> {
69 pub to: Vec<String>,
70 pub cc: Vec<String>,
71 pub bcc: Vec<String>,
72 pub subject: String,
73 pub body: String,
74 pub in_reply_to: Option<String>,
75 pub references: Vec<String>,
76 pub reply_all: bool,
77 _state: PhantomData<State>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct SendRequest<State> {
82 draft: OutboundDraft<ReadyToSend>,
83 _state: PhantomData<State>,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct SentMessage {
88 pub envelope_to: Vec<String>,
89 pub subject: String,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Error)]
93pub enum DraftValidationError {
94 #[error("missing recipient")]
95 MissingRecipient,
96 #[error("body exceeds maximum size: {bytes} bytes")]
97 BodyTooLarge { bytes: usize },
98}
99
100impl Mailbox {
101 pub fn new(
102 account: impl Into<String>,
103 name: impl Into<String>,
104 unread_count: usize,
105 total_count: usize,
106 ) -> Self {
107 Self {
108 account: account.into(),
109 name: name.into(),
110 unread_count,
111 total_count,
112 }
113 }
114}
115
116impl OutboundDraft<Editing> {
117 pub fn new() -> Self {
118 Self {
119 to: Vec::new(),
120 cc: Vec::new(),
121 bcc: Vec::new(),
122 subject: String::new(),
123 body: String::new(),
124 in_reply_to: None,
125 references: Vec::new(),
126 reply_all: false,
127 _state: PhantomData,
128 }
129 }
130
131 pub fn with_to(mut self, recipients: Vec<String>) -> Self {
132 self.to = recipients;
133 self
134 }
135
136 pub fn with_cc(mut self, recipients: Vec<String>) -> Self {
137 self.cc = recipients;
138 self
139 }
140
141 pub fn with_bcc(mut self, recipients: Vec<String>) -> Self {
142 self.bcc = recipients;
143 self
144 }
145
146 pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
147 self.subject = subject.into();
148 self
149 }
150
151 pub fn with_body(mut self, body: impl Into<String>) -> Self {
152 self.body = body.into();
153 self
154 }
155
156 pub fn with_in_reply_to(mut self, value: Option<String>) -> Self {
157 self.in_reply_to = value;
158 self
159 }
160
161 pub fn with_references(mut self, values: Vec<String>) -> Self {
162 self.references = values;
163 self
164 }
165
166 pub fn with_reply_all(mut self, reply_all: bool) -> Self {
167 self.reply_all = reply_all;
168 self
169 }
170
171 pub fn ready(self) -> Result<OutboundDraft<ReadyToSend>, DraftValidationError> {
172 if self.to.iter().all(|value| value.trim().is_empty()) {
173 return Err(DraftValidationError::MissingRecipient);
174 }
175 if self.body.len() > 1024 * 1024 {
176 return Err(DraftValidationError::BodyTooLarge {
177 bytes: self.body.len(),
178 });
179 }
180
181 Ok(OutboundDraft::<ReadyToSend> {
182 to: self.to,
183 cc: self.cc,
184 bcc: self.bcc,
185 subject: self.subject,
186 body: self.body,
187 in_reply_to: self.in_reply_to,
188 references: self.references,
189 reply_all: self.reply_all,
190 _state: PhantomData,
191 })
192 }
193}
194
195impl Default for OutboundDraft<Editing> {
196 fn default() -> Self {
197 Self::new()
198 }
199}
200
201impl OutboundDraft<ReadyToSend> {
202 pub fn prepare_send(self) -> SendRequest<PendingConfirmation> {
203 SendRequest {
204 draft: self,
205 _state: PhantomData,
206 }
207 }
208
209 pub fn envelope_recipients(&self) -> Vec<String> {
210 let mut recipients = Vec::new();
211 recipients.extend(self.to.iter().cloned());
212 recipients.extend(self.cc.iter().cloned());
213 recipients.extend(self.bcc.iter().cloned());
214 recipients
215 }
216}
217
218impl SendRequest<PendingConfirmation> {
219 pub fn confirm(self) -> SendRequest<Confirmed> {
220 SendRequest {
221 draft: self.draft,
222 _state: PhantomData,
223 }
224 }
225
226 pub fn draft(&self) -> &OutboundDraft<ReadyToSend> {
227 &self.draft
228 }
229}
230
231impl SendRequest<Confirmed> {
232 pub fn draft(&self) -> &OutboundDraft<ReadyToSend> {
233 &self.draft
234 }
235
236 pub fn into_draft(self) -> OutboundDraft<ReadyToSend> {
237 self.draft
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[test]
246 fn creates_mailbox() {
247 let mailbox = Mailbox::new("personal", "inbox", 3, 10);
248 assert_eq!(mailbox.account, "personal");
249 assert_eq!(mailbox.name, "inbox");
250 assert_eq!(mailbox.unread_count, 3);
251 assert_eq!(mailbox.total_count, 10);
252 }
253
254 #[test]
255 fn draft_typestate_requires_recipient_and_confirmation() {
256 let editing = OutboundDraft::<Editing>::new()
257 .with_subject("hello")
258 .with_body("world");
259 assert_eq!(editing.ready(), Err(DraftValidationError::MissingRecipient));
260
261 let ready = OutboundDraft::<Editing>::new()
262 .with_to(vec!["alice@example.com".to_owned()])
263 .with_subject("hello")
264 .with_body("world")
265 .ready()
266 .expect("valid draft should become ready");
267 let request = ready.prepare_send();
268 assert_eq!(request.draft().to, vec!["alice@example.com".to_owned()]);
269
270 let confirmed = request.confirm();
271 assert_eq!(confirmed.draft().subject, "hello");
272 }
273}