feat(graph): support sending, replying, and forwarding emails

This commit is contained in:
rustmailer
2026-02-04 00:37:07 +08:00
parent df54ff66ae
commit a6854396be
11 changed files with 328 additions and 112 deletions
+5 -5
View File
@@ -1852,7 +1852,7 @@ message ReplyEmailRequest {
// A map of custom headers for the reply.
map<string, HeaderValue> 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;
}
+15
View File
@@ -390,6 +390,21 @@ impl OutlookClient {
})
}
pub async fn send_email(
account_id: u64,
use_proxy: Option<u64>,
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<u64>,
+1 -1
View File
@@ -351,7 +351,7 @@ impl OutlookEnvelope {
impl TryFrom<Message> for OutlookEnvelope {
type Error = RustMailerError;
fn try_from(msg: Message) -> Result<Self, Self::Error> {
fn try_from(msg: Message) -> Result<Self, Self::Error> {
fn parse_datetime(dt: &Option<String>) -> RustMailerResult<Option<i64>> {
dt.as_ref()
.map(|s| {
+52
View File
@@ -350,6 +350,58 @@ impl HttpClient {
}
}
pub async fn post_text<T: Serialize>(
&self,
url: &str,
access_token: &str,
body: String,
expect_json_response: bool,
) -> RustMailerResult<serde_json::Value> {
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
+4 -2
View File
@@ -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::<u32>().ok().unwrap(),
)
.await?;
.await?
.into();
let from = Address::new_address(
account.name.as_ref().map(|n| Cow::Owned(n.to_string())),
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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,
);
+37 -20
View File
@@ -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<bool>,
/// 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<bool>,
/// 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<AnswerEmail>) = match account.mailer_type {
MailerType::ImapSmtp => {
let uid = self.id.parse::<u32>().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<MessageBuilder<'static>> {
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<MessageBuilder<'static>> {
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)?;
}
+152 -50
View File
@@ -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<AttachmentRef>,
}
@@ -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<BodyPart<'static>> {
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<Option<FullMessageContent>> {
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<OutlookEnvelope> {
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<MessageBuilder<'static>> {
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::<u32>().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<String>, html: String) -> String {
+30 -25
View File
@@ -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<bool>,
/// 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<bool>,
/// 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<bool>,
/// 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<AnswerEmail>) = match account.mailer_type {
MailerType::ImapSmtp => {
let uid = self.id.parse::<u32>().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<MessageBuilder<'static>> {
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<MessageBuilder<'static>> {
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<MessageBuilder<'static>> {
let builder = if let Some(message_id) = &envelope.message_id {
builder.in_reply_to(message_id.clone())
+26 -3
View File
@@ -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<u64>,
raw_encoded: String,
) -> RustMailerResult<()> {
OutlookClient::send_email(account_id, use_proxy, raw_encoded).await
}