1use crate::types::JmapSetError;
10use chrono::{DateTime, Utc};
11use rusmes_storage::MessageStore;
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(rename_all = "camelCase")]
18pub struct EmailSubmission {
19 pub id: String,
21 pub identity_id: String,
23 pub email_id: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub thread_id: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub envelope: Option<Envelope>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub send_at: Option<DateTime<Utc>>,
34 pub undo_status: UndoStatus,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub delivery_status: Option<HashMap<String, DeliveryStatus>>,
39 #[serde(skip_serializing_if = "Option::is_none")]
41 pub dsn_blob_ids: Option<Vec<String>>,
42 #[serde(skip_serializing_if = "Option::is_none")]
44 pub mdn_blob_ids: Option<Vec<String>>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(rename_all = "camelCase")]
50pub struct Envelope {
51 pub mail_from: Address,
52 pub rcpt_to: Vec<Address>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct Address {
59 pub email: String,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub parameters: Option<HashMap<String, Option<String>>>,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub enum UndoStatus {
68 Pending,
69 Final,
70 Canceled,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75#[serde(rename_all = "camelCase")]
76pub struct DeliveryStatus {
77 pub smtp_reply: String,
78 pub delivered: DeliveryState,
79 pub displayed: DisplayedState,
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "camelCase")]
85pub enum DeliveryState {
86 Queued,
87 Yes,
88 No,
89 Unknown,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub enum DisplayedState {
96 Unknown,
97 Yes,
98 No,
99}
100
101#[derive(Debug, Clone, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct EmailSubmissionGetRequest {
105 pub account_id: String,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub ids: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub properties: Option<Vec<String>>,
110}
111
112#[derive(Debug, Clone, Serialize)]
114#[serde(rename_all = "camelCase")]
115pub struct EmailSubmissionGetResponse {
116 pub account_id: String,
117 pub state: String,
118 pub list: Vec<EmailSubmission>,
119 pub not_found: Vec<String>,
120}
121
122#[derive(Debug, Clone, Deserialize)]
124#[serde(rename_all = "camelCase")]
125pub struct EmailSubmissionSetRequest {
126 pub account_id: String,
127 #[serde(skip_serializing_if = "Option::is_none")]
128 pub if_in_state: Option<String>,
129 #[serde(skip_serializing_if = "Option::is_none")]
130 pub create: Option<HashMap<String, EmailSubmissionObject>>,
131 #[serde(skip_serializing_if = "Option::is_none")]
132 pub update: Option<HashMap<String, serde_json::Value>>,
133 #[serde(skip_serializing_if = "Option::is_none")]
134 pub destroy: Option<Vec<String>>,
135 #[serde(skip_serializing_if = "Option::is_none")]
136 pub on_success_update_email: Option<HashMap<String, serde_json::Value>>,
137 #[serde(skip_serializing_if = "Option::is_none")]
138 pub on_success_destroy_email: Option<Vec<String>>,
139}
140
141#[derive(Debug, Clone, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct EmailSubmissionObject {
145 pub identity_id: String,
146 pub email_id: String,
147 #[serde(skip_serializing_if = "Option::is_none")]
148 pub envelope: Option<Envelope>,
149 #[serde(skip_serializing_if = "Option::is_none")]
150 pub send_at: Option<DateTime<Utc>>,
151}
152
153#[derive(Debug, Clone, Serialize)]
155#[serde(rename_all = "camelCase")]
156pub struct EmailSubmissionSetResponse {
157 pub account_id: String,
158 pub old_state: String,
159 pub new_state: String,
160 #[serde(skip_serializing_if = "Option::is_none")]
161 pub created: Option<HashMap<String, EmailSubmission>>,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub updated: Option<HashMap<String, Option<EmailSubmission>>>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub destroyed: Option<Vec<String>>,
166 #[serde(skip_serializing_if = "Option::is_none")]
167 pub not_created: Option<HashMap<String, JmapSetError>>,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub not_updated: Option<HashMap<String, JmapSetError>>,
170 #[serde(skip_serializing_if = "Option::is_none")]
171 pub not_destroyed: Option<HashMap<String, JmapSetError>>,
172}
173
174#[derive(Debug, Clone, Deserialize)]
176#[serde(rename_all = "camelCase")]
177pub struct EmailSubmissionQueryRequest {
178 pub account_id: String,
179 #[serde(skip_serializing_if = "Option::is_none")]
180 pub filter: Option<EmailSubmissionFilterCondition>,
181 #[serde(skip_serializing_if = "Option::is_none")]
182 pub sort: Option<Vec<EmailSubmissionSort>>,
183 #[serde(skip_serializing_if = "Option::is_none")]
184 pub position: Option<i64>,
185 #[serde(skip_serializing_if = "Option::is_none")]
186 pub limit: Option<u64>,
187 #[serde(skip_serializing_if = "Option::is_none")]
188 pub calculate_total: Option<bool>,
189}
190
191#[derive(Debug, Clone, Deserialize)]
193#[serde(rename_all = "camelCase")]
194pub struct EmailSubmissionFilterCondition {
195 #[serde(skip_serializing_if = "Option::is_none")]
196 pub identity_ids: Option<Vec<String>>,
197 #[serde(skip_serializing_if = "Option::is_none")]
198 pub email_ids: Option<Vec<String>>,
199 #[serde(skip_serializing_if = "Option::is_none")]
200 pub thread_ids: Option<Vec<String>>,
201 #[serde(skip_serializing_if = "Option::is_none")]
202 pub undo_status: Option<UndoStatus>,
203 #[serde(skip_serializing_if = "Option::is_none")]
204 pub before: Option<DateTime<Utc>>,
205 #[serde(skip_serializing_if = "Option::is_none")]
206 pub after: Option<DateTime<Utc>>,
207}
208
209#[derive(Debug, Clone, Deserialize)]
211#[serde(rename_all = "camelCase")]
212pub struct EmailSubmissionSort {
213 pub property: String,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub is_ascending: Option<bool>,
216}
217
218#[derive(Debug, Clone, Serialize)]
220#[serde(rename_all = "camelCase")]
221pub struct EmailSubmissionQueryResponse {
222 pub account_id: String,
223 pub query_state: String,
224 pub can_calculate_changes: bool,
225 pub position: i64,
226 pub ids: Vec<String>,
227 #[serde(skip_serializing_if = "Option::is_none")]
228 pub total: Option<u64>,
229 #[serde(skip_serializing_if = "Option::is_none")]
230 pub limit: Option<u64>,
231}
232
233#[derive(Debug, Clone, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct EmailSubmissionChangesRequest {
237 pub account_id: String,
238 pub since_state: String,
239 #[serde(skip_serializing_if = "Option::is_none")]
240 pub max_changes: Option<u64>,
241}
242
243#[derive(Debug, Clone, Serialize)]
245#[serde(rename_all = "camelCase")]
246pub struct EmailSubmissionChangesResponse {
247 pub account_id: String,
248 pub old_state: String,
249 pub new_state: String,
250 pub has_more_changes: bool,
251 pub created: Vec<String>,
252 pub updated: Vec<String>,
253 pub destroyed: Vec<String>,
254}
255
256pub async fn email_submission_get(
258 request: EmailSubmissionGetRequest,
259 _message_store: &dyn MessageStore,
260) -> anyhow::Result<EmailSubmissionGetResponse> {
261 let list = Vec::new();
262 let mut not_found = Vec::new();
263
264 let ids = request.ids.unwrap_or_default();
266
267 for id in ids {
268 not_found.push(id);
270 }
271
272 Ok(EmailSubmissionGetResponse {
273 account_id: request.account_id,
274 state: "1".to_string(),
275 list,
276 not_found,
277 })
278}
279
280#[allow(clippy::too_many_arguments)]
282pub async fn email_submission_set(
283 request: EmailSubmissionSetRequest,
284 _message_store: &dyn MessageStore,
285) -> anyhow::Result<EmailSubmissionSetResponse> {
286 let created = HashMap::new();
287 let updated = HashMap::new();
288 let destroyed = Vec::new();
289 let mut not_created = HashMap::new();
290 let mut not_updated = HashMap::new();
291 let mut not_destroyed = HashMap::new();
292
293 if let Some(create_map) = request.create {
295 for (creation_id, _submission_obj) in create_map {
296 not_created.insert(
304 creation_id,
305 JmapSetError {
306 error_type: "notImplemented".to_string(),
307 description: Some("Email submission not yet implemented".to_string()),
308 },
309 );
310 }
311 }
312
313 if let Some(update_map) = request.update {
315 for (id, _patch) in update_map {
316 not_updated.insert(
318 id,
319 JmapSetError {
320 error_type: "notImplemented".to_string(),
321 description: Some("Submission update not yet implemented".to_string()),
322 },
323 );
324 }
325 }
326
327 if let Some(destroy_ids) = request.destroy {
329 for id in destroy_ids {
330 not_destroyed.insert(
331 id,
332 JmapSetError {
333 error_type: "notImplemented".to_string(),
334 description: Some("Submission deletion not yet implemented".to_string()),
335 },
336 );
337 }
338 }
339
340 Ok(EmailSubmissionSetResponse {
341 account_id: request.account_id,
342 old_state: "1".to_string(),
343 new_state: "2".to_string(),
344 created: if created.is_empty() {
345 None
346 } else {
347 Some(created)
348 },
349 updated: if updated.is_empty() {
350 None
351 } else {
352 Some(updated)
353 },
354 destroyed: if destroyed.is_empty() {
355 None
356 } else {
357 Some(destroyed)
358 },
359 not_created: if not_created.is_empty() {
360 None
361 } else {
362 Some(not_created)
363 },
364 not_updated: if not_updated.is_empty() {
365 None
366 } else {
367 Some(not_updated)
368 },
369 not_destroyed: if not_destroyed.is_empty() {
370 None
371 } else {
372 Some(not_destroyed)
373 },
374 })
375}
376
377pub async fn email_submission_query(
379 request: EmailSubmissionQueryRequest,
380 _message_store: &dyn MessageStore,
381) -> anyhow::Result<EmailSubmissionQueryResponse> {
382 let ids = Vec::new();
384
385 let position = request.position.unwrap_or(0);
386 let limit = request.limit.unwrap_or(100);
387
388 Ok(EmailSubmissionQueryResponse {
389 account_id: request.account_id,
390 query_state: "1".to_string(),
391 can_calculate_changes: false,
392 position,
393 ids,
394 total: if request.calculate_total.unwrap_or(false) {
395 Some(0)
396 } else {
397 None
398 },
399 limit: Some(limit),
400 })
401}
402
403pub async fn email_submission_changes(
405 request: EmailSubmissionChangesRequest,
406 _message_store: &dyn MessageStore,
407) -> anyhow::Result<EmailSubmissionChangesResponse> {
408 let since_state: u64 = request.since_state.parse().unwrap_or(0);
409 let new_state = (since_state + 1).to_string();
410
411 let created = Vec::new();
413 let updated = Vec::new();
414 let destroyed = Vec::new();
415
416 Ok(EmailSubmissionChangesResponse {
417 account_id: request.account_id,
418 old_state: request.since_state,
419 new_state,
420 has_more_changes: false,
421 created,
422 updated,
423 destroyed,
424 })
425}
426
427#[cfg(test)]
428mod tests {
429 use super::*;
430 use rusmes_storage::backends::filesystem::FilesystemBackend;
431 use rusmes_storage::StorageBackend;
432 use std::path::PathBuf;
433
434 fn create_test_store() -> std::sync::Arc<dyn MessageStore> {
435 let backend = FilesystemBackend::new(PathBuf::from("/tmp/rusmes-test-storage"));
436 backend.message_store()
437 }
438
439 #[tokio::test]
440 async fn test_email_submission_get() {
441 let store = create_test_store();
442 let request = EmailSubmissionGetRequest {
443 account_id: "acc1".to_string(),
444 ids: Some(vec!["sub1".to_string()]),
445 properties: None,
446 };
447
448 let response = email_submission_get(request, store.as_ref()).await.unwrap();
449 assert_eq!(response.account_id, "acc1");
450 assert_eq!(response.not_found.len(), 1);
451 }
452
453 #[tokio::test]
454 async fn test_email_submission_set_create() {
455 let store = create_test_store();
456 let mut create_map = HashMap::new();
457 create_map.insert(
458 "sub1".to_string(),
459 EmailSubmissionObject {
460 identity_id: "id1".to_string(),
461 email_id: "email1".to_string(),
462 envelope: None,
463 send_at: None,
464 },
465 );
466
467 let request = EmailSubmissionSetRequest {
468 account_id: "acc1".to_string(),
469 if_in_state: None,
470 create: Some(create_map),
471 update: None,
472 destroy: None,
473 on_success_update_email: None,
474 on_success_destroy_email: None,
475 };
476
477 let response = email_submission_set(request, store.as_ref()).await.unwrap();
478 assert_eq!(response.account_id, "acc1");
479 assert!(response.not_created.is_some());
480 }
481
482 #[tokio::test]
483 async fn test_email_submission_query() {
484 let store = create_test_store();
485 let request = EmailSubmissionQueryRequest {
486 account_id: "acc1".to_string(),
487 filter: None,
488 sort: None,
489 position: None,
490 limit: Some(50),
491 calculate_total: Some(true),
492 };
493
494 let response = email_submission_query(request, store.as_ref())
495 .await
496 .unwrap();
497 assert_eq!(response.account_id, "acc1");
498 assert_eq!(response.total, Some(0));
499 }
500
501 #[tokio::test]
502 async fn test_email_submission_changes() {
503 let store = create_test_store();
504 let request = EmailSubmissionChangesRequest {
505 account_id: "acc1".to_string(),
506 since_state: "1".to_string(),
507 max_changes: Some(50),
508 };
509
510 let response = email_submission_changes(request, store.as_ref())
511 .await
512 .unwrap();
513 assert_eq!(response.account_id, "acc1");
514 assert_eq!(response.old_state, "1");
515 assert!(!response.has_more_changes);
516 }
517
518 #[tokio::test]
519 async fn test_email_submission_delayed_send() {
520 let store = create_test_store();
521 let mut create_map = HashMap::new();
522 let send_at = Utc::now() + chrono::Duration::hours(2);
523
524 create_map.insert(
525 "delayed1".to_string(),
526 EmailSubmissionObject {
527 identity_id: "id1".to_string(),
528 email_id: "email1".to_string(),
529 envelope: None,
530 send_at: Some(send_at),
531 },
532 );
533
534 let request = EmailSubmissionSetRequest {
535 account_id: "acc1".to_string(),
536 if_in_state: None,
537 create: Some(create_map),
538 update: None,
539 destroy: None,
540 on_success_update_email: None,
541 on_success_destroy_email: None,
542 };
543
544 let response = email_submission_set(request, store.as_ref()).await.unwrap();
545 assert!(response.not_created.is_some());
546 }
547
548 #[tokio::test]
549 async fn test_email_submission_with_envelope() {
550 let store = create_test_store();
551 let mut create_map = HashMap::new();
552 let envelope = Envelope {
553 mail_from: Address {
554 email: "sender@example.com".to_string(),
555 parameters: None,
556 },
557 rcpt_to: vec![Address {
558 email: "recipient@example.com".to_string(),
559 parameters: None,
560 }],
561 };
562
563 create_map.insert(
564 "env1".to_string(),
565 EmailSubmissionObject {
566 identity_id: "id1".to_string(),
567 email_id: "email1".to_string(),
568 envelope: Some(envelope),
569 send_at: None,
570 },
571 );
572
573 let request = EmailSubmissionSetRequest {
574 account_id: "acc1".to_string(),
575 if_in_state: None,
576 create: Some(create_map),
577 update: None,
578 destroy: None,
579 on_success_update_email: None,
580 on_success_destroy_email: None,
581 };
582
583 let response = email_submission_set(request, store.as_ref()).await.unwrap();
584 assert!(response.not_created.is_some());
585 }
586
587 #[tokio::test]
588 async fn test_email_submission_query_with_filter() {
589 let store = create_test_store();
590 let filter = EmailSubmissionFilterCondition {
591 identity_ids: Some(vec!["id1".to_string()]),
592 email_ids: None,
593 thread_ids: None,
594 undo_status: Some(UndoStatus::Pending),
595 before: None,
596 after: None,
597 };
598
599 let request = EmailSubmissionQueryRequest {
600 account_id: "acc1".to_string(),
601 filter: Some(filter),
602 sort: None,
603 position: None,
604 limit: None,
605 calculate_total: Some(false),
606 };
607
608 let response = email_submission_query(request, store.as_ref())
609 .await
610 .unwrap();
611 assert!(response.total.is_none());
612 }
613
614 #[tokio::test]
615 async fn test_email_submission_update_undo_status() {
616 let store = create_test_store();
617 let mut update_map = HashMap::new();
618 update_map.insert(
619 "sub1".to_string(),
620 serde_json::json!({"undoStatus": "canceled"}),
621 );
622
623 let request = EmailSubmissionSetRequest {
624 account_id: "acc1".to_string(),
625 if_in_state: None,
626 create: None,
627 update: Some(update_map),
628 destroy: None,
629 on_success_update_email: None,
630 on_success_destroy_email: None,
631 };
632
633 let response = email_submission_set(request, store.as_ref()).await.unwrap();
634 assert!(response.not_updated.is_some());
635 }
636
637 #[tokio::test]
638 async fn test_email_submission_destroy() {
639 let store = create_test_store();
640 let request = EmailSubmissionSetRequest {
641 account_id: "acc1".to_string(),
642 if_in_state: None,
643 create: None,
644 update: None,
645 destroy: Some(vec!["sub1".to_string(), "sub2".to_string()]),
646 on_success_update_email: None,
647 on_success_destroy_email: None,
648 };
649
650 let response = email_submission_set(request, store.as_ref()).await.unwrap();
651 assert!(response.not_destroyed.is_some());
652 }
653
654 #[tokio::test]
655 async fn test_email_submission_on_success_actions() {
656 let store = create_test_store();
657 let mut create_map = HashMap::new();
658 create_map.insert(
659 "sub1".to_string(),
660 EmailSubmissionObject {
661 identity_id: "id1".to_string(),
662 email_id: "email1".to_string(),
663 envelope: None,
664 send_at: None,
665 },
666 );
667
668 let mut on_success_update = HashMap::new();
669 on_success_update.insert(
670 "email1".to_string(),
671 serde_json::json!({"keywords/$sent": true}),
672 );
673
674 let request = EmailSubmissionSetRequest {
675 account_id: "acc1".to_string(),
676 if_in_state: None,
677 create: Some(create_map),
678 update: None,
679 destroy: None,
680 on_success_update_email: Some(on_success_update),
681 on_success_destroy_email: None,
682 };
683
684 let response = email_submission_set(request, store.as_ref()).await.unwrap();
685 assert!(response.not_created.is_some());
686 }
687
688 #[tokio::test]
689 async fn test_email_submission_query_sort() {
690 let store = create_test_store();
691 let sort = vec![EmailSubmissionSort {
692 property: "sendAt".to_string(),
693 is_ascending: Some(false),
694 }];
695
696 let request = EmailSubmissionQueryRequest {
697 account_id: "acc1".to_string(),
698 filter: None,
699 sort: Some(sort),
700 position: Some(10),
701 limit: Some(25),
702 calculate_total: None,
703 };
704
705 let response = email_submission_query(request, store.as_ref())
706 .await
707 .unwrap();
708 assert_eq!(response.position, 10);
709 }
710
711 #[tokio::test]
712 async fn test_email_submission_multiple_recipients() {
713 let store = create_test_store();
714 let mut create_map = HashMap::new();
715 let envelope = Envelope {
716 mail_from: Address {
717 email: "sender@example.com".to_string(),
718 parameters: None,
719 },
720 rcpt_to: vec![
721 Address {
722 email: "recipient1@example.com".to_string(),
723 parameters: None,
724 },
725 Address {
726 email: "recipient2@example.com".to_string(),
727 parameters: None,
728 },
729 Address {
730 email: "recipient3@example.com".to_string(),
731 parameters: None,
732 },
733 ],
734 };
735
736 create_map.insert(
737 "multi1".to_string(),
738 EmailSubmissionObject {
739 identity_id: "id1".to_string(),
740 email_id: "email1".to_string(),
741 envelope: Some(envelope),
742 send_at: None,
743 },
744 );
745
746 let request = EmailSubmissionSetRequest {
747 account_id: "acc1".to_string(),
748 if_in_state: None,
749 create: Some(create_map),
750 update: None,
751 destroy: None,
752 on_success_update_email: None,
753 on_success_destroy_email: None,
754 };
755
756 let response = email_submission_set(request, store.as_ref()).await.unwrap();
757 assert!(response.not_created.is_some());
758 }
759
760 #[tokio::test]
761 async fn test_email_submission_changes_pagination() {
762 let store = create_test_store();
763 let request = EmailSubmissionChangesRequest {
764 account_id: "acc1".to_string(),
765 since_state: "100".to_string(),
766 max_changes: Some(10),
767 };
768
769 let response = email_submission_changes(request, store.as_ref())
770 .await
771 .unwrap();
772 assert_eq!(response.old_state, "100");
773 assert_eq!(response.new_state, "101");
774 }
775
776 #[tokio::test]
777 async fn test_email_submission_get_with_properties() {
778 let store = create_test_store();
779 let properties = vec![
780 "id".to_string(),
781 "identityId".to_string(),
782 "emailId".to_string(),
783 "undoStatus".to_string(),
784 "deliveryStatus".to_string(),
785 ];
786
787 let request = EmailSubmissionGetRequest {
788 account_id: "acc1".to_string(),
789 ids: Some(vec!["sub1".to_string()]),
790 properties: Some(properties),
791 };
792
793 let response = email_submission_get(request, store.as_ref()).await.unwrap();
794 assert_eq!(response.list.len(), 0);
795 }
796
797 #[tokio::test]
798 async fn test_email_submission_query_date_range() {
799 let store = create_test_store();
800 let now = Utc::now();
801 let filter = EmailSubmissionFilterCondition {
802 identity_ids: None,
803 email_ids: None,
804 thread_ids: None,
805 undo_status: None,
806 before: Some(now),
807 after: Some(now - chrono::Duration::days(7)),
808 };
809
810 let request = EmailSubmissionQueryRequest {
811 account_id: "acc1".to_string(),
812 filter: Some(filter),
813 sort: None,
814 position: None,
815 limit: None,
816 calculate_total: Some(true),
817 };
818
819 let response = email_submission_query(request, store.as_ref())
820 .await
821 .unwrap();
822 assert_eq!(response.total, Some(0));
823 }
824
825 #[tokio::test]
826 async fn test_email_submission_envelope_parameters() {
827 let store = create_test_store();
828 let mut create_map = HashMap::new();
829 let mut params = HashMap::new();
830 params.insert("SIZE".to_string(), Some("1024".to_string()));
831 params.insert("BODY".to_string(), Some("8BITMIME".to_string()));
832
833 let envelope = Envelope {
834 mail_from: Address {
835 email: "sender@example.com".to_string(),
836 parameters: Some(params),
837 },
838 rcpt_to: vec![Address {
839 email: "recipient@example.com".to_string(),
840 parameters: None,
841 }],
842 };
843
844 create_map.insert(
845 "params1".to_string(),
846 EmailSubmissionObject {
847 identity_id: "id1".to_string(),
848 email_id: "email1".to_string(),
849 envelope: Some(envelope),
850 send_at: None,
851 },
852 );
853
854 let request = EmailSubmissionSetRequest {
855 account_id: "acc1".to_string(),
856 if_in_state: None,
857 create: Some(create_map),
858 update: None,
859 destroy: None,
860 on_success_update_email: None,
861 on_success_destroy_email: None,
862 };
863
864 let response = email_submission_set(request, store.as_ref()).await.unwrap();
865 assert!(response.not_created.is_some());
866 }
867
868 #[tokio::test]
869 async fn test_email_submission_if_in_state() {
870 let store = create_test_store();
871 let request = EmailSubmissionSetRequest {
872 account_id: "acc1".to_string(),
873 if_in_state: Some("state123".to_string()),
874 create: None,
875 update: None,
876 destroy: None,
877 on_success_update_email: None,
878 on_success_destroy_email: None,
879 };
880
881 let response = email_submission_set(request, store.as_ref()).await.unwrap();
882 assert_eq!(response.old_state, "1");
883 }
884
885 #[tokio::test]
886 async fn test_email_submission_query_thread_filter() {
887 let store = create_test_store();
888 let filter = EmailSubmissionFilterCondition {
889 identity_ids: None,
890 email_ids: None,
891 thread_ids: Some(vec!["thread1".to_string(), "thread2".to_string()]),
892 undo_status: None,
893 before: None,
894 after: None,
895 };
896
897 let request = EmailSubmissionQueryRequest {
898 account_id: "acc1".to_string(),
899 filter: Some(filter),
900 sort: None,
901 position: None,
902 limit: None,
903 calculate_total: None,
904 };
905
906 let response = email_submission_query(request, store.as_ref())
907 .await
908 .unwrap();
909 assert_eq!(response.ids.len(), 0);
910 }
911
912 #[tokio::test]
913 async fn test_email_submission_undo_status_values() {
914 assert_eq!(
915 serde_json::to_string(&UndoStatus::Pending).unwrap(),
916 "\"pending\""
917 );
918 assert_eq!(
919 serde_json::to_string(&UndoStatus::Final).unwrap(),
920 "\"final\""
921 );
922 assert_eq!(
923 serde_json::to_string(&UndoStatus::Canceled).unwrap(),
924 "\"canceled\""
925 );
926 }
927
928 #[tokio::test]
929 async fn test_email_submission_delivery_states() {
930 assert_eq!(
931 serde_json::to_string(&DeliveryState::Queued).unwrap(),
932 "\"queued\""
933 );
934 assert_eq!(
935 serde_json::to_string(&DeliveryState::Yes).unwrap(),
936 "\"yes\""
937 );
938 assert_eq!(serde_json::to_string(&DeliveryState::No).unwrap(), "\"no\"");
939 assert_eq!(
940 serde_json::to_string(&DeliveryState::Unknown).unwrap(),
941 "\"unknown\""
942 );
943 }
944
945 #[tokio::test]
946 async fn test_email_submission_get_all() {
947 let store = create_test_store();
948 let request = EmailSubmissionGetRequest {
949 account_id: "acc1".to_string(),
950 ids: None,
951 properties: None,
952 };
953
954 let response = email_submission_get(request, store.as_ref()).await.unwrap();
955 assert_eq!(response.list.len(), 0);
956 }
957
958 #[tokio::test]
959 async fn test_email_submission_batch_create() {
960 let store = create_test_store();
961 let mut create_map = HashMap::new();
962
963 for i in 1..=10 {
964 create_map.insert(
965 format!("sub{}", i),
966 EmailSubmissionObject {
967 identity_id: format!("id{}", i),
968 email_id: format!("email{}", i),
969 envelope: None,
970 send_at: None,
971 },
972 );
973 }
974
975 let request = EmailSubmissionSetRequest {
976 account_id: "acc1".to_string(),
977 if_in_state: None,
978 create: Some(create_map),
979 update: None,
980 destroy: None,
981 on_success_update_email: None,
982 on_success_destroy_email: None,
983 };
984
985 let response = email_submission_set(request, store.as_ref()).await.unwrap();
986 assert_eq!(response.not_created.unwrap().len(), 10);
987 }
988
989 #[tokio::test]
990 async fn test_email_submission_on_success_destroy() {
991 let store = create_test_store();
992 let mut create_map = HashMap::new();
993 create_map.insert(
994 "sub1".to_string(),
995 EmailSubmissionObject {
996 identity_id: "id1".to_string(),
997 email_id: "draft1".to_string(),
998 envelope: None,
999 send_at: None,
1000 },
1001 );
1002
1003 let request = EmailSubmissionSetRequest {
1004 account_id: "acc1".to_string(),
1005 if_in_state: None,
1006 create: Some(create_map),
1007 update: None,
1008 destroy: None,
1009 on_success_update_email: None,
1010 on_success_destroy_email: Some(vec!["draft1".to_string()]),
1011 };
1012
1013 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1014 assert!(response.not_created.is_some());
1015 }
1016
1017 #[tokio::test]
1018 async fn test_email_submission_delayed_send_past() {
1019 let store = create_test_store();
1020 let mut create_map = HashMap::new();
1021 let send_at = Utc::now() - chrono::Duration::hours(1);
1022
1023 create_map.insert(
1024 "past1".to_string(),
1025 EmailSubmissionObject {
1026 identity_id: "id1".to_string(),
1027 email_id: "email1".to_string(),
1028 envelope: None,
1029 send_at: Some(send_at),
1030 },
1031 );
1032
1033 let request = EmailSubmissionSetRequest {
1034 account_id: "acc1".to_string(),
1035 if_in_state: None,
1036 create: Some(create_map),
1037 update: None,
1038 destroy: None,
1039 on_success_update_email: None,
1040 on_success_destroy_email: None,
1041 };
1042
1043 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1044 assert!(response.not_created.is_some());
1045 }
1046
1047 #[tokio::test]
1048 async fn test_email_submission_empty_envelope_recipients() {
1049 let store = create_test_store();
1050 let mut create_map = HashMap::new();
1051 let envelope = Envelope {
1052 mail_from: Address {
1053 email: "sender@example.com".to_string(),
1054 parameters: None,
1055 },
1056 rcpt_to: vec![],
1057 };
1058
1059 create_map.insert(
1060 "empty1".to_string(),
1061 EmailSubmissionObject {
1062 identity_id: "id1".to_string(),
1063 email_id: "email1".to_string(),
1064 envelope: Some(envelope),
1065 send_at: None,
1066 },
1067 );
1068
1069 let request = EmailSubmissionSetRequest {
1070 account_id: "acc1".to_string(),
1071 if_in_state: None,
1072 create: Some(create_map),
1073 update: None,
1074 destroy: None,
1075 on_success_update_email: None,
1076 on_success_destroy_email: None,
1077 };
1078
1079 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1080 assert!(response.not_created.is_some());
1081 }
1082
1083 #[tokio::test]
1084 async fn test_email_submission_query_email_ids_filter() {
1085 let store = create_test_store();
1086 let filter = EmailSubmissionFilterCondition {
1087 identity_ids: None,
1088 email_ids: Some(vec!["email1".to_string(), "email2".to_string()]),
1089 thread_ids: None,
1090 undo_status: None,
1091 before: None,
1092 after: None,
1093 };
1094
1095 let request = EmailSubmissionQueryRequest {
1096 account_id: "acc1".to_string(),
1097 filter: Some(filter),
1098 sort: None,
1099 position: None,
1100 limit: None,
1101 calculate_total: Some(true),
1102 };
1103
1104 let response = email_submission_query(request, store.as_ref())
1105 .await
1106 .unwrap();
1107 assert_eq!(response.total, Some(0));
1108 }
1109
1110 #[tokio::test]
1111 async fn test_email_submission_query_final_status() {
1112 let store = create_test_store();
1113 let filter = EmailSubmissionFilterCondition {
1114 identity_ids: None,
1115 email_ids: None,
1116 thread_ids: None,
1117 undo_status: Some(UndoStatus::Final),
1118 before: None,
1119 after: None,
1120 };
1121
1122 let request = EmailSubmissionQueryRequest {
1123 account_id: "acc1".to_string(),
1124 filter: Some(filter),
1125 sort: None,
1126 position: None,
1127 limit: None,
1128 calculate_total: None,
1129 };
1130
1131 let response = email_submission_query(request, store.as_ref())
1132 .await
1133 .unwrap();
1134 assert_eq!(response.ids.len(), 0);
1135 }
1136
1137 #[tokio::test]
1138 async fn test_email_submission_query_canceled_status() {
1139 let store = create_test_store();
1140 let filter = EmailSubmissionFilterCondition {
1141 identity_ids: None,
1142 email_ids: None,
1143 thread_ids: None,
1144 undo_status: Some(UndoStatus::Canceled),
1145 before: None,
1146 after: None,
1147 };
1148
1149 let request = EmailSubmissionQueryRequest {
1150 account_id: "acc1".to_string(),
1151 filter: Some(filter),
1152 sort: None,
1153 position: None,
1154 limit: None,
1155 calculate_total: None,
1156 };
1157
1158 let response = email_submission_query(request, store.as_ref())
1159 .await
1160 .unwrap();
1161 assert_eq!(response.ids.len(), 0);
1162 }
1163
1164 #[tokio::test]
1165 async fn test_email_submission_mixed_operations() {
1166 let store = create_test_store();
1167 let mut create_map = HashMap::new();
1168 create_map.insert(
1169 "new1".to_string(),
1170 EmailSubmissionObject {
1171 identity_id: "id1".to_string(),
1172 email_id: "email1".to_string(),
1173 envelope: None,
1174 send_at: None,
1175 },
1176 );
1177
1178 let mut update_map = HashMap::new();
1179 update_map.insert(
1180 "sub1".to_string(),
1181 serde_json::json!({"undoStatus": "canceled"}),
1182 );
1183
1184 let request = EmailSubmissionSetRequest {
1185 account_id: "acc1".to_string(),
1186 if_in_state: None,
1187 create: Some(create_map),
1188 update: Some(update_map),
1189 destroy: Some(vec!["sub2".to_string()]),
1190 on_success_update_email: None,
1191 on_success_destroy_email: None,
1192 };
1193
1194 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1195 assert!(response.not_created.is_some());
1196 assert!(response.not_updated.is_some());
1197 assert!(response.not_destroyed.is_some());
1198 }
1199
1200 #[tokio::test]
1201 async fn test_email_submission_changes_zero_state() {
1202 let store = create_test_store();
1203 let request = EmailSubmissionChangesRequest {
1204 account_id: "acc1".to_string(),
1205 since_state: "0".to_string(),
1206 max_changes: None,
1207 };
1208
1209 let response = email_submission_changes(request, store.as_ref())
1210 .await
1211 .unwrap();
1212 assert_eq!(response.old_state, "0");
1213 assert_eq!(response.new_state, "1");
1214 }
1215
1216 #[tokio::test]
1217 async fn test_displayed_state_serialization() {
1218 assert_eq!(
1219 serde_json::to_string(&DisplayedState::Unknown).unwrap(),
1220 "\"unknown\""
1221 );
1222 assert_eq!(
1223 serde_json::to_string(&DisplayedState::Yes).unwrap(),
1224 "\"yes\""
1225 );
1226 assert_eq!(
1227 serde_json::to_string(&DisplayedState::No).unwrap(),
1228 "\"no\""
1229 );
1230 }
1231
1232 #[tokio::test]
1233 async fn test_email_submission_get_multiple_ids() {
1234 let store = create_test_store();
1235 let request = EmailSubmissionGetRequest {
1236 account_id: "acc1".to_string(),
1237 ids: Some(vec![
1238 "sub1".to_string(),
1239 "sub2".to_string(),
1240 "sub3".to_string(),
1241 ]),
1242 properties: None,
1243 };
1244
1245 let response = email_submission_get(request, store.as_ref()).await.unwrap();
1246 assert_eq!(response.not_found.len(), 3);
1247 }
1248
1249 #[tokio::test]
1250 async fn test_email_submission_query_multiple_sorts() {
1251 let store = create_test_store();
1252 let sort = vec![
1253 EmailSubmissionSort {
1254 property: "sendAt".to_string(),
1255 is_ascending: Some(false),
1256 },
1257 EmailSubmissionSort {
1258 property: "emailId".to_string(),
1259 is_ascending: Some(true),
1260 },
1261 ];
1262
1263 let request = EmailSubmissionQueryRequest {
1264 account_id: "acc1".to_string(),
1265 filter: None,
1266 sort: Some(sort),
1267 position: None,
1268 limit: None,
1269 calculate_total: None,
1270 };
1271
1272 let response = email_submission_query(request, store.as_ref())
1273 .await
1274 .unwrap();
1275 assert_eq!(response.ids.len(), 0);
1276 }
1277
1278 #[tokio::test]
1279 async fn test_email_submission_query_complex_filter() {
1280 let store = create_test_store();
1281 let now = Utc::now();
1282 let filter = EmailSubmissionFilterCondition {
1283 identity_ids: Some(vec!["id1".to_string(), "id2".to_string()]),
1284 email_ids: Some(vec!["email1".to_string()]),
1285 thread_ids: Some(vec!["thread1".to_string()]),
1286 undo_status: Some(UndoStatus::Pending),
1287 before: Some(now),
1288 after: Some(now - chrono::Duration::days(30)),
1289 };
1290
1291 let request = EmailSubmissionQueryRequest {
1292 account_id: "acc1".to_string(),
1293 filter: Some(filter),
1294 sort: None,
1295 position: Some(5),
1296 limit: Some(10),
1297 calculate_total: Some(true),
1298 };
1299
1300 let response = email_submission_query(request, store.as_ref())
1301 .await
1302 .unwrap();
1303 assert_eq!(response.position, 5);
1304 assert_eq!(response.limit, Some(10));
1305 }
1306
1307 #[tokio::test]
1308 async fn test_email_submission_envelope_with_dsn_params() {
1309 let store = create_test_store();
1310 let mut create_map = HashMap::new();
1311 let mut params = HashMap::new();
1312 params.insert("RET".to_string(), Some("FULL".to_string()));
1313 params.insert("ENVID".to_string(), Some("QQ314159".to_string()));
1314
1315 let envelope = Envelope {
1316 mail_from: Address {
1317 email: "sender@example.com".to_string(),
1318 parameters: Some(params),
1319 },
1320 rcpt_to: vec![Address {
1321 email: "recipient@example.com".to_string(),
1322 parameters: None,
1323 }],
1324 };
1325
1326 create_map.insert(
1327 "dsn1".to_string(),
1328 EmailSubmissionObject {
1329 identity_id: "id1".to_string(),
1330 email_id: "email1".to_string(),
1331 envelope: Some(envelope),
1332 send_at: None,
1333 },
1334 );
1335
1336 let request = EmailSubmissionSetRequest {
1337 account_id: "acc1".to_string(),
1338 if_in_state: None,
1339 create: Some(create_map),
1340 update: None,
1341 destroy: None,
1342 on_success_update_email: None,
1343 on_success_destroy_email: None,
1344 };
1345
1346 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1347 assert!(response.not_created.is_some());
1348 }
1349
1350 #[tokio::test]
1351 async fn test_email_submission_with_recipient_params() {
1352 let store = create_test_store();
1353 let mut create_map = HashMap::new();
1354 let mut rcpt_params = HashMap::new();
1355 rcpt_params.insert("NOTIFY".to_string(), Some("SUCCESS,FAILURE".to_string()));
1356
1357 let envelope = Envelope {
1358 mail_from: Address {
1359 email: "sender@example.com".to_string(),
1360 parameters: None,
1361 },
1362 rcpt_to: vec![Address {
1363 email: "recipient@example.com".to_string(),
1364 parameters: Some(rcpt_params),
1365 }],
1366 };
1367
1368 create_map.insert(
1369 "notify1".to_string(),
1370 EmailSubmissionObject {
1371 identity_id: "id1".to_string(),
1372 email_id: "email1".to_string(),
1373 envelope: Some(envelope),
1374 send_at: None,
1375 },
1376 );
1377
1378 let request = EmailSubmissionSetRequest {
1379 account_id: "acc1".to_string(),
1380 if_in_state: None,
1381 create: Some(create_map),
1382 update: None,
1383 destroy: None,
1384 on_success_update_email: None,
1385 on_success_destroy_email: None,
1386 };
1387
1388 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1389 assert!(response.not_created.is_some());
1390 }
1391
1392 #[tokio::test]
1393 async fn test_email_submission_destroy_multiple() {
1394 let store = create_test_store();
1395 let request = EmailSubmissionSetRequest {
1396 account_id: "acc1".to_string(),
1397 if_in_state: None,
1398 create: None,
1399 update: None,
1400 destroy: Some(vec![
1401 "sub1".to_string(),
1402 "sub2".to_string(),
1403 "sub3".to_string(),
1404 "sub4".to_string(),
1405 "sub5".to_string(),
1406 ]),
1407 on_success_update_email: None,
1408 on_success_destroy_email: None,
1409 };
1410
1411 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1412 assert_eq!(response.not_destroyed.unwrap().len(), 5);
1413 }
1414
1415 #[tokio::test]
1416 async fn test_email_submission_query_position_and_limit() {
1417 let store = create_test_store();
1418 let request = EmailSubmissionQueryRequest {
1419 account_id: "acc1".to_string(),
1420 filter: None,
1421 sort: None,
1422 position: Some(100),
1423 limit: Some(5),
1424 calculate_total: Some(true),
1425 };
1426
1427 let response = email_submission_query(request, store.as_ref())
1428 .await
1429 .unwrap();
1430 assert_eq!(response.position, 100);
1431 assert_eq!(response.limit, Some(5));
1432 assert_eq!(response.total, Some(0));
1433 }
1434
1435 #[tokio::test]
1436 async fn test_email_submission_empty_request() {
1437 let store = create_test_store();
1438 let request = EmailSubmissionSetRequest {
1439 account_id: "acc1".to_string(),
1440 if_in_state: None,
1441 create: None,
1442 update: None,
1443 destroy: None,
1444 on_success_update_email: None,
1445 on_success_destroy_email: None,
1446 };
1447
1448 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1449 assert!(response.created.is_none());
1450 assert!(response.updated.is_none());
1451 assert!(response.destroyed.is_none());
1452 }
1453
1454 #[tokio::test]
1455 async fn test_email_submission_query_default_limit() {
1456 let store = create_test_store();
1457 let request = EmailSubmissionQueryRequest {
1458 account_id: "acc1".to_string(),
1459 filter: None,
1460 sort: None,
1461 position: None,
1462 limit: None,
1463 calculate_total: None,
1464 };
1465
1466 let response = email_submission_query(request, store.as_ref())
1467 .await
1468 .unwrap();
1469 assert_eq!(response.limit, Some(100));
1470 }
1471
1472 #[tokio::test]
1473 async fn test_email_submission_changes_with_max_changes() {
1474 let store = create_test_store();
1475 let request = EmailSubmissionChangesRequest {
1476 account_id: "acc1".to_string(),
1477 since_state: "5".to_string(),
1478 max_changes: Some(100),
1479 };
1480
1481 let response = email_submission_changes(request, store.as_ref())
1482 .await
1483 .unwrap();
1484 assert_eq!(response.old_state, "5");
1485 assert_eq!(response.new_state, "6");
1486 assert!(!response.has_more_changes);
1487 }
1488
1489 #[tokio::test]
1490 async fn test_email_submission_on_success_combined() {
1491 let store = create_test_store();
1492 let mut create_map = HashMap::new();
1493 create_map.insert(
1494 "sub1".to_string(),
1495 EmailSubmissionObject {
1496 identity_id: "id1".to_string(),
1497 email_id: "draft1".to_string(),
1498 envelope: None,
1499 send_at: None,
1500 },
1501 );
1502
1503 let mut on_success_update = HashMap::new();
1504 on_success_update.insert(
1505 "draft1".to_string(),
1506 serde_json::json!({"keywords/$sent": true}),
1507 );
1508
1509 let request = EmailSubmissionSetRequest {
1510 account_id: "acc1".to_string(),
1511 if_in_state: None,
1512 create: Some(create_map),
1513 update: None,
1514 destroy: None,
1515 on_success_update_email: Some(on_success_update),
1516 on_success_destroy_email: Some(vec!["draft2".to_string()]),
1517 };
1518
1519 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1520 assert!(response.not_created.is_some());
1521 }
1522
1523 #[tokio::test]
1524 async fn test_email_submission_serialization() {
1525 let submission = EmailSubmission {
1526 id: "sub1".to_string(),
1527 identity_id: "id1".to_string(),
1528 email_id: "email1".to_string(),
1529 thread_id: Some("thread1".to_string()),
1530 envelope: None,
1531 send_at: None,
1532 undo_status: UndoStatus::Pending,
1533 delivery_status: None,
1534 dsn_blob_ids: None,
1535 mdn_blob_ids: None,
1536 };
1537
1538 let json = serde_json::to_string(&submission).unwrap();
1539 assert!(json.contains("\"id\":\"sub1\""));
1540 assert!(json.contains("\"undoStatus\":\"pending\""));
1541 }
1542
1543 #[tokio::test]
1544 async fn test_email_submission_with_all_fields() {
1545 let mut delivery = HashMap::new();
1546 delivery.insert(
1547 "recipient@example.com".to_string(),
1548 DeliveryStatus {
1549 smtp_reply: "250 OK".to_string(),
1550 delivered: DeliveryState::Yes,
1551 displayed: DisplayedState::Unknown,
1552 },
1553 );
1554
1555 let submission = EmailSubmission {
1556 id: "sub1".to_string(),
1557 identity_id: "id1".to_string(),
1558 email_id: "email1".to_string(),
1559 thread_id: Some("thread1".to_string()),
1560 envelope: Some(Envelope {
1561 mail_from: Address {
1562 email: "sender@example.com".to_string(),
1563 parameters: None,
1564 },
1565 rcpt_to: vec![Address {
1566 email: "recipient@example.com".to_string(),
1567 parameters: None,
1568 }],
1569 }),
1570 send_at: Some(Utc::now()),
1571 undo_status: UndoStatus::Final,
1572 delivery_status: Some(delivery),
1573 dsn_blob_ids: Some(vec!["blob1".to_string()]),
1574 mdn_blob_ids: Some(vec!["blob2".to_string()]),
1575 };
1576
1577 let json = serde_json::to_string(&submission).unwrap();
1578 assert!(json.contains("\"id\":\"sub1\""));
1579 assert!(json.contains("\"deliveryStatus\""));
1580 }
1581
1582 #[tokio::test]
1583 async fn test_email_submission_query_zero_limit() {
1584 let store = create_test_store();
1585 let request = EmailSubmissionQueryRequest {
1586 account_id: "acc1".to_string(),
1587 filter: None,
1588 sort: None,
1589 position: None,
1590 limit: Some(0),
1591 calculate_total: Some(true),
1592 };
1593
1594 let response = email_submission_query(request, store.as_ref())
1595 .await
1596 .unwrap();
1597 assert_eq!(response.limit, Some(0));
1598 }
1599
1600 #[tokio::test]
1601 async fn test_email_submission_delayed_send_far_future() {
1602 let store = create_test_store();
1603 let mut create_map = HashMap::new();
1604 let send_at = Utc::now() + chrono::Duration::days(365);
1605
1606 create_map.insert(
1607 "future1".to_string(),
1608 EmailSubmissionObject {
1609 identity_id: "id1".to_string(),
1610 email_id: "email1".to_string(),
1611 envelope: None,
1612 send_at: Some(send_at),
1613 },
1614 );
1615
1616 let request = EmailSubmissionSetRequest {
1617 account_id: "acc1".to_string(),
1618 if_in_state: None,
1619 create: Some(create_map),
1620 update: None,
1621 destroy: None,
1622 on_success_update_email: None,
1623 on_success_destroy_email: None,
1624 };
1625
1626 let response = email_submission_set(request, store.as_ref()).await.unwrap();
1627 assert!(response.not_created.is_some());
1628 }
1629}