Skip to main content

cardinal_core/
mail.rs

1//! Mail domain types.
2//!
3//! These types model local mail concepts without choosing a protocol or storage
4//! backend. Maildir, IMAP, JMAP, and test fixtures can all map into these types.
5
6use 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}