Skip to main content

nylas_types/
message.rs

1//! Message types for the Nylas API v3.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Attachment, EmailAddress, FolderId, GrantId, MessageId, ThreadId, TrackingOptions};
6use std::fmt;
7
8/// Schedule ID for scheduled messages.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct ScheduleId(String);
12
13impl ScheduleId {
14    /// Create a new schedule ID.
15    pub fn new(id: impl Into<String>) -> Self {
16        Self(id.into())
17    }
18
19    /// Get the schedule ID as a string slice.
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23}
24
25impl fmt::Display for ScheduleId {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write!(f, "{}", self.0)
28    }
29}
30
31impl From<String> for ScheduleId {
32    fn from(s: String) -> Self {
33        Self(s)
34    }
35}
36
37impl From<&str> for ScheduleId {
38    fn from(s: &str) -> Self {
39        Self(s.to_string())
40    }
41}
42
43/// Email header from a message.
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
45pub struct EmailHeader {
46    /// Header name (e.g., "X-Custom-Header").
47    pub name: String,
48
49    /// Header value.
50    pub value: String,
51}
52
53/// A message object from the Nylas API.
54///
55/// Messages are email messages in a user's mailbox.
56///
57/// # Example
58///
59/// ```
60/// # use nylas_types::{Message, MessageId, GrantId, ThreadId};
61/// let message = Message {
62///     id: MessageId::new("msg_123"),
63///     grant_id: GrantId::new("grant_123"),
64///     thread_id: Some(ThreadId::new("thread_123")),
65///     subject: Some("Hello".to_string()),
66///     from: vec![],
67///     to: vec![],
68///     cc: vec![],
69///     bcc: vec![],
70///     reply_to: vec![],
71///     date: 1234567890,
72///     unread: Some(false),
73///     starred: Some(false),
74///     snippet: Some("Message snippet...".to_string()),
75///     body: Some("Message body".to_string()),
76///     attachments: vec![],
77///     folders: vec![],
78///     created_at: Some(1234567890),
79///     headers: None,
80///     raw_mime: None,
81///     bounce_detected: None,
82///     schedule_id: None,
83///     send_at: None,
84/// };
85/// ```
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Message {
88    /// Unique identifier for the message.
89    pub id: MessageId,
90
91    /// Grant ID associated with this message.
92    pub grant_id: GrantId,
93
94    /// Thread ID this message belongs to.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub thread_id: Option<ThreadId>,
97
98    /// Subject line of the message.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub subject: Option<String>,
101
102    /// Sender email address(es).
103    #[serde(default)]
104    pub from: Vec<EmailAddress>,
105
106    /// Recipient email address(es).
107    #[serde(default)]
108    pub to: Vec<EmailAddress>,
109
110    /// CC recipient email address(es).
111    #[serde(default)]
112    pub cc: Vec<EmailAddress>,
113
114    /// BCC recipient email address(es).
115    #[serde(default)]
116    pub bcc: Vec<EmailAddress>,
117
118    /// Reply-to email address(es).
119    #[serde(default)]
120    pub reply_to: Vec<EmailAddress>,
121
122    /// Unix timestamp when the message was sent/received.
123    pub date: i64,
124
125    /// Whether the message is unread.
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub unread: Option<bool>,
128
129    /// Whether the message is starred.
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub starred: Option<bool>,
132
133    /// Short snippet of the message body.
134    #[serde(skip_serializing_if = "Option::is_none")]
135    pub snippet: Option<String>,
136
137    /// Full body of the message.
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub body: Option<String>,
140
141    /// Attachments included with the message.
142    #[serde(default)]
143    pub attachments: Vec<Attachment>,
144
145    /// Folder IDs this message belongs to.
146    #[serde(default)]
147    pub folders: Vec<FolderId>,
148
149    /// Unix timestamp when the message was created.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub created_at: Option<i64>,
152
153    /// Email headers (returned when fields=include_headers is specified).
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub headers: Option<Vec<EmailHeader>>,
156
157    /// Raw MIME data (Base64url-encoded, returned when fields=raw_mime is specified).
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub raw_mime: Option<String>,
160
161    /// Whether bounce was detected for this message.
162    #[serde(skip_serializing_if = "Option::is_none")]
163    pub bounce_detected: Option<bool>,
164
165    /// Schedule ID if this message is scheduled to be sent.
166    #[serde(skip_serializing_if = "Option::is_none")]
167    pub schedule_id: Option<ScheduleId>,
168
169    /// Scheduled send time (Unix timestamp) if this message is scheduled.
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub send_at: Option<i64>,
172}
173
174/// Query parameters for listing messages.
175///
176/// # Example
177///
178/// ```
179/// # use nylas_types::MessageQueryParams;
180/// let params = MessageQueryParams::builder()
181///     .subject("invoice")
182///     .unread(true)
183///     .limit(50)
184///     .build();
185/// ```
186#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
187pub struct MessageQueryParams {
188    /// Filter by subject (substring match).
189    #[serde(skip_serializing_if = "Option::is_none")]
190    pub subject: Option<String>,
191
192    /// Filter by sender email.
193    #[serde(skip_serializing_if = "Option::is_none")]
194    pub from: Option<String>,
195
196    /// Filter by recipient email (to field).
197    #[serde(skip_serializing_if = "Option::is_none")]
198    pub to: Option<String>,
199
200    /// Filter by CC recipient email.
201    #[serde(skip_serializing_if = "Option::is_none")]
202    pub cc: Option<String>,
203
204    /// Filter by BCC recipient email.
205    #[serde(skip_serializing_if = "Option::is_none")]
206    pub bcc: Option<String>,
207
208    /// Filter by any email (to, from, cc, bcc).
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub any_email: Option<String>,
211
212    /// Filter by thread ID.
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub thread_id: Option<String>,
215
216    /// Filter by unread status.
217    #[serde(skip_serializing_if = "Option::is_none")]
218    pub unread: Option<bool>,
219
220    /// Filter by starred status.
221    #[serde(skip_serializing_if = "Option::is_none")]
222    pub starred: Option<bool>,
223
224    /// Filter by messages with attachments.
225    #[serde(skip_serializing_if = "Option::is_none")]
226    pub has_attachment: Option<bool>,
227
228    /// Filter messages received before this timestamp.
229    #[serde(skip_serializing_if = "Option::is_none")]
230    pub received_before: Option<i64>,
231
232    /// Filter messages received after this timestamp.
233    #[serde(skip_serializing_if = "Option::is_none")]
234    pub received_after: Option<i64>,
235
236    /// Filter by folder ID.
237    #[serde(skip_serializing_if = "Option::is_none")]
238    pub in_: Option<String>,
239
240    /// Maximum number of results to return.
241    #[serde(skip_serializing_if = "Option::is_none")]
242    pub limit: Option<u32>,
243
244    /// Page token for pagination.
245    #[serde(skip_serializing_if = "Option::is_none")]
246    pub page_token: Option<String>,
247
248    /// IMAP-specific search query.
249    ///
250    /// Uses IMAP SEARCH syntax. Only works with IMAP providers.
251    ///
252    /// # Example
253    ///
254    /// ```
255    /// use nylas_types::MessageQueryParams;
256    ///
257    /// let params = MessageQueryParams::builder()
258    ///     .query_imap("FROM \"sender@example.com\" SUBJECT \"invoice\"")
259    ///     .build();
260    /// ```
261    #[serde(skip_serializing_if = "Option::is_none")]
262    pub query_imap: Option<String>,
263}
264
265impl MessageQueryParams {
266    /// Create a new builder for message query parameters.
267    pub fn builder() -> MessageQueryParamsBuilder {
268        MessageQueryParamsBuilder::default()
269    }
270}
271
272/// Builder for message query parameters.
273#[derive(Debug, Clone, Default)]
274pub struct MessageQueryParamsBuilder {
275    params: MessageQueryParams,
276}
277
278impl MessageQueryParamsBuilder {
279    /// Filter by subject (substring match).
280    pub fn subject(mut self, subject: impl Into<String>) -> Self {
281        self.params.subject = Some(subject.into());
282        self
283    }
284
285    /// Filter by sender email.
286    pub fn from(mut self, from: impl Into<String>) -> Self {
287        self.params.from = Some(from.into());
288        self
289    }
290
291    /// Filter by recipient email (to field).
292    pub fn to(mut self, to: impl Into<String>) -> Self {
293        self.params.to = Some(to.into());
294        self
295    }
296
297    /// Filter by CC recipient email.
298    pub fn cc(mut self, cc: impl Into<String>) -> Self {
299        self.params.cc = Some(cc.into());
300        self
301    }
302
303    /// Filter by BCC recipient email.
304    pub fn bcc(mut self, bcc: impl Into<String>) -> Self {
305        self.params.bcc = Some(bcc.into());
306        self
307    }
308
309    /// Filter by any email (to, from, cc, bcc).
310    pub fn any_email(mut self, email: impl Into<String>) -> Self {
311        self.params.any_email = Some(email.into());
312        self
313    }
314
315    /// Filter by thread ID.
316    pub fn thread_id(mut self, thread_id: impl Into<String>) -> Self {
317        self.params.thread_id = Some(thread_id.into());
318        self
319    }
320
321    /// Filter by unread status.
322    pub fn unread(mut self, unread: bool) -> Self {
323        self.params.unread = Some(unread);
324        self
325    }
326
327    /// Filter by starred status.
328    pub fn starred(mut self, starred: bool) -> Self {
329        self.params.starred = Some(starred);
330        self
331    }
332
333    /// Filter by messages with attachments.
334    pub fn has_attachment(mut self, has_attachment: bool) -> Self {
335        self.params.has_attachment = Some(has_attachment);
336        self
337    }
338
339    /// Filter messages received before this timestamp.
340    pub fn received_before(mut self, timestamp: i64) -> Self {
341        self.params.received_before = Some(timestamp);
342        self
343    }
344
345    /// Filter messages received after this timestamp.
346    pub fn received_after(mut self, timestamp: i64) -> Self {
347        self.params.received_after = Some(timestamp);
348        self
349    }
350
351    /// Filter by folder ID.
352    pub fn in_folder(mut self, folder_id: impl Into<String>) -> Self {
353        self.params.in_ = Some(folder_id.into());
354        self
355    }
356
357    /// Maximum number of results to return.
358    pub fn limit(mut self, limit: u32) -> Self {
359        self.params.limit = Some(limit);
360        self
361    }
362
363    /// Page token for pagination.
364    pub fn page_token(mut self, token: impl Into<String>) -> Self {
365        self.params.page_token = Some(token.into());
366        self
367    }
368
369    /// Set IMAP-specific search query.
370    ///
371    /// Uses IMAP SEARCH syntax. Only works with IMAP providers.
372    ///
373    /// # Arguments
374    ///
375    /// * `query` - IMAP SEARCH query string
376    ///
377    /// # Example
378    ///
379    /// ```
380    /// use nylas_types::MessageQueryParams;
381    ///
382    /// let params = MessageQueryParams::builder()
383    ///     .query_imap("UNSEEN FROM \"boss@company.com\"")
384    ///     .limit(50)
385    ///     .build();
386    /// ```
387    pub fn query_imap(mut self, query: impl Into<String>) -> Self {
388        self.params.query_imap = Some(query.into());
389        self
390    }
391
392    /// Build the query parameters.
393    pub fn build(self) -> MessageQueryParams {
394        self.params
395    }
396}
397
398/// Request to update a message.
399///
400/// # Example
401///
402/// ```
403/// # use nylas_types::UpdateMessageRequest;
404/// let update = UpdateMessageRequest::builder()
405///     .unread(false)
406///     .starred(true)
407///     .build();
408/// ```
409#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
410pub struct UpdateMessageRequest {
411    /// Mark message as read/unread.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub unread: Option<bool>,
414
415    /// Mark message as starred/unstarred.
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub starred: Option<bool>,
418
419    /// Update folders the message belongs to.
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub folders: Option<Vec<String>>,
422}
423
424impl UpdateMessageRequest {
425    /// Create a new builder for update message request.
426    pub fn builder() -> UpdateMessageRequestBuilder {
427        UpdateMessageRequestBuilder::default()
428    }
429}
430
431/// Builder for update message request.
432#[derive(Debug, Clone, Default)]
433pub struct UpdateMessageRequestBuilder {
434    request: UpdateMessageRequest,
435}
436
437impl UpdateMessageRequestBuilder {
438    /// Mark message as read/unread.
439    pub fn unread(mut self, unread: bool) -> Self {
440        self.request.unread = Some(unread);
441        self
442    }
443
444    /// Mark message as starred/unstarred.
445    pub fn starred(mut self, starred: bool) -> Self {
446        self.request.starred = Some(starred);
447        self
448    }
449
450    /// Update folders the message belongs to.
451    pub fn folders(mut self, folders: Vec<String>) -> Self {
452        self.request.folders = Some(folders);
453        self
454    }
455
456    /// Build the update request.
457    pub fn build(self) -> UpdateMessageRequest {
458        self.request
459    }
460}
461
462/// Request to send a new message.
463///
464/// # Example
465///
466/// ```
467/// # use nylas_types::SendMessageRequest;
468/// let request = SendMessageRequest::builder()
469///     .subject("Hello")
470///     .body("Message body")
471///     .to(vec!["recipient@example.com".to_string()])
472///     .build();
473/// ```
474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
475pub struct SendMessageRequest {
476    /// Message subject.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub subject: Option<String>,
479
480    /// Message body (plain text or HTML).
481    #[serde(skip_serializing_if = "Option::is_none")]
482    pub body: Option<String>,
483
484    /// Recipient email addresses.
485    #[serde(default)]
486    pub to: Vec<String>,
487
488    /// CC recipient email addresses.
489    #[serde(default, skip_serializing_if = "Vec::is_empty")]
490    pub cc: Vec<String>,
491
492    /// BCC recipient email addresses.
493    #[serde(default, skip_serializing_if = "Vec::is_empty")]
494    pub bcc: Vec<String>,
495
496    /// Reply-to email addresses.
497    #[serde(default, skip_serializing_if = "Vec::is_empty")]
498    pub reply_to: Vec<String>,
499
500    /// Attachment IDs to include.
501    #[serde(default, skip_serializing_if = "Vec::is_empty")]
502    pub attachments: Vec<String>,
503
504    /// Unix timestamp to send the message (scheduled send).
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub send_at: Option<i64>,
507
508    /// Tracking options for this message.
509    #[serde(skip_serializing_if = "Option::is_none")]
510    pub tracking_options: Option<TrackingOptions>,
511
512    /// Reply to message ID.
513    #[serde(skip_serializing_if = "Option::is_none")]
514    pub reply_to_message_id: Option<String>,
515}
516
517impl SendMessageRequest {
518    /// Create a new builder for send message request.
519    pub fn builder() -> SendMessageRequestBuilder {
520        SendMessageRequestBuilder::default()
521    }
522}
523
524/// Builder for send message request.
525#[derive(Debug, Clone, Default)]
526pub struct SendMessageRequestBuilder {
527    request: SendMessageRequest,
528}
529
530impl SendMessageRequestBuilder {
531    /// Set the message subject.
532    pub fn subject(mut self, subject: impl Into<String>) -> Self {
533        self.request.subject = Some(subject.into());
534        self
535    }
536
537    /// Set the message body.
538    pub fn body(mut self, body: impl Into<String>) -> Self {
539        self.request.body = Some(body.into());
540        self
541    }
542
543    /// Set recipient email addresses.
544    pub fn to(mut self, to: Vec<String>) -> Self {
545        self.request.to = to;
546        self
547    }
548
549    /// Add a single recipient email address.
550    pub fn add_to(mut self, to: impl Into<String>) -> Self {
551        self.request.to.push(to.into());
552        self
553    }
554
555    /// Set CC recipient email addresses.
556    pub fn cc(mut self, cc: Vec<String>) -> Self {
557        self.request.cc = cc;
558        self
559    }
560
561    /// Add a single CC recipient email address.
562    pub fn add_cc(mut self, cc: impl Into<String>) -> Self {
563        self.request.cc.push(cc.into());
564        self
565    }
566
567    /// Set BCC recipient email addresses.
568    pub fn bcc(mut self, bcc: Vec<String>) -> Self {
569        self.request.bcc = bcc;
570        self
571    }
572
573    /// Add a single BCC recipient email address.
574    pub fn add_bcc(mut self, bcc: impl Into<String>) -> Self {
575        self.request.bcc.push(bcc.into());
576        self
577    }
578
579    /// Set reply-to email addresses.
580    pub fn reply_to(mut self, reply_to: Vec<String>) -> Self {
581        self.request.reply_to = reply_to;
582        self
583    }
584
585    /// Set attachment IDs.
586    pub fn attachments(mut self, attachments: Vec<String>) -> Self {
587        self.request.attachments = attachments;
588        self
589    }
590
591    /// Add a single attachment ID.
592    pub fn add_attachment(mut self, attachment_id: impl Into<String>) -> Self {
593        self.request.attachments.push(attachment_id.into());
594        self
595    }
596
597    /// Set scheduled send time (Unix timestamp).
598    pub fn send_at(mut self, timestamp: i64) -> Self {
599        self.request.send_at = Some(timestamp);
600        self
601    }
602
603    /// Set tracking options.
604    pub fn tracking(mut self, tracking: TrackingOptions) -> Self {
605        self.request.tracking_options = Some(tracking);
606        self
607    }
608
609    /// Set reply to message ID.
610    pub fn reply_to_message_id(mut self, message_id: impl Into<String>) -> Self {
611        self.request.reply_to_message_id = Some(message_id.into());
612        self
613    }
614
615    /// Build the send message request.
616    pub fn build(self) -> SendMessageRequest {
617        self.request
618    }
619}
620
621/// Request to clean message bodies.
622///
623/// Removes quoted text, signatures, and other non-essential content.
624///
625/// # Example
626///
627/// ```
628/// # use nylas_types::CleanMessagesRequest;
629/// let request = CleanMessagesRequest::builder()
630///     .message_ids(vec!["msg_1".to_string(), "msg_2".to_string()])
631///     .ignore_links(true)
632///     .build();
633/// ```
634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
635pub struct CleanMessagesRequest {
636    /// Message IDs to clean.
637    pub message_ids: Vec<String>,
638
639    /// Remove quoted text from previous emails.
640    #[serde(skip_serializing_if = "Option::is_none")]
641    pub ignore_quotes: Option<bool>,
642
643    /// Remove links from the message.
644    #[serde(skip_serializing_if = "Option::is_none")]
645    pub ignore_links: Option<bool>,
646
647    /// Remove images from the message.
648    #[serde(skip_serializing_if = "Option::is_none")]
649    pub ignore_images: Option<bool>,
650
651    /// Remove tables from the message.
652    #[serde(skip_serializing_if = "Option::is_none")]
653    pub ignore_tables: Option<bool>,
654}
655
656impl CleanMessagesRequest {
657    /// Create a new builder for clean messages request.
658    pub fn builder() -> CleanMessagesRequestBuilder {
659        CleanMessagesRequestBuilder::default()
660    }
661}
662
663/// Builder for clean messages request.
664#[derive(Debug, Clone, Default)]
665pub struct CleanMessagesRequestBuilder {
666    message_ids: Vec<String>,
667    ignore_quotes: Option<bool>,
668    ignore_links: Option<bool>,
669    ignore_images: Option<bool>,
670    ignore_tables: Option<bool>,
671}
672
673impl CleanMessagesRequestBuilder {
674    /// Set message IDs to clean.
675    pub fn message_ids(mut self, message_ids: Vec<String>) -> Self {
676        self.message_ids = message_ids;
677        self
678    }
679
680    /// Add a single message ID.
681    pub fn add_message_id(mut self, message_id: impl Into<String>) -> Self {
682        self.message_ids.push(message_id.into());
683        self
684    }
685
686    /// Set whether to ignore quotes.
687    pub fn ignore_quotes(mut self, ignore: bool) -> Self {
688        self.ignore_quotes = Some(ignore);
689        self
690    }
691
692    /// Set whether to ignore links.
693    pub fn ignore_links(mut self, ignore: bool) -> Self {
694        self.ignore_links = Some(ignore);
695        self
696    }
697
698    /// Set whether to ignore images.
699    pub fn ignore_images(mut self, ignore: bool) -> Self {
700        self.ignore_images = Some(ignore);
701        self
702    }
703
704    /// Set whether to ignore tables.
705    pub fn ignore_tables(mut self, ignore: bool) -> Self {
706        self.ignore_tables = Some(ignore);
707        self
708    }
709
710    /// Build the clean messages request.
711    pub fn build(self) -> CleanMessagesRequest {
712        CleanMessagesRequest {
713            message_ids: self.message_ids,
714            ignore_quotes: self.ignore_quotes,
715            ignore_links: self.ignore_links,
716            ignore_images: self.ignore_images,
717            ignore_tables: self.ignore_tables,
718        }
719    }
720}
721
722/// Response from clean messages API.
723#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
724pub struct CleanMessagesResponse {
725    /// Cleaned message bodies indexed by message ID.
726    pub cleaned: std::collections::HashMap<String, String>,
727}
728
729/// Response from clean single message endpoint.
730#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
731pub struct CleanMessageResponse {
732    /// Message ID.
733    #[serde(rename = "id")]
734    pub message_id: MessageId,
735
736    /// Cleaned conversation text (signature and quotes removed).
737    pub conversation: String,
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743
744    #[test]
745    fn test_message_creation() {
746        let message = Message {
747            id: MessageId::new("msg_123"),
748            grant_id: GrantId::new("grant_123"),
749            thread_id: Some(ThreadId::new("thread_123")),
750            subject: Some("Test Subject".to_string()),
751            from: vec![],
752            to: vec![],
753            cc: vec![],
754            bcc: vec![],
755            reply_to: vec![],
756            date: 1234567890,
757            unread: Some(true),
758            starred: Some(false),
759            snippet: Some("Test snippet".to_string()),
760            body: Some("Test body".to_string()),
761            attachments: vec![],
762            folders: vec![],
763            created_at: Some(1234567890),
764            headers: None,
765            raw_mime: None,
766            bounce_detected: None,
767            schedule_id: None,
768            send_at: None,
769        };
770
771        assert_eq!(message.id.as_str(), "msg_123");
772        assert_eq!(message.grant_id.as_str(), "grant_123");
773        assert_eq!(message.subject, Some("Test Subject".to_string()));
774        assert_eq!(message.unread, Some(true));
775    }
776
777    #[test]
778    fn test_message_serialization() {
779        let message = Message {
780            id: MessageId::new("msg_123"),
781            grant_id: GrantId::new("grant_123"),
782            thread_id: Some(ThreadId::new("thread_123")),
783            subject: Some("Test".to_string()),
784            from: vec![],
785            to: vec![],
786            cc: vec![],
787            bcc: vec![],
788            reply_to: vec![],
789            date: 1234567890,
790            unread: Some(true),
791            starred: Some(false),
792            snippet: Some("Snippet".to_string()),
793            body: Some("Body".to_string()),
794            attachments: vec![],
795            folders: vec![],
796            created_at: Some(1234567890),
797            headers: None,
798            raw_mime: None,
799            bounce_detected: None,
800            schedule_id: None,
801            send_at: None,
802        };
803
804        let json = serde_json::to_string(&message).unwrap();
805        assert!(json.contains("msg_123"));
806        assert!(json.contains("grant_123"));
807        assert!(json.contains("Test"));
808
809        let deserialized: Message = serde_json::from_str(&json).unwrap();
810        assert_eq!(deserialized, message);
811    }
812
813    #[test]
814    fn test_query_params_builder() {
815        let params = MessageQueryParams::builder()
816            .subject("invoice")
817            .unread(true)
818            .limit(50)
819            .build();
820
821        assert_eq!(params.subject, Some("invoice".to_string()));
822        assert_eq!(params.unread, Some(true));
823        assert_eq!(params.limit, Some(50));
824    }
825
826    #[test]
827    fn test_query_params_all_fields() {
828        let params = MessageQueryParams::builder()
829            .subject("test")
830            .from("sender@example.com")
831            .to("recipient@example.com")
832            .cc("cc@example.com")
833            .bcc("bcc@example.com")
834            .any_email("any@example.com")
835            .thread_id("thread_123")
836            .unread(true)
837            .starred(false)
838            .has_attachment(true)
839            .received_before(2000000000)
840            .received_after(1000000000)
841            .in_folder("folder_123")
842            .limit(100)
843            .page_token("token_abc")
844            .build();
845
846        assert_eq!(params.subject, Some("test".to_string()));
847        assert_eq!(params.from, Some("sender@example.com".to_string()));
848        assert_eq!(params.to, Some("recipient@example.com".to_string()));
849        assert_eq!(params.limit, Some(100));
850        assert_eq!(params.page_token, Some("token_abc".to_string()));
851    }
852
853    #[test]
854    fn test_query_params_serialization() {
855        let params = MessageQueryParams::builder()
856            .subject("test")
857            .unread(true)
858            .limit(50)
859            .build();
860
861        let json = serde_json::to_string(&params).unwrap();
862        assert!(json.contains("test"));
863        assert!(json.contains("true"));
864        assert!(json.contains("50"));
865    }
866
867    #[test]
868    fn test_update_message_request_builder() {
869        let update = UpdateMessageRequest::builder()
870            .unread(false)
871            .starred(true)
872            .build();
873
874        assert_eq!(update.unread, Some(false));
875        assert_eq!(update.starred, Some(true));
876    }
877
878    #[test]
879    fn test_update_message_request_with_folders() {
880        let update = UpdateMessageRequest::builder()
881            .unread(false)
882            .folders(vec!["folder1".to_string(), "folder2".to_string()])
883            .build();
884
885        assert_eq!(update.unread, Some(false));
886        assert_eq!(
887            update.folders,
888            Some(vec!["folder1".to_string(), "folder2".to_string()])
889        );
890    }
891
892    #[test]
893    fn test_update_message_request_serialization() {
894        let update = UpdateMessageRequest::builder()
895            .unread(false)
896            .starred(true)
897            .build();
898
899        let json = serde_json::to_string(&update).unwrap();
900        let deserialized: UpdateMessageRequest = serde_json::from_str(&json).unwrap();
901        assert_eq!(deserialized, update);
902    }
903
904    #[test]
905    fn test_send_message_request_builder() {
906        let request = SendMessageRequest::builder()
907            .subject("Test Subject")
908            .body("Test Body")
909            .to(vec!["recipient@example.com".to_string()])
910            .build();
911
912        assert_eq!(request.subject, Some("Test Subject".to_string()));
913        assert_eq!(request.body, Some("Test Body".to_string()));
914        assert_eq!(request.to, vec!["recipient@example.com".to_string()]);
915    }
916
917    #[test]
918    fn test_send_message_request_with_scheduled_send() {
919        let send_time = 1735689600; // 2025-01-01 00:00:00 UTC
920        let request = SendMessageRequest::builder()
921            .subject("Scheduled Email")
922            .body("This will be sent later")
923            .to(vec!["recipient@example.com".to_string()])
924            .send_at(send_time)
925            .build();
926
927        assert_eq!(request.send_at, Some(send_time));
928    }
929
930    #[test]
931    fn test_send_message_request_with_tracking() {
932        let tracking = TrackingOptions::all_enabled();
933        let request = SendMessageRequest::builder()
934            .subject("Tracked Email")
935            .body("This will track opens and clicks")
936            .to(vec!["recipient@example.com".to_string()])
937            .tracking(tracking.clone())
938            .build();
939
940        assert_eq!(request.tracking_options, Some(tracking));
941    }
942
943    #[test]
944    fn test_clean_messages_request() {
945        let request = CleanMessagesRequest::builder()
946            .message_ids(vec!["msg_1".to_string(), "msg_2".to_string()])
947            .ignore_links(true)
948            .ignore_images(false)
949            .build();
950
951        assert_eq!(request.message_ids.len(), 2);
952        assert_eq!(request.ignore_links, Some(true));
953        assert_eq!(request.ignore_images, Some(false));
954    }
955
956    #[test]
957    fn test_schedule_id_creation() {
958        let schedule_id = ScheduleId::new("schedule_123");
959        assert_eq!(schedule_id.as_str(), "schedule_123");
960    }
961
962    #[test]
963    fn test_schedule_id_from_string() {
964        let schedule_id: ScheduleId = "schedule_456".into();
965        assert_eq!(schedule_id.as_str(), "schedule_456");
966    }
967
968    #[test]
969    fn test_schedule_id_display() {
970        let schedule_id = ScheduleId::new("schedule_789");
971        assert_eq!(format!("{}", schedule_id), "schedule_789");
972    }
973
974    #[test]
975    fn test_email_header_serialization() {
976        let header = EmailHeader {
977            name: "X-Custom-Header".to_string(),
978            value: "custom-value".to_string(),
979        };
980
981        let json = serde_json::to_string(&header).unwrap();
982        assert!(json.contains("X-Custom-Header"));
983        assert!(json.contains("custom-value"));
984
985        let deserialized: EmailHeader = serde_json::from_str(&json).unwrap();
986        assert_eq!(deserialized.name, "X-Custom-Header");
987        assert_eq!(deserialized.value, "custom-value");
988    }
989
990    #[test]
991    fn test_clean_message_response_deserialization() {
992        let json = r#"{"id":"msg_123","conversation":"Clean text here"}"#;
993        let response: CleanMessageResponse = serde_json::from_str(json).unwrap();
994
995        assert_eq!(response.message_id.as_str(), "msg_123");
996        assert_eq!(response.conversation, "Clean text here");
997    }
998
999    #[test]
1000    fn test_message_with_headers() {
1001        let headers = vec![
1002            EmailHeader {
1003                name: "From".to_string(),
1004                value: "sender@example.com".to_string(),
1005            },
1006            EmailHeader {
1007                name: "Subject".to_string(),
1008                value: "Test Email".to_string(),
1009            },
1010        ];
1011
1012        let message = Message {
1013            id: MessageId::new("msg_123"),
1014            grant_id: GrantId::new("grant_123"),
1015            thread_id: None,
1016            subject: Some("Test".to_string()),
1017            from: vec![],
1018            to: vec![],
1019            cc: vec![],
1020            bcc: vec![],
1021            reply_to: vec![],
1022            date: 1234567890,
1023            unread: None,
1024            starred: None,
1025            snippet: None,
1026            body: None,
1027            attachments: vec![],
1028            folders: vec![],
1029            created_at: None,
1030            headers: Some(headers.clone()),
1031            raw_mime: None,
1032            bounce_detected: None,
1033            schedule_id: None,
1034            send_at: None,
1035        };
1036
1037        assert!(message.headers.is_some());
1038        let msg_headers = message.headers.unwrap();
1039        assert_eq!(msg_headers.len(), 2);
1040        assert_eq!(msg_headers[0].name, "From");
1041        assert_eq!(msg_headers[1].name, "Subject");
1042    }
1043
1044    #[test]
1045    fn test_message_with_bounce_detected() {
1046        let message = Message {
1047            id: MessageId::new("msg_123"),
1048            grant_id: GrantId::new("grant_123"),
1049            thread_id: None,
1050            subject: Some("Bounced".to_string()),
1051            from: vec![],
1052            to: vec![],
1053            cc: vec![],
1054            bcc: vec![],
1055            reply_to: vec![],
1056            date: 1234567890,
1057            unread: None,
1058            starred: None,
1059            snippet: None,
1060            body: None,
1061            attachments: vec![],
1062            folders: vec![],
1063            created_at: None,
1064            headers: None,
1065            raw_mime: None,
1066            bounce_detected: Some(true),
1067            schedule_id: None,
1068            send_at: None,
1069        };
1070
1071        assert_eq!(message.bounce_detected, Some(true));
1072    }
1073
1074    #[test]
1075    fn test_message_with_schedule_id() {
1076        let message = Message {
1077            id: MessageId::new("msg_123"),
1078            grant_id: GrantId::new("grant_123"),
1079            thread_id: None,
1080            subject: Some("Scheduled".to_string()),
1081            from: vec![],
1082            to: vec![],
1083            cc: vec![],
1084            bcc: vec![],
1085            reply_to: vec![],
1086            date: 1234567890,
1087            unread: None,
1088            starred: None,
1089            snippet: None,
1090            body: None,
1091            attachments: vec![],
1092            folders: vec![],
1093            created_at: None,
1094            headers: None,
1095            raw_mime: None,
1096            bounce_detected: None,
1097            schedule_id: Some(ScheduleId::new("schedule_123")),
1098            send_at: Some(1735776000),
1099        };
1100
1101        assert!(message.schedule_id.is_some());
1102        assert_eq!(message.schedule_id.unwrap().as_str(), "schedule_123");
1103        assert_eq!(message.send_at, Some(1735776000));
1104    }
1105
1106    #[test]
1107    fn test_query_imap_parameter() {
1108        let params = MessageQueryParams::builder()
1109            .query_imap("FROM \"test@example.com\"")
1110            .build();
1111
1112        assert_eq!(
1113            params.query_imap,
1114            Some("FROM \"test@example.com\"".to_string())
1115        );
1116    }
1117
1118    #[test]
1119    fn test_query_imap_serialization() {
1120        let params = MessageQueryParams {
1121            query_imap: Some("UNSEEN SUBJECT \"urgent\"".to_string()),
1122            ..Default::default()
1123        };
1124
1125        let json = serde_json::to_string(&params).unwrap();
1126        assert!(json.contains("query_imap"));
1127        assert!(json.contains("UNSEEN SUBJECT"));
1128    }
1129}