use serde::{Deserialize, Serialize};
use crate::{Attachment, EmailAddress, FolderId, GrantId, MessageId, ThreadId, TrackingOptions};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ScheduleId(String);
impl ScheduleId {
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ScheduleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<String> for ScheduleId {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for ScheduleId {
fn from(s: &str) -> Self {
Self(s.to_string())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EmailHeader {
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Message {
pub id: MessageId,
pub grant_id: GrantId,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<ThreadId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(default)]
pub from: Vec<EmailAddress>,
#[serde(default)]
pub to: Vec<EmailAddress>,
#[serde(default)]
pub cc: Vec<EmailAddress>,
#[serde(default)]
pub bcc: Vec<EmailAddress>,
#[serde(default)]
pub reply_to: Vec<EmailAddress>,
pub date: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub unread: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default)]
pub attachments: Vec<Attachment>,
#[serde(default)]
pub folders: Vec<FolderId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<Vec<EmailHeader>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw_mime: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bounce_detected: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub schedule_id: Option<ScheduleId>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_at: Option<i64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct MessageQueryParams {
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bcc: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub any_email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thread_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unread: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_attachment: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub received_before: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub received_after: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub in_: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub page_token: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_imap: Option<String>,
}
impl MessageQueryParams {
pub fn builder() -> MessageQueryParamsBuilder {
MessageQueryParamsBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct MessageQueryParamsBuilder {
params: MessageQueryParams,
}
impl MessageQueryParamsBuilder {
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.params.subject = Some(subject.into());
self
}
pub fn from(mut self, from: impl Into<String>) -> Self {
self.params.from = Some(from.into());
self
}
pub fn to(mut self, to: impl Into<String>) -> Self {
self.params.to = Some(to.into());
self
}
pub fn cc(mut self, cc: impl Into<String>) -> Self {
self.params.cc = Some(cc.into());
self
}
pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
self.params.bcc = Some(bcc.into());
self
}
pub fn any_email(mut self, email: impl Into<String>) -> Self {
self.params.any_email = Some(email.into());
self
}
pub fn thread_id(mut self, thread_id: impl Into<String>) -> Self {
self.params.thread_id = Some(thread_id.into());
self
}
pub fn unread(mut self, unread: bool) -> Self {
self.params.unread = Some(unread);
self
}
pub fn starred(mut self, starred: bool) -> Self {
self.params.starred = Some(starred);
self
}
pub fn has_attachment(mut self, has_attachment: bool) -> Self {
self.params.has_attachment = Some(has_attachment);
self
}
pub fn received_before(mut self, timestamp: i64) -> Self {
self.params.received_before = Some(timestamp);
self
}
pub fn received_after(mut self, timestamp: i64) -> Self {
self.params.received_after = Some(timestamp);
self
}
pub fn in_folder(mut self, folder_id: impl Into<String>) -> Self {
self.params.in_ = Some(folder_id.into());
self
}
pub fn limit(mut self, limit: u32) -> Self {
self.params.limit = Some(limit);
self
}
pub fn page_token(mut self, token: impl Into<String>) -> Self {
self.params.page_token = Some(token.into());
self
}
pub fn query_imap(mut self, query: impl Into<String>) -> Self {
self.params.query_imap = Some(query.into());
self
}
pub fn build(self) -> MessageQueryParams {
self.params
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UpdateMessageRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub unread: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub starred: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub folders: Option<Vec<String>>,
}
impl UpdateMessageRequest {
pub fn builder() -> UpdateMessageRequestBuilder {
UpdateMessageRequestBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct UpdateMessageRequestBuilder {
request: UpdateMessageRequest,
}
impl UpdateMessageRequestBuilder {
pub fn unread(mut self, unread: bool) -> Self {
self.request.unread = Some(unread);
self
}
pub fn starred(mut self, starred: bool) -> Self {
self.request.starred = Some(starred);
self
}
pub fn folders(mut self, folders: Vec<String>) -> Self {
self.request.folders = Some(folders);
self
}
pub fn build(self) -> UpdateMessageRequest {
self.request
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct SendMessageRequest {
#[serde(skip_serializing_if = "Option::is_none")]
pub subject: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub body: Option<String>,
#[serde(default)]
pub to: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub cc: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bcc: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reply_to: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub attachments: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub send_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tracking_options: Option<TrackingOptions>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reply_to_message_id: Option<String>,
}
impl SendMessageRequest {
pub fn builder() -> SendMessageRequestBuilder {
SendMessageRequestBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct SendMessageRequestBuilder {
request: SendMessageRequest,
}
impl SendMessageRequestBuilder {
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.request.subject = Some(subject.into());
self
}
pub fn body(mut self, body: impl Into<String>) -> Self {
self.request.body = Some(body.into());
self
}
pub fn to(mut self, to: Vec<String>) -> Self {
self.request.to = to;
self
}
pub fn add_to(mut self, to: impl Into<String>) -> Self {
self.request.to.push(to.into());
self
}
pub fn cc(mut self, cc: Vec<String>) -> Self {
self.request.cc = cc;
self
}
pub fn add_cc(mut self, cc: impl Into<String>) -> Self {
self.request.cc.push(cc.into());
self
}
pub fn bcc(mut self, bcc: Vec<String>) -> Self {
self.request.bcc = bcc;
self
}
pub fn add_bcc(mut self, bcc: impl Into<String>) -> Self {
self.request.bcc.push(bcc.into());
self
}
pub fn reply_to(mut self, reply_to: Vec<String>) -> Self {
self.request.reply_to = reply_to;
self
}
pub fn attachments(mut self, attachments: Vec<String>) -> Self {
self.request.attachments = attachments;
self
}
pub fn add_attachment(mut self, attachment_id: impl Into<String>) -> Self {
self.request.attachments.push(attachment_id.into());
self
}
pub fn send_at(mut self, timestamp: i64) -> Self {
self.request.send_at = Some(timestamp);
self
}
pub fn tracking(mut self, tracking: TrackingOptions) -> Self {
self.request.tracking_options = Some(tracking);
self
}
pub fn reply_to_message_id(mut self, message_id: impl Into<String>) -> Self {
self.request.reply_to_message_id = Some(message_id.into());
self
}
pub fn build(self) -> SendMessageRequest {
self.request
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CleanMessagesRequest {
pub message_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_quotes: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_links: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_images: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ignore_tables: Option<bool>,
}
impl CleanMessagesRequest {
pub fn builder() -> CleanMessagesRequestBuilder {
CleanMessagesRequestBuilder::default()
}
}
#[derive(Debug, Clone, Default)]
pub struct CleanMessagesRequestBuilder {
message_ids: Vec<String>,
ignore_quotes: Option<bool>,
ignore_links: Option<bool>,
ignore_images: Option<bool>,
ignore_tables: Option<bool>,
}
impl CleanMessagesRequestBuilder {
pub fn message_ids(mut self, message_ids: Vec<String>) -> Self {
self.message_ids = message_ids;
self
}
pub fn add_message_id(mut self, message_id: impl Into<String>) -> Self {
self.message_ids.push(message_id.into());
self
}
pub fn ignore_quotes(mut self, ignore: bool) -> Self {
self.ignore_quotes = Some(ignore);
self
}
pub fn ignore_links(mut self, ignore: bool) -> Self {
self.ignore_links = Some(ignore);
self
}
pub fn ignore_images(mut self, ignore: bool) -> Self {
self.ignore_images = Some(ignore);
self
}
pub fn ignore_tables(mut self, ignore: bool) -> Self {
self.ignore_tables = Some(ignore);
self
}
pub fn build(self) -> CleanMessagesRequest {
CleanMessagesRequest {
message_ids: self.message_ids,
ignore_quotes: self.ignore_quotes,
ignore_links: self.ignore_links,
ignore_images: self.ignore_images,
ignore_tables: self.ignore_tables,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CleanMessagesResponse {
pub cleaned: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CleanMessageResponse {
#[serde(rename = "id")]
pub message_id: MessageId,
pub conversation: String,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation() {
let message = Message {
id: MessageId::new("msg_123"),
grant_id: GrantId::new("grant_123"),
thread_id: Some(ThreadId::new("thread_123")),
subject: Some("Test Subject".to_string()),
from: vec![],
to: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
date: 1234567890,
unread: Some(true),
starred: Some(false),
snippet: Some("Test snippet".to_string()),
body: Some("Test body".to_string()),
attachments: vec![],
folders: vec![],
created_at: Some(1234567890),
headers: None,
raw_mime: None,
bounce_detected: None,
schedule_id: None,
send_at: None,
};
assert_eq!(message.id.as_str(), "msg_123");
assert_eq!(message.grant_id.as_str(), "grant_123");
assert_eq!(message.subject, Some("Test Subject".to_string()));
assert_eq!(message.unread, Some(true));
}
#[test]
fn test_message_serialization() {
let message = Message {
id: MessageId::new("msg_123"),
grant_id: GrantId::new("grant_123"),
thread_id: Some(ThreadId::new("thread_123")),
subject: Some("Test".to_string()),
from: vec![],
to: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
date: 1234567890,
unread: Some(true),
starred: Some(false),
snippet: Some("Snippet".to_string()),
body: Some("Body".to_string()),
attachments: vec![],
folders: vec![],
created_at: Some(1234567890),
headers: None,
raw_mime: None,
bounce_detected: None,
schedule_id: None,
send_at: None,
};
let json = serde_json::to_string(&message).unwrap();
assert!(json.contains("msg_123"));
assert!(json.contains("grant_123"));
assert!(json.contains("Test"));
let deserialized: Message = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, message);
}
#[test]
fn test_query_params_builder() {
let params = MessageQueryParams::builder()
.subject("invoice")
.unread(true)
.limit(50)
.build();
assert_eq!(params.subject, Some("invoice".to_string()));
assert_eq!(params.unread, Some(true));
assert_eq!(params.limit, Some(50));
}
#[test]
fn test_query_params_all_fields() {
let params = MessageQueryParams::builder()
.subject("test")
.from("sender@example.com")
.to("recipient@example.com")
.cc("cc@example.com")
.bcc("bcc@example.com")
.any_email("any@example.com")
.thread_id("thread_123")
.unread(true)
.starred(false)
.has_attachment(true)
.received_before(2000000000)
.received_after(1000000000)
.in_folder("folder_123")
.limit(100)
.page_token("token_abc")
.build();
assert_eq!(params.subject, Some("test".to_string()));
assert_eq!(params.from, Some("sender@example.com".to_string()));
assert_eq!(params.to, Some("recipient@example.com".to_string()));
assert_eq!(params.limit, Some(100));
assert_eq!(params.page_token, Some("token_abc".to_string()));
}
#[test]
fn test_query_params_serialization() {
let params = MessageQueryParams::builder()
.subject("test")
.unread(true)
.limit(50)
.build();
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("test"));
assert!(json.contains("true"));
assert!(json.contains("50"));
}
#[test]
fn test_update_message_request_builder() {
let update = UpdateMessageRequest::builder()
.unread(false)
.starred(true)
.build();
assert_eq!(update.unread, Some(false));
assert_eq!(update.starred, Some(true));
}
#[test]
fn test_update_message_request_with_folders() {
let update = UpdateMessageRequest::builder()
.unread(false)
.folders(vec!["folder1".to_string(), "folder2".to_string()])
.build();
assert_eq!(update.unread, Some(false));
assert_eq!(
update.folders,
Some(vec!["folder1".to_string(), "folder2".to_string()])
);
}
#[test]
fn test_update_message_request_serialization() {
let update = UpdateMessageRequest::builder()
.unread(false)
.starred(true)
.build();
let json = serde_json::to_string(&update).unwrap();
let deserialized: UpdateMessageRequest = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, update);
}
#[test]
fn test_send_message_request_builder() {
let request = SendMessageRequest::builder()
.subject("Test Subject")
.body("Test Body")
.to(vec!["recipient@example.com".to_string()])
.build();
assert_eq!(request.subject, Some("Test Subject".to_string()));
assert_eq!(request.body, Some("Test Body".to_string()));
assert_eq!(request.to, vec!["recipient@example.com".to_string()]);
}
#[test]
fn test_send_message_request_with_scheduled_send() {
let send_time = 1735689600; let request = SendMessageRequest::builder()
.subject("Scheduled Email")
.body("This will be sent later")
.to(vec!["recipient@example.com".to_string()])
.send_at(send_time)
.build();
assert_eq!(request.send_at, Some(send_time));
}
#[test]
fn test_send_message_request_with_tracking() {
let tracking = TrackingOptions::all_enabled();
let request = SendMessageRequest::builder()
.subject("Tracked Email")
.body("This will track opens and clicks")
.to(vec!["recipient@example.com".to_string()])
.tracking(tracking.clone())
.build();
assert_eq!(request.tracking_options, Some(tracking));
}
#[test]
fn test_clean_messages_request() {
let request = CleanMessagesRequest::builder()
.message_ids(vec!["msg_1".to_string(), "msg_2".to_string()])
.ignore_links(true)
.ignore_images(false)
.build();
assert_eq!(request.message_ids.len(), 2);
assert_eq!(request.ignore_links, Some(true));
assert_eq!(request.ignore_images, Some(false));
}
#[test]
fn test_schedule_id_creation() {
let schedule_id = ScheduleId::new("schedule_123");
assert_eq!(schedule_id.as_str(), "schedule_123");
}
#[test]
fn test_schedule_id_from_string() {
let schedule_id: ScheduleId = "schedule_456".into();
assert_eq!(schedule_id.as_str(), "schedule_456");
}
#[test]
fn test_schedule_id_display() {
let schedule_id = ScheduleId::new("schedule_789");
assert_eq!(format!("{}", schedule_id), "schedule_789");
}
#[test]
fn test_email_header_serialization() {
let header = EmailHeader {
name: "X-Custom-Header".to_string(),
value: "custom-value".to_string(),
};
let json = serde_json::to_string(&header).unwrap();
assert!(json.contains("X-Custom-Header"));
assert!(json.contains("custom-value"));
let deserialized: EmailHeader = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "X-Custom-Header");
assert_eq!(deserialized.value, "custom-value");
}
#[test]
fn test_clean_message_response_deserialization() {
let json = r#"{"id":"msg_123","conversation":"Clean text here"}"#;
let response: CleanMessageResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.message_id.as_str(), "msg_123");
assert_eq!(response.conversation, "Clean text here");
}
#[test]
fn test_message_with_headers() {
let headers = vec![
EmailHeader {
name: "From".to_string(),
value: "sender@example.com".to_string(),
},
EmailHeader {
name: "Subject".to_string(),
value: "Test Email".to_string(),
},
];
let message = Message {
id: MessageId::new("msg_123"),
grant_id: GrantId::new("grant_123"),
thread_id: None,
subject: Some("Test".to_string()),
from: vec![],
to: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
date: 1234567890,
unread: None,
starred: None,
snippet: None,
body: None,
attachments: vec![],
folders: vec![],
created_at: None,
headers: Some(headers.clone()),
raw_mime: None,
bounce_detected: None,
schedule_id: None,
send_at: None,
};
assert!(message.headers.is_some());
let msg_headers = message.headers.unwrap();
assert_eq!(msg_headers.len(), 2);
assert_eq!(msg_headers[0].name, "From");
assert_eq!(msg_headers[1].name, "Subject");
}
#[test]
fn test_message_with_bounce_detected() {
let message = Message {
id: MessageId::new("msg_123"),
grant_id: GrantId::new("grant_123"),
thread_id: None,
subject: Some("Bounced".to_string()),
from: vec![],
to: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
date: 1234567890,
unread: None,
starred: None,
snippet: None,
body: None,
attachments: vec![],
folders: vec![],
created_at: None,
headers: None,
raw_mime: None,
bounce_detected: Some(true),
schedule_id: None,
send_at: None,
};
assert_eq!(message.bounce_detected, Some(true));
}
#[test]
fn test_message_with_schedule_id() {
let message = Message {
id: MessageId::new("msg_123"),
grant_id: GrantId::new("grant_123"),
thread_id: None,
subject: Some("Scheduled".to_string()),
from: vec![],
to: vec![],
cc: vec![],
bcc: vec![],
reply_to: vec![],
date: 1234567890,
unread: None,
starred: None,
snippet: None,
body: None,
attachments: vec![],
folders: vec![],
created_at: None,
headers: None,
raw_mime: None,
bounce_detected: None,
schedule_id: Some(ScheduleId::new("schedule_123")),
send_at: Some(1735776000),
};
assert!(message.schedule_id.is_some());
assert_eq!(message.schedule_id.unwrap().as_str(), "schedule_123");
assert_eq!(message.send_at, Some(1735776000));
}
#[test]
fn test_query_imap_parameter() {
let params = MessageQueryParams::builder()
.query_imap("FROM \"test@example.com\"")
.build();
assert_eq!(
params.query_imap,
Some("FROM \"test@example.com\"".to_string())
);
}
#[test]
fn test_query_imap_serialization() {
let params = MessageQueryParams {
query_imap: Some("UNSEEN SUBJECT \"urgent\"".to_string()),
..Default::default()
};
let json = serde_json::to_string(¶ms).unwrap();
assert!(json.contains("query_imap"));
assert!(json.contains("UNSEEN SUBJECT"));
}
}