From a6854396be67b028e78d2c43bf7eb60ef74fdd81 Mon Sep 17 00:00:00 2001 From: rustmailer Date: Wed, 4 Feb 2026 00:37:07 +0800 Subject: [PATCH] feat(graph): support sending, replying, and forwarding emails --- protos/rustmailer.proto | 10 +- .../cache/vendor/outlook/sync/client.rs | 15 ++ .../cache/vendor/outlook/sync/envelope.rs | 2 +- src/modules/common/http/mod.rs | 52 +++++ src/modules/message/append.rs | 6 +- src/modules/message/content.rs | 2 +- src/modules/smtp/composer/mod.rs | 10 +- src/modules/smtp/request/forward.rs | 57 +++-- src/modules/smtp/request/mod.rs | 202 +++++++++++++----- src/modules/smtp/request/reply.rs | 55 ++--- src/modules/smtp/request/task.rs | 29 ++- 11 files changed, 328 insertions(+), 112 deletions(-) diff --git a/protos/rustmailer.proto b/protos/rustmailer.proto index 58d1966..e26cd41 100644 --- a/protos/rustmailer.proto +++ b/protos/rustmailer.proto @@ -1852,7 +1852,7 @@ message ReplyEmailRequest { // A map of custom headers for the reply. map headers = 6; // If true, replies to all recipients (To, CC) of the original email. - bool reply_all = 7; + optional bool reply_all = 7; // A list of attachments to include in the reply. repeated MailAttachment attachments = 8; // A list of additional CC recipients for the reply. @@ -1862,9 +1862,9 @@ message ReplyEmailRequest { // Optional: The timezone for date headers in the reply. optional string timezone = 11; // If true, includes the original message content in the reply. - bool include_original = 12; + optional bool include_original = 12; // If true, includes all attachments from the original message in the reply. - bool include_all_attachments = 13; + optional bool include_all_attachments = 13; // Controls the sending process, including retry policies and DSN. SendControl send_control = 14; } @@ -1897,9 +1897,9 @@ message ForwardEmailRequest { // A list of attachments to include in the forwarded email. repeated MailAttachment attachments = 11; // If true, includes the original message content in the forwarded email. - bool include_original = 12; + optional bool include_original = 12; // If true, includes all attachments from the original message in the forwarded email. - bool include_all_attachments = 13; + optional bool include_all_attachments = 13; // Controls the sending process, including retry policies and DSN. SendControl send_control = 14; } diff --git a/src/modules/cache/vendor/outlook/sync/client.rs b/src/modules/cache/vendor/outlook/sync/client.rs index bdaac04..21eda00 100644 --- a/src/modules/cache/vendor/outlook/sync/client.rs +++ b/src/modules/cache/vendor/outlook/sync/client.rs @@ -390,6 +390,21 @@ impl OutlookClient { }) } + + pub async fn send_email( + account_id: u64, + use_proxy: Option, + message_base64: String, + ) -> RustMailerResult<()> { + let url = "https://graph.microsoft.com/v1.0/me/sendMail"; + let client = HttpClient::new(use_proxy).await?; + let access_token = Self::get_access_token(account_id).await?; + client + .post_text::<()>(url, &access_token, message_base64, false) + .await?; + Ok(()) + } + pub async fn batch_get_categories( account_id: u64, use_proxy: Option, diff --git a/src/modules/cache/vendor/outlook/sync/envelope.rs b/src/modules/cache/vendor/outlook/sync/envelope.rs index fb4d333..d378270 100644 --- a/src/modules/cache/vendor/outlook/sync/envelope.rs +++ b/src/modules/cache/vendor/outlook/sync/envelope.rs @@ -351,7 +351,7 @@ impl OutlookEnvelope { impl TryFrom for OutlookEnvelope { type Error = RustMailerError; - fn try_from(msg: Message) -> Result { + fn try_from(msg: Message) -> Result { fn parse_datetime(dt: &Option) -> RustMailerResult> { dt.as_ref() .map(|s| { diff --git a/src/modules/common/http/mod.rs b/src/modules/common/http/mod.rs index e4e54b3..af3fe22 100644 --- a/src/modules/common/http/mod.rs +++ b/src/modules/common/http/mod.rs @@ -350,6 +350,58 @@ impl HttpClient { } } + pub async fn post_text( + &self, + url: &str, + access_token: &str, + body: String, + expect_json_response: bool, + ) -> RustMailerResult { + let mut builder = self + .client + .post(url) + .header(AUTHORIZATION, format!("Bearer {}", access_token)) + .header(CONTENT_TYPE, "text/plain"); + builder = builder.body(body); + + let res = builder.send().await.map_err(|e| { + raise_error!( + format!("Request failed: {:#?}", e), + ErrorCode::InternalError + ) + })?; + + if res.status().is_success() { + if expect_json_response { + let json: serde_json::Value = res.json().await.map_err(|e| { + raise_error!( + format!("Failed to parse response: {:#?}", e), + ErrorCode::InternalError + ) + })?; + Ok(json) + } else { + Ok(serde_json::Value::Null) + } + } else { + let status = res.status(); + let text = res.text().await.map_err(|e| { + raise_error!( + format!("Failed to read error response: {:#?}", e), + ErrorCode::InternalError + ) + })?; + // Return the error with status and response text for more context + Err(raise_error!( + format!( + "API call to {} failed with status {}: {}", + url, status, text + ), + ErrorCode::ApiCallFailed + )) + } + } + /// Wrapper around the Gmail API `POST` request. pub async fn delete(&self, url: &str, access_token: &str) -> RustMailerResult<()> { let res = self diff --git a/src/modules/message/append.rs b/src/modules/message/append.rs index 60ba784..e7c79c9 100644 --- a/src/modules/message/append.rs +++ b/src/modules/message/append.rs @@ -18,6 +18,7 @@ use crate::{ account::{entity::MailerType, migration::AccountModel}, cache::{ imap::mailbox::AttributeEnum, + model::Envelope, vendor::{ gmail::sync::{client::GmailClient, envelope::GmailEnvelope}, outlook::sync::client::OutlookClient, @@ -145,12 +146,13 @@ impl AppendReplyToDraftRequest { ) })?; - let envelope = EmailHandler::get_envelope( + let envelope: Envelope = EmailHandler::get_envelope( account, self.mailbox_name.as_deref().unwrap(), self.id.parse::().ok().unwrap(), ) - .await?; + .await? + .into(); let from = Address::new_address( account.name.as_ref().map(|n| Cow::Owned(n.to_string())), diff --git a/src/modules/message/content.rs b/src/modules/message/content.rs index 69578dc..e1c9774 100644 --- a/src/modules/message/content.rs +++ b/src/modules/message/content.rs @@ -43,7 +43,7 @@ pub struct MessageContentRequest { /// /// - For IMAP accounts, this is the UID converted to a string. It must be a valid numeric string /// that can be parsed back to a `u32`. - /// - For Gmail API accounts, this is the message ID (`mid`) returned by the API. + /// - For Gmail/Graph API accounts, this is the message ID (`mid`) returned by the API. pub id: String, /// Optional maximum length to retrieve for text parts (useful for large messages) /// - Supported by both IMAP/SMTP and Gmail API diff --git a/src/modules/smtp/composer/mod.rs b/src/modules/smtp/composer/mod.rs index 056cc4d..926a061 100644 --- a/src/modules/smtp/composer/mod.rs +++ b/src/modules/smtp/composer/mod.rs @@ -2,7 +2,7 @@ // Licensed under RustMailer License Agreement v1.0 // Unauthorized copying, modification, or distribution is prohibited. -use crate::modules::cache::imap::migration::EmailEnvelopeV3; +use crate::modules::cache::model::Envelope; use scraper::{Html, Selector}; use time::{macros::format_description, OffsetDateTime}; use time_tz::timezones; @@ -50,7 +50,7 @@ impl BodyComposer { pub fn generate_html( original_html: &str, reply_content: &str, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, timezone_name: &str, reply: bool, ) -> String { @@ -161,7 +161,7 @@ impl BodyComposer { pub fn generate_text( original_text: &str, reply_content: &str, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, timezone_name: &str, reply: bool, ) -> String { @@ -337,7 +337,7 @@ mod tests { let result = BodyComposer::generate_html( original_html, reply_content, - &envelope, + &envelope.into(), "Asia/Shanghai", true, ); @@ -386,7 +386,7 @@ mod tests { let result = BodyComposer::generate_text( original_text, reply_content, - &envelope, + &envelope.into(), "Asia/Shanghai", true, ); diff --git a/src/modules/smtp/request/forward.rs b/src/modules/smtp/request/forward.rs index 203642d..87f920a 100644 --- a/src/modules/smtp/request/forward.rs +++ b/src/modules/smtp/request/forward.rs @@ -3,7 +3,7 @@ // Unauthorized copying, modification, or distribution is prohibited. use crate::modules::account::entity::MailerType; -use crate::modules::cache::imap::migration::EmailEnvelopeV3; +use crate::modules::cache::model::Envelope; use crate::modules::error::code::ErrorCode; use crate::modules::smtp::request::builder::EmailBuilder; use crate::modules::smtp::request::headers::HeaderValue; @@ -90,12 +90,12 @@ pub struct ForwardEmailRequest { /// Whether to include the original message in the forwarded email body. /// /// If true, the full original message content will be included in the body. - pub include_original: bool, + pub include_original: Option, /// Whether to include all original attachments in the forwarded email. /// /// If true, all attachments from the original message will be forwarded as well. - pub include_all_attachments: bool, + pub include_all_attachments: Option, /// Configuration options for controlling the email sending process. /// @@ -157,7 +157,7 @@ impl EmailBuilder for ForwardEmailRequest { self.validate().await?; let account = &AccountModel::get(account_id).await?; - let (envelope, answer_email) = match account.mailer_type { + let (envelope, answer_email): (Envelope, Option) = match account.mailer_type { MailerType::ImapSmtp => { let uid = self.id.parse::().ok().ok_or_else(|| { raise_error!( @@ -167,7 +167,7 @@ impl EmailBuilder for ForwardEmailRequest { })?; let envelope = EmailHandler::get_envelope(account, &self.mailbox_name, uid).await?; ( - envelope, + envelope.into(), Some(AnswerEmail { reply: true, mailbox: self.mailbox_name.clone(), @@ -178,9 +178,14 @@ impl EmailBuilder for ForwardEmailRequest { MailerType::GmailApi => { let envelope = EmailHandler::get_gmail_envelope(account, &self.mailbox_name, &self.id).await?; - (envelope, None) + (envelope.into(), None) + } + MailerType::GraphApi => { + let envelope = + EmailHandler::get_outlook_envelope(account, &self.mailbox_name, &self.id) + .await?; + (envelope.into(), None) } - MailerType::GraphApi => todo!(), }; let from = Address::new_address( account.name.as_ref().map(|n| Cow::Owned(n.to_string())), @@ -253,7 +258,7 @@ impl ForwardEmailRequest { fn apply_references( &self, builder: MessageBuilder<'static>, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, ) -> RustMailerResult> { let mut references = envelope.references.clone().unwrap_or_default(); if let Some(message_id) = &envelope.message_id { @@ -267,12 +272,12 @@ impl ForwardEmailRequest { async fn apply_content( &self, mut builder: MessageBuilder<'static>, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, account: &AccountModel, ) -> RustMailerResult> { let timezone = self.timezone.as_deref().unwrap_or("UTC"); - if self.include_original { + if self.include_original.unwrap_or_default() { if let Some(content) = EmailHandler::retrieve_message_content(account, envelope).await? { if let Some(original_html) = content.html() { @@ -305,20 +310,32 @@ impl ForwardEmailRequest { } else if let Some(text) = &self.text { builder = builder.text_body(text.clone()); } + + //include_all_attachments + if self.include_all_attachments.unwrap_or_default() { + builder = EmailHandler::add_attachment( + builder, + envelope.attachments.as_deref(), + content.attachments.as_deref(), + envelope, + account, + ) + .await?; + } } else { builder = self.apply_fallback_content(builder)?; } - if let Some(attachments) = envelope.attachments.as_ref() { - if self.include_all_attachments { - for attachment in attachments.iter().filter(|att| !att.inline) { - builder = EmailHandler::add_attachment( - builder, attachment, envelope, false, account, - ) - .await?; - } - } - } + // if let Some(attachments) = envelope.attachments.as_ref() { + // if self.include_all_attachments { + // for attachment in attachments.iter().filter(|att| !att.inline) { + // builder = EmailHandler::add_attachment( + // builder, attachment, envelope, false, account, + // ) + // .await?; + // } + // } + // } } else { builder = self.apply_fallback_content(builder)?; } diff --git a/src/modules/smtp/request/mod.rs b/src/modules/smtp/request/mod.rs index 4e1b77e..794e9c7 100644 --- a/src/modules/smtp/request/mod.rs +++ b/src/modules/smtp/request/mod.rs @@ -5,23 +5,30 @@ use crate::base64_decode_url_safe; use crate::encode_mailbox_name; use crate::generate_token; +use crate::modules::account::entity::MailerType; use crate::modules::cache::disk::DISK_CACHE; use crate::modules::cache::imap::mailbox::EmailFlag; use crate::modules::cache::imap::mailbox::EnvelopeFlag; use crate::modules::cache::imap::mailbox::MailBox; use crate::modules::cache::imap::migration::EmailEnvelopeV3; +use crate::modules::cache::model::Envelope; use crate::modules::cache::vendor::gmail::sync::client::GmailClient; use crate::modules::cache::vendor::gmail::sync::envelope::GmailEnvelope; use crate::modules::cache::vendor::gmail::sync::labels::GmailLabels; +use crate::modules::cache::vendor::outlook::sync::client::OutlookClient; +use crate::modules::cache::vendor::outlook::sync::envelope::OutlookEnvelope; +use crate::modules::cache::vendor::outlook::sync::folders::OutlookFolder; use crate::modules::common::Addr; use crate::modules::context::executors::RUST_MAIL_CONTEXT; use crate::modules::envelope::extractor::extract_envelope; use crate::modules::error::code::ErrorCode; use crate::modules::message::content::retrieve_email_content; +use crate::modules::message::content::AttachmentInfo; use crate::modules::message::content::FullMessageContent; use crate::modules::message::content::MessageContentRequest; use crate::modules::smtp::template::preview::EmailPreview; use crate::modules::tasks::queue::RustMailerTaskQueue; +use crate::modules::utils::envelope_hash_from_id; use crate::utc_now; use crate::validate_email; use crate::{ @@ -46,6 +53,7 @@ use std::borrow::Cow; use task::AnswerEmail; use task::SmtpTask; use tokio::io::AsyncReadExt; +use tracing::warn; pub mod builder; pub mod forward; @@ -118,7 +126,13 @@ pub struct AttachmentPayload { /// This optional field refers to an attachment that exists in a different email /// within the same mailbox account. It is used when the current message does not /// contain the attachment content directly in `base64_content`, but instead links - /// to an existing attachment (e.g., by message ID and section index). + /// to an existing attachment (e.g. by message UID and MIME section index). + /// + /// ## Notes + /// - This feature is currently **IMAP-only**. + /// - Gmail API and Microsoft Graph API do **not** support cross-message attachment + /// references at this time. + /// - For non-IMAP accounts, attachment content must be provided explicitly. pub attachment_ref: Option, } @@ -163,8 +177,23 @@ impl MailAttachment { } if let Some(attachment_ref) = &self.payload.attachment_ref { - return Self::retrieve_and_decode_attachment(attachment_ref, &self.mime_type, account) - .await; + if matches!(account.mailer_type, MailerType::ImapSmtp) { + let request = AttachmentRequest { + id: attachment_ref.uid.to_string(), + mailbox: Some(attachment_ref.mailbox_name.clone()), + attachment: Some(attachment_ref.attachment_data.clone()), + attachment_info: None, + filename: None, + attachment_id: None, + }; + return Self::retrieve_and_decode_attachment(request, &self.mime_type, account) + .await; + } else { + warn!( + "AttachmentRef is only supported for IMAP accounts. \ + Gmail and Microsoft Graph API do not support this feature yet." + ); + } } Err(raise_error!( @@ -205,22 +234,11 @@ impl MailAttachment { } async fn retrieve_and_decode_attachment( - attachment_ref: &AttachmentRef, + request: AttachmentRequest, mime_type: &str, account: &AccountModel, ) -> RustMailerResult> { - let (mut reader, _) = retrieve_email_attachment( - account.id, - AttachmentRequest { - id: attachment_ref.uid.to_string(), - mailbox: Some(attachment_ref.mailbox_name.clone()), - attachment: Some(attachment_ref.attachment_data.clone()), - attachment_info: None, - filename: None, - attachment_id: None, - }, - ) - .await?; + let (mut reader, _) = retrieve_email_attachment(account.id, request).await?; let mut buffer = Vec::new(); reader.read_to_end(&mut buffer).await.map_err(|e| { @@ -607,7 +625,7 @@ impl EmailHandler { pub async fn retrieve_message_content( account: &AccountModel, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, ) -> RustMailerResult> { let body_meta = match &envelope.body_meta { Some(meta) => meta, @@ -621,7 +639,7 @@ impl EmailHandler { }); let request = MessageContentRequest { mailbox: Some(envelope.mailbox_name.clone()), - id: envelope.uid.to_string(), + id: envelope.id.clone(), max_length: None, sections: Some(body_meta.clone()), inline: inline_attachments, @@ -631,6 +649,24 @@ impl EmailHandler { .map(Some) } + pub async fn get_outlook_envelope( + account: &AccountModel, + mailbox_name: &str, + mid: &str, + ) -> RustMailerResult { + if !account.minimal_sync() { + if let Ok(folder) = OutlookFolder::get_by_name(account.id, mailbox_name).await { + let envelope = + OutlookEnvelope::get(envelope_hash_from_id(account.id, folder.id, mid)).await?; + if let Some(envelope) = envelope { + return Ok(envelope); + } + } + } + let message = OutlookClient::get_message(account.id, account.use_proxy, mid).await?; + Ok(message.try_into()?) + } + pub async fn get_gmail_envelope( account: &AccountModel, label_name: &str, @@ -726,41 +762,107 @@ impl EmailHandler { } async fn add_attachment( - builder: MessageBuilder<'static>, - attachment: &ImapAttachment, - envelope: &EmailEnvelopeV3, - inline: bool, + mut builder: MessageBuilder<'static>, + imap_attachments: Option<&[ImapAttachment]>, + attachments: Option<&[AttachmentInfo]>, + envelope: &Envelope, account: &AccountModel, ) -> RustMailerResult> { - let attachment_ref = AttachmentRef { - mailbox_name: envelope.mailbox_name.clone(), - uid: envelope.uid, - attachment_data: attachment.clone(), - }; - let mime_type = from_ext(&attachment.file_type) - .first_or_octet_stream() - .to_string(); - let content = - MailAttachment::retrieve_and_decode_attachment(&attachment_ref, &mime_type, account) - .await?; + match account.mailer_type { + MailerType::ImapSmtp => { + if let Some(attachments) = imap_attachments { + for attachment in attachments { + let uid = envelope.id.parse::().ok().ok_or_else(|| { + raise_error!( + "Invalid IMAP UID: `id` must be a numeric string".into(), + ErrorCode::InvalidParameter + ) + })?; + let attachment_ref = AttachmentRef { + mailbox_name: envelope.mailbox_name.clone(), + uid, + attachment_data: attachment.clone(), + }; + let mime_type = from_ext(&attachment.file_type) + .first_or_octet_stream() + .to_string(); + let request = AttachmentRequest { + id: attachment_ref.uid.to_string(), + mailbox: Some(attachment_ref.mailbox_name.clone()), + attachment: Some(attachment_ref.attachment_data.clone()), + attachment_info: None, + filename: None, + attachment_id: None, + }; + let content = MailAttachment::retrieve_and_decode_attachment( + request, &mime_type, account, + ) + .await?; - Ok(if inline { - builder.inline( - mime_type, - attachment.content_id.clone().ok_or_else(|| { - raise_error!("Missing content_id".into(), ErrorCode::ImapUnexpectedResult) - })?, - content, - ) - } else { - builder.attachment( - mime_type, - attachment.filename.clone().ok_or_else(|| { - raise_error!("Missing filename".into(), ErrorCode::ImapUnexpectedResult) - })?, - content, - ) - }) + if attachment.inline { + builder = builder.inline( + mime_type, + attachment.content_id.clone().ok_or_else(|| { + raise_error!( + "Missing content_id".into(), + ErrorCode::ImapUnexpectedResult + ) + })?, + content, + ); + } else { + builder = builder.attachment( + mime_type, + attachment.filename.clone().ok_or_else(|| { + raise_error!( + "Missing filename".into(), + ErrorCode::ImapUnexpectedResult + ) + })?, + content, + ); + } + } + } + } + _ => { + if let Some(attachments) = attachments { + for attachment in attachments { + let mime_type = attachment.file_type.clone(); + let request = AttachmentRequest { + id: envelope.id.clone(), + mailbox: Some(envelope.mailbox_name.clone()), + attachment: None, + attachment_info: Some(attachment.clone()), + filename: None, + attachment_id: Some(attachment.id.clone()), + }; + let content = MailAttachment::retrieve_and_decode_attachment( + request, &mime_type, account, + ) + .await?; + + if attachment.inline { + builder = builder.inline( + mime_type, + attachment.content_id.clone().ok_or_else(|| { + raise_error!( + "Missing content_id".into(), + ErrorCode::ImapUnexpectedResult + ) + })?, + content, + ); + } else { + builder = + builder.attachment(mime_type, attachment.filename.clone(), content); + } + } + } + } + } + + Ok(builder) } pub fn insert_preview(preview: &Option, html: String) -> String { diff --git a/src/modules/smtp/request/reply.rs b/src/modules/smtp/request/reply.rs index c49c4ae..b2cb7f9 100644 --- a/src/modules/smtp/request/reply.rs +++ b/src/modules/smtp/request/reply.rs @@ -5,7 +5,7 @@ use crate::{ modules::{ account::{entity::MailerType, migration::AccountModel}, - cache::{imap::migration::EmailEnvelopeV3, vendor::gmail::sync::envelope::GmailEnvelope}, + cache::{model::Envelope, vendor::gmail::sync::envelope::GmailEnvelope}, error::{code::ErrorCode, RustMailerResult}, smtp::{ composer::BodyComposer, @@ -36,7 +36,7 @@ pub struct ReplyEmailRequest { /// /// - For IMAP accounts, this is the UID converted to a string. It must be a valid numeric string /// that can be parsed back to a `u32`. - /// - For Gmail API accounts, this is the message ID (`mid`) returned by the API. + /// - For Gmail/Graph API accounts, this is the message ID (`mid`) returned by the API. pub id: String, /// The plain text body of the reply email. /// @@ -57,7 +57,7 @@ pub struct ReplyEmailRequest { /// Whether to reply to all original recipients (Reply-All). /// /// If true, the reply will be sent to all original recipients, including Cc. - pub reply_all: bool, + pub reply_all: Option, /// A list of attachments to include in the reply. /// /// This optional field allows adding new file attachments to the reply email. @@ -73,11 +73,11 @@ pub struct ReplyEmailRequest { /// Whether to include the original message in the reply body. /// /// If true, the original message content will be quoted and included in the reply. - pub include_original: bool, + pub include_original: Option, /// Whether to include all original attachments in the reply. /// /// If true, all attachments from the original message will be included in the reply. - pub include_all_attachments: bool, + pub include_all_attachments: Option, /// The sender's timezone (e.g., "Asia/Shanghai"). /// /// This optional field may be used for formatting date/time in the reply body. @@ -134,7 +134,7 @@ impl EmailBuilder for ReplyEmailRequest { let account = &AccountModel::get(account_id).await?; self.validate().await?; - let (envelope, answer_email) = match account.mailer_type { + let (envelope, answer_email): (Envelope, Option) = match account.mailer_type { MailerType::ImapSmtp => { let uid = self.id.parse::().ok().ok_or_else(|| { raise_error!( @@ -144,7 +144,7 @@ impl EmailBuilder for ReplyEmailRequest { })?; let envelope = EmailHandler::get_envelope(account, &self.mailbox_name, uid).await?; ( - envelope, + envelope.into(), Some(AnswerEmail { reply: true, mailbox: self.mailbox_name.clone(), @@ -155,9 +155,14 @@ impl EmailBuilder for ReplyEmailRequest { MailerType::GmailApi => { let envelope = EmailHandler::get_gmail_envelope(account, &self.mailbox_name, &self.id).await?; - (envelope, None) + (envelope.into(), None) + } + MailerType::GraphApi => { + let envelope = + EmailHandler::get_outlook_envelope(account, &self.mailbox_name, &self.id) + .await?; + (envelope.into(), None) } - MailerType::GraphApi => todo!(), }; let from = Address::new_address( @@ -218,10 +223,10 @@ impl ReplyEmailRequest { fn apply_recipient_headers( &self, mut builder: MessageBuilder<'static>, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, message_id: &str, ) -> RustMailerResult> { - if self.reply_all { + if self.reply_all.unwrap_or_default() { if let Some(cc) = &envelope.cc { builder = builder.cc(Address::from(cc.clone())); } @@ -255,12 +260,12 @@ impl ReplyEmailRequest { async fn apply_content( &self, mut builder: MessageBuilder<'static>, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, account: &AccountModel, ) -> RustMailerResult> { let timezone = self.timezone.as_deref().unwrap_or("UTC"); - if self.include_original { + if self.include_original.unwrap_or_default() { if let Some(content) = EmailHandler::retrieve_message_content(account, envelope).await? { if let Some(original_html) = content.html() { @@ -293,20 +298,20 @@ impl ReplyEmailRequest { } else if let Some(text) = &self.text { builder = builder.text_body(text.clone()); } + + if self.include_all_attachments.unwrap_or_default() { + builder = EmailHandler::add_attachment( + builder, + envelope.attachments.as_deref(), + content.attachments.as_deref(), + envelope, + account, + ) + .await?; + } } else { builder = self.apply_fallback_content(builder)?; } - - if let Some(attachments) = envelope.attachments.as_ref() { - if self.include_all_attachments { - for attachment in attachments.iter().filter(|att| !att.inline) { - builder = EmailHandler::add_attachment( - builder, attachment, envelope, false, account, - ) - .await?; - } - } - } } else { builder = self.apply_fallback_content(builder)?; } @@ -369,7 +374,7 @@ impl ReplyEmailRequest { pub fn apply_references( builder: MessageBuilder<'static>, - envelope: &EmailEnvelopeV3, + envelope: &Envelope, ) -> RustMailerResult> { let builder = if let Some(message_id) = &envelope.message_id { builder.in_reply_to(message_id.clone()) diff --git a/src/modules/smtp/request/task.rs b/src/modules/smtp/request/task.rs index 54388d4..db1db48 100644 --- a/src/modules/smtp/request/task.rs +++ b/src/modules/smtp/request/task.rs @@ -8,6 +8,7 @@ use std::time::Instant; use crate::modules::account::entity::MailerType; use crate::modules::cache::disk::DISK_CACHE; use crate::modules::cache::vendor::gmail::sync::client::GmailClient; +use crate::modules::cache::vendor::outlook::sync::client::OutlookClient; use crate::modules::error::code::ErrorCode; use crate::modules::error::RustMailerResult; use crate::modules::hook::channel::{Event, EVENT_CHANNEL}; @@ -20,7 +21,7 @@ use crate::modules::metrics::{ RUSTMAILER_EMAIL_SENT_TOTAL, SUCCESS, }; use crate::modules::smtp::executor::SmtpExecutor; -use crate::{base64_encode_url_safe, raise_error}; +use crate::{base64_encode, base64_encode_url_safe, raise_error}; use crate::modules::scheduler::{ retry::{RetryPolicy, RetryStrategy}, @@ -315,14 +316,28 @@ impl Task for SmtpTask { .await; let raw_encoded = base64_encode_url_safe!(&message.body); match gmail_send_email(self.account_id, account.use_proxy, raw_encoded).await { - Ok(()) => self.handle_email_send_success(start, body.len()).await, + Ok(_) => self.handle_email_send_success(start, body.len()).await, + Err(e) => { + Self::record_send_failure_metrics(start); + Err(e) + } + } + } + MailerType::GraphApi => { + let envelope_opt = self.control.as_ref().and_then(|c| c.envelope.as_ref()); + let message = + Self::build_message(envelope_opt, &body, self.from.clone(), &self.to, None) + .await; + let raw_encoded = base64_encode!(&message.body); + match outlook_send_email(self.account_id, account.use_proxy, raw_encoded).await + { + Ok(_) => self.handle_email_send_success(start, body.len()).await, Err(e) => { Self::record_send_failure_metrics(start); Err(e) } } } - MailerType::GraphApi => todo!(), } }) } @@ -340,3 +355,11 @@ async fn gmail_send_email( GmailClient::send_email(account_id, use_proxy, raw_encoded).await?; Ok(()) } + +async fn outlook_send_email( + account_id: u64, + use_proxy: Option, + raw_encoded: String, +) -> RustMailerResult<()> { + OutlookClient::send_email(account_id, use_proxy, raw_encoded).await +}