use std::marker::PhantomData;
use crate::calendar::InviteSummary;
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MailAccount {
pub name: String,
pub address: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mailbox {
pub account: String,
pub name: String,
pub unread_count: usize,
pub total_count: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageId(pub String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageSummary {
pub id: MessageId,
pub from: String,
pub subject: String,
pub date: String,
pub unread: bool,
pub has_invite: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageBody {
pub id: MessageId,
pub headers: Vec<(String, String)>,
pub plain_text: String,
pub attachments: Vec<AttachmentSummary>,
pub invite_summary: Option<InviteSummary>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachmentSummary {
pub index: usize,
pub filename: String,
pub content_type: String,
pub size_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Editing;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReadyToSend;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PendingConfirmation;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Confirmed;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutboundDraft<State> {
pub to: Vec<String>,
pub cc: Vec<String>,
pub bcc: Vec<String>,
pub subject: String,
pub body: String,
pub in_reply_to: Option<String>,
pub references: Vec<String>,
pub reply_all: bool,
_state: PhantomData<State>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SendRequest<State> {
draft: OutboundDraft<ReadyToSend>,
_state: PhantomData<State>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SentMessage {
pub envelope_to: Vec<String>,
pub subject: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum DraftValidationError {
#[error("missing recipient")]
MissingRecipient,
#[error("body exceeds maximum size: {bytes} bytes")]
BodyTooLarge { bytes: usize },
}
impl Mailbox {
pub fn new(
account: impl Into<String>,
name: impl Into<String>,
unread_count: usize,
total_count: usize,
) -> Self {
Self {
account: account.into(),
name: name.into(),
unread_count,
total_count,
}
}
}
impl OutboundDraft<Editing> {
pub fn new() -> Self {
Self {
to: Vec::new(),
cc: Vec::new(),
bcc: Vec::new(),
subject: String::new(),
body: String::new(),
in_reply_to: None,
references: Vec::new(),
reply_all: false,
_state: PhantomData,
}
}
pub fn with_to(mut self, recipients: Vec<String>) -> Self {
self.to = recipients;
self
}
pub fn with_cc(mut self, recipients: Vec<String>) -> Self {
self.cc = recipients;
self
}
pub fn with_bcc(mut self, recipients: Vec<String>) -> Self {
self.bcc = recipients;
self
}
pub fn with_subject(mut self, subject: impl Into<String>) -> Self {
self.subject = subject.into();
self
}
pub fn with_body(mut self, body: impl Into<String>) -> Self {
self.body = body.into();
self
}
pub fn with_in_reply_to(mut self, value: Option<String>) -> Self {
self.in_reply_to = value;
self
}
pub fn with_references(mut self, values: Vec<String>) -> Self {
self.references = values;
self
}
pub fn with_reply_all(mut self, reply_all: bool) -> Self {
self.reply_all = reply_all;
self
}
pub fn ready(self) -> Result<OutboundDraft<ReadyToSend>, DraftValidationError> {
if self.to.iter().all(|value| value.trim().is_empty()) {
return Err(DraftValidationError::MissingRecipient);
}
if self.body.len() > 1024 * 1024 {
return Err(DraftValidationError::BodyTooLarge {
bytes: self.body.len(),
});
}
Ok(OutboundDraft::<ReadyToSend> {
to: self.to,
cc: self.cc,
bcc: self.bcc,
subject: self.subject,
body: self.body,
in_reply_to: self.in_reply_to,
references: self.references,
reply_all: self.reply_all,
_state: PhantomData,
})
}
}
impl Default for OutboundDraft<Editing> {
fn default() -> Self {
Self::new()
}
}
impl OutboundDraft<ReadyToSend> {
pub fn prepare_send(self) -> SendRequest<PendingConfirmation> {
SendRequest {
draft: self,
_state: PhantomData,
}
}
pub fn envelope_recipients(&self) -> Vec<String> {
let mut recipients = Vec::new();
recipients.extend(self.to.iter().cloned());
recipients.extend(self.cc.iter().cloned());
recipients.extend(self.bcc.iter().cloned());
recipients
}
}
impl SendRequest<PendingConfirmation> {
pub fn confirm(self) -> SendRequest<Confirmed> {
SendRequest {
draft: self.draft,
_state: PhantomData,
}
}
pub fn draft(&self) -> &OutboundDraft<ReadyToSend> {
&self.draft
}
}
impl SendRequest<Confirmed> {
pub fn draft(&self) -> &OutboundDraft<ReadyToSend> {
&self.draft
}
pub fn into_draft(self) -> OutboundDraft<ReadyToSend> {
self.draft
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn creates_mailbox() {
let mailbox = Mailbox::new("personal", "inbox", 3, 10);
assert_eq!(mailbox.account, "personal");
assert_eq!(mailbox.name, "inbox");
assert_eq!(mailbox.unread_count, 3);
assert_eq!(mailbox.total_count, 10);
}
#[test]
fn draft_typestate_requires_recipient_and_confirmation() {
let editing = OutboundDraft::<Editing>::new()
.with_subject("hello")
.with_body("world");
assert_eq!(editing.ready(), Err(DraftValidationError::MissingRecipient));
let ready = OutboundDraft::<Editing>::new()
.with_to(vec!["alice@example.com".to_owned()])
.with_subject("hello")
.with_body("world")
.ready()
.expect("valid draft should become ready");
let request = ready.prepare_send();
assert_eq!(request.draft().to, vec!["alice@example.com".to_owned()]);
let confirmed = request.confirm();
assert_eq!(confirmed.draft().subject, "hello");
}
}