mxr_provider_gmail/
send.rs1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use mxr_compose::email::{build_message, format_message_for_gmail};
3use mxr_core::types::{Address, Draft};
4
5pub fn build_rfc2822(draft: &Draft, from: &Address) -> Result<Vec<u8>, GmailSendError> {
7 let message =
8 build_message(draft, from, true).map_err(|err| GmailSendError::Build(err.to_string()))?;
9 Ok(format_message_for_gmail(&message))
10}
11
12pub fn encode_for_gmail(rfc2822: &[u8]) -> String {
14 URL_SAFE_NO_PAD.encode(rfc2822)
15}
16
17#[derive(Debug, thiserror::Error)]
18pub enum GmailSendError {
19 #[error("Failed to build message: {0}")]
20 Build(String),
21 #[error("Gmail API error: {0}")]
22 Api(String),
23}
24
25#[cfg(test)]
26mod tests {
27 use super::*;
28 use mxr_core::id::{AccountId, DraftId};
29 use mxr_core::types::ReplyHeaders;
30
31 fn test_draft() -> Draft {
32 Draft {
33 id: DraftId::new(),
34 account_id: AccountId::new(),
35 reply_headers: None,
36 to: vec![Address {
37 name: Some("Alice".into()),
38 email: "alice@example.com".into(),
39 }],
40 cc: vec![],
41 bcc: vec![],
42 subject: "Test Subject".into(),
43 body_markdown: "Hello **world**!".into(),
44 attachments: vec![],
45 created_at: chrono::Utc::now(),
46 updated_at: chrono::Utc::now(),
47 }
48 }
49
50 fn from() -> Address {
51 Address {
52 name: Some("Me".into()),
53 email: "me@example.com".into(),
54 }
55 }
56
57 #[test]
58 fn rfc2822_basic_message() {
59 let draft = test_draft();
60 let msg = String::from_utf8(build_rfc2822(&draft, &from()).unwrap()).unwrap();
61 assert!(msg.contains("From: Me <me@example.com>"));
62 assert!(msg.contains("To: Alice <alice@example.com>"));
63 assert!(msg.contains("Subject: Test Subject"));
64 assert!(msg.contains("MIME-Version: 1.0"));
65 assert!(msg.contains("Content-Type: multipart/alternative"));
66 assert!(msg.contains("text/plain; charset=utf-8"));
67 assert!(msg.contains("text/html; charset=utf-8"));
68 assert!(msg.contains("\r\n"));
69 }
70
71 #[test]
72 fn rfc2822_reply_has_full_references_chain() {
73 let mut draft = test_draft();
74 draft.reply_headers = Some(ReplyHeaders {
75 in_reply_to: "<parent@example.com>".into(),
76 references: vec!["<root@example.com>".into()],
77 });
78 let msg = String::from_utf8(build_rfc2822(&draft, &from()).unwrap()).unwrap();
79 assert!(msg.contains("In-Reply-To: <parent@example.com>\r\n"));
80 assert!(msg.contains("References: <root@example.com> <parent@example.com>\r\n"));
81 }
82
83 #[test]
84 fn encode_for_gmail_base64url() {
85 let rfc2822 = b"From: test@test.com\r\nTo: alice@test.com\r\n\r\nHello";
86 let encoded = encode_for_gmail(rfc2822);
87 assert!(!encoded.contains('+'));
88 assert!(!encoded.contains('/'));
89 }
90
91 #[test]
92 fn rfc2822_keeps_bcc_for_gmail_submission() {
93 let mut draft = test_draft();
94 draft.bcc = vec![Address {
95 name: None,
96 email: "hidden@example.com".into(),
97 }];
98 let msg = String::from_utf8(build_rfc2822(&draft, &from()).unwrap()).unwrap();
99 assert!(msg.contains("Bcc: hidden@example.com\r\n"));
100 }
101}