mirror of
https://github.com/rustmailer/rustmailer.git
synced 2026-09-09 08:02:03 +00:00
feat(graph): support sending, replying, and forwarding emails
This commit is contained in:
@@ -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
@@ -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
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user