Skip to main content

mxr_provider_gmail/
send.rs

1use 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
5/// Build an RFC 5322 message from a Draft and return the raw bytes.
6pub 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
12/// Encode an RFC 5322 message as base64url for Gmail API.
13pub 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}