Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,881 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use common::{
|
||||
Server,
|
||||
config::smtp::queue::QueueName,
|
||||
network::{ServerInstance, stream::NullIo},
|
||||
storage::index::ObjectIndexBuilder,
|
||||
};
|
||||
use email::{
|
||||
identity::Identity,
|
||||
message::metadata::{ArchivedMetadataHeaderName, ArchivedMetadataHeaderValue, MessageMetadata},
|
||||
submission::{Address, Delivered, DeliveryStatus, EmailSubmission, UndoStatus},
|
||||
};
|
||||
use jmap_proto::{
|
||||
error::set::{SetError, SetErrorType},
|
||||
method::set::{SetRequest, SetResponse},
|
||||
object::email_submission::{self, EmailSubmissionProperty, EmailSubmissionValue},
|
||||
references::resolve::ResolveCreatedReference,
|
||||
request::{
|
||||
Call, MaybeInvalid, RequestMethod, SetRequestMethod,
|
||||
method::{MethodFunction, MethodName, MethodObject},
|
||||
reference::{MaybeIdReference, MaybeResultReference},
|
||||
},
|
||||
types::{date::UTCDate, state::State},
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use smtp::{
|
||||
core::{Session, SessionData},
|
||||
queue::spool::SmtpSpool,
|
||||
};
|
||||
use smtp_proto::{MailFrom, RcptTo, request::parser::Rfc5321Parser};
|
||||
use std::{borrow::Cow, future::Future};
|
||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||
use store::{
|
||||
ValueKey,
|
||||
write::{AlignedBytes, Archive, BatchBuilder, now},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{collection::Collection, field::EmailField, id::Id};
|
||||
use utils::{map::vec_map::VecMap, sanitize_email};
|
||||
|
||||
pub trait EmailSubmissionSet: Sync + Send {
|
||||
fn email_submission_set<'x>(
|
||||
&self,
|
||||
request: SetRequest<'x, email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
) -> impl Future<Output = trc::Result<SetResponse<email_submission::EmailSubmission>>> + Send;
|
||||
|
||||
fn send_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
response: &SetResponse<email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
object: Value<'_, EmailSubmissionProperty, EmailSubmissionValue>,
|
||||
) -> impl Future<
|
||||
Output = trc::Result<Result<EmailSubmission, SetError<EmailSubmissionProperty>>>,
|
||||
> + Send;
|
||||
}
|
||||
|
||||
impl EmailSubmissionSet for Server {
|
||||
async fn email_submission_set<'x>(
|
||||
&self,
|
||||
mut request: SetRequest<'x, email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
next_call: &mut Option<Call<RequestMethod<'x>>>,
|
||||
) -> trc::Result<SetResponse<email_submission::EmailSubmission>> {
|
||||
let account_id = request.account_id.document_id();
|
||||
let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?;
|
||||
let will_destroy = response.collect_will_destroy(request.unwrap_destroy());
|
||||
|
||||
// Process creates
|
||||
let mut success_email_ids = HashMap::new();
|
||||
let mut batch = BatchBuilder::new();
|
||||
for (id, object) in request.unwrap_create() {
|
||||
match self
|
||||
.send_message(account_id, &response, instance, object)
|
||||
.await?
|
||||
{
|
||||
Ok(submission) => {
|
||||
// Add id mapping
|
||||
success_email_ids.insert(
|
||||
id.clone(),
|
||||
Id::from_parts(submission.thread_id, submission.email_id),
|
||||
);
|
||||
|
||||
let send_at = submission.send_at;
|
||||
let undo_status = match submission.undo_status {
|
||||
UndoStatus::Pending => email_submission::UndoStatus::Pending,
|
||||
UndoStatus::Final => email_submission::UndoStatus::Final,
|
||||
UndoStatus::Canceled => email_submission::UndoStatus::Canceled,
|
||||
};
|
||||
|
||||
// Insert record
|
||||
let document_id = self
|
||||
.store()
|
||||
.assign_document_ids(account_id, Collection::EmailSubmission, 1)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::EmailSubmission)
|
||||
.with_document(document_id)
|
||||
.custom(ObjectIndexBuilder::<(), _>::new().with_changes(submission))
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
|
||||
response.created.insert(
|
||||
id,
|
||||
Value::Object(
|
||||
Map::with_capacity(3)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::Id,
|
||||
Value::Element(Id::from(document_id).into()),
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::SendAt,
|
||||
Value::Element(EmailSubmissionValue::Date(
|
||||
UTCDate::from_timestamp(send_at as i64),
|
||||
)),
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::UndoStatus,
|
||||
Value::Element(EmailSubmissionValue::UndoStatus(undo_status)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
response.not_created.append(id, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process updates
|
||||
'update: for (id, object) in request.unwrap_update() {
|
||||
let id = match id {
|
||||
MaybeInvalid::Value(id) => id,
|
||||
invalid => {
|
||||
response.not_updated.append(invalid, SetError::not_found());
|
||||
continue 'update;
|
||||
}
|
||||
};
|
||||
// Make sure id won't be destroyed
|
||||
if will_destroy.contains(&id) {
|
||||
response.not_updated.append(id, SetError::will_destroy());
|
||||
continue 'update;
|
||||
}
|
||||
|
||||
// Obtain submission
|
||||
let document_id = id.document_id();
|
||||
let submission = if let Some(submission) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::EmailSubmission,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
submission
|
||||
.into_deserialized::<EmailSubmission>()
|
||||
.caused_by(trc::location!())?
|
||||
} else {
|
||||
response.not_updated.append(id, SetError::not_found());
|
||||
continue 'update;
|
||||
};
|
||||
|
||||
let mut queue_id = u64::MAX;
|
||||
let mut undo_status = None;
|
||||
|
||||
for (property, mut value) in object.into_expanded_object() {
|
||||
if let Err(err) = response.resolve_self_references(&mut value, 0, false) {
|
||||
response.not_updated.append(id, err);
|
||||
continue 'update;
|
||||
};
|
||||
|
||||
if matches!(&property, Key::Property(EmailSubmissionProperty::Id)) {
|
||||
if !crate::matches_id(&value, id) {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Id)
|
||||
.with_description("The id property is immutable."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let (
|
||||
Key::Property(EmailSubmissionProperty::UndoStatus),
|
||||
Value::Element(EmailSubmissionValue::UndoStatus(undo_status_)),
|
||||
Some(queue_id_),
|
||||
) = (&property, value, submission.inner.queue_id)
|
||||
{
|
||||
undo_status = undo_status_.into();
|
||||
queue_id = queue_id_;
|
||||
} else {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(property.into_owned())
|
||||
.with_description("Field could not be set."),
|
||||
);
|
||||
continue 'update;
|
||||
}
|
||||
}
|
||||
|
||||
match undo_status {
|
||||
Some(email_submission::UndoStatus::Canceled) => {
|
||||
if let Some(queue_message) =
|
||||
self.read_message(queue_id, QueueName::default()).await
|
||||
{
|
||||
// Delete message from queue
|
||||
queue_message.remove(self, None).await;
|
||||
|
||||
// Update record
|
||||
let mut new_submission = submission.inner.clone();
|
||||
new_submission.undo_status = UndoStatus::Canceled;
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::EmailSubmission)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::new()
|
||||
.with_current(submission)
|
||||
.with_changes(new_submission),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
response.updated.append(id, None);
|
||||
} else {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::new(SetErrorType::CannotUnsend).with_description(
|
||||
"The requested message is no longer in the queue.",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::UndoStatus)
|
||||
.with_description("Email submissions can only be cancelled."),
|
||||
);
|
||||
}
|
||||
None => {
|
||||
response.not_updated.append(
|
||||
id,
|
||||
SetError::invalid_properties()
|
||||
.with_description("No properties to set were found."),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process deletions
|
||||
for id in will_destroy {
|
||||
let document_id = id.document_id();
|
||||
if let Some(submission) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::EmailSubmission,
|
||||
document_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
// Update record
|
||||
batch
|
||||
.with_account_id(account_id)
|
||||
.with_collection(Collection::EmailSubmission)
|
||||
.with_document(document_id)
|
||||
.custom(
|
||||
ObjectIndexBuilder::<_, ()>::new().with_current(
|
||||
submission
|
||||
.to_unarchived::<EmailSubmission>()
|
||||
.caused_by(trc::location!())?,
|
||||
),
|
||||
)
|
||||
.caused_by(trc::location!())?
|
||||
.commit_point();
|
||||
response.destroyed.push(id);
|
||||
} else {
|
||||
response.not_destroyed.append(id, SetError::not_found());
|
||||
}
|
||||
}
|
||||
|
||||
// Write changes
|
||||
if !batch.is_empty() {
|
||||
let change_id = self
|
||||
.commit_batch(batch)
|
||||
.await
|
||||
.and_then(|ids| ids.last_change_id(account_id))
|
||||
.caused_by(trc::location!())?;
|
||||
response.new_state = State::Exact(change_id).into();
|
||||
}
|
||||
|
||||
// On success
|
||||
if (request
|
||||
.arguments
|
||||
.on_success_destroy_email
|
||||
.as_ref()
|
||||
.is_some_and(|p| !p.is_empty())
|
||||
|| request
|
||||
.arguments
|
||||
.on_success_update_email
|
||||
.as_ref()
|
||||
.is_some_and(|p| !p.is_empty()))
|
||||
&& response.has_changes()
|
||||
{
|
||||
*next_call = Call {
|
||||
id: String::new(),
|
||||
name: MethodName::new(MethodObject::Email, MethodFunction::Set),
|
||||
method: RequestMethod::Set(SetRequestMethod::Email(Box::new(SetRequest {
|
||||
account_id: request.account_id,
|
||||
if_in_state: None,
|
||||
create: None,
|
||||
update: request.arguments.on_success_update_email.map(|update| {
|
||||
update
|
||||
.into_iter()
|
||||
.filter_map(|(id, value)| {
|
||||
(
|
||||
match id {
|
||||
MaybeIdReference::Id(id) => MaybeInvalid::Value(id),
|
||||
MaybeIdReference::Reference(id_ref) => {
|
||||
MaybeInvalid::Value(*(success_email_ids.get(&id_ref)?))
|
||||
}
|
||||
MaybeIdReference::Invalid(id) => MaybeInvalid::Invalid(id),
|
||||
},
|
||||
value,
|
||||
)
|
||||
.into()
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
destroy: request.arguments.on_success_destroy_email.map(|ids| {
|
||||
MaybeResultReference::Value(
|
||||
ids.into_iter()
|
||||
.filter_map(|id| match id {
|
||||
MaybeIdReference::Id(id) => Some(id),
|
||||
MaybeIdReference::Reference(id_ref) => {
|
||||
success_email_ids.get(&id_ref).copied()
|
||||
}
|
||||
MaybeIdReference::Invalid(_) => None,
|
||||
})
|
||||
.map(MaybeInvalid::Value)
|
||||
.collect(),
|
||||
)
|
||||
}),
|
||||
arguments: Default::default(),
|
||||
}))),
|
||||
}
|
||||
.into();
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
&self,
|
||||
account_id: u32,
|
||||
response: &SetResponse<email_submission::EmailSubmission>,
|
||||
instance: &Arc<ServerInstance>,
|
||||
object: Value<'_, EmailSubmissionProperty, EmailSubmissionValue>,
|
||||
) -> trc::Result<Result<EmailSubmission, SetError<EmailSubmissionProperty>>> {
|
||||
let mut submission = EmailSubmission {
|
||||
email_id: u32::MAX,
|
||||
identity_id: u32::MAX,
|
||||
thread_id: u32::MAX,
|
||||
..Default::default()
|
||||
};
|
||||
let mut mail_from: Option<MailFrom<Cow<'_, str>>> = None;
|
||||
let mut rcpt_to: Vec<RcptTo<Cow<'_, str>>> = Vec::new();
|
||||
|
||||
for (property, mut value) in object.into_expanded_object() {
|
||||
if let Err(err) = response.resolve_self_references(&mut value, 0, false) {
|
||||
return Ok(Err(err));
|
||||
};
|
||||
|
||||
match (&property, value) {
|
||||
(
|
||||
Key::Property(EmailSubmissionProperty::EmailId),
|
||||
Value::Element(EmailSubmissionValue::Id(value)),
|
||||
) => {
|
||||
submission.email_id = value.document_id();
|
||||
submission.thread_id = value.prefix_id();
|
||||
}
|
||||
(
|
||||
Key::Property(EmailSubmissionProperty::IdentityId),
|
||||
Value::Element(EmailSubmissionValue::Id(value)),
|
||||
) => {
|
||||
submission.identity_id = value.document_id();
|
||||
}
|
||||
(Key::Property(EmailSubmissionProperty::Envelope), Value::Object(value)) => {
|
||||
for (property, value) in value.into_vec() {
|
||||
match (&property, value) {
|
||||
(Key::Property(EmailSubmissionProperty::MailFrom), value) => {
|
||||
match parse_envelope_address(value) {
|
||||
Ok((addr, params, smtp_params)) => {
|
||||
match Rfc5321Parser::new(
|
||||
&mut smtp_params
|
||||
.as_ref()
|
||||
.map_or(&b"\n"[..], |p| p.as_bytes())
|
||||
.iter(),
|
||||
)
|
||||
.mail_from_parameters(addr.into())
|
||||
{
|
||||
Ok(addr) => {
|
||||
submission.envelope.mail_from = Address {
|
||||
email: addr.address.as_ref().to_string(),
|
||||
parameters: params,
|
||||
};
|
||||
mail_from = from_into_static(addr).into();
|
||||
}
|
||||
Err(err) => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description(format!(
|
||||
"Failed to parse mailFrom parameters: {err}."
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Ok(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
(
|
||||
Key::Property(EmailSubmissionProperty::RcptTo),
|
||||
Value::Array(value),
|
||||
) => {
|
||||
for addr in value {
|
||||
match parse_envelope_address(addr) {
|
||||
Ok((addr, params, smtp_params)) => {
|
||||
match Rfc5321Parser::new(
|
||||
&mut smtp_params
|
||||
.as_ref()
|
||||
.map_or(&b"\n"[..], |p| p.as_bytes())
|
||||
.iter(),
|
||||
)
|
||||
.rcpt_to_parameters(addr.into())
|
||||
{
|
||||
Ok(addr) => {
|
||||
if !rcpt_to
|
||||
.iter()
|
||||
.any(|rcpt| rcpt.address == addr.address)
|
||||
{
|
||||
submission.envelope.rcpt_to.push(Address {
|
||||
email: addr
|
||||
.address
|
||||
.as_ref()
|
||||
.to_string(),
|
||||
parameters: params,
|
||||
});
|
||||
rcpt_to.push(rcpt_into_static(addr));
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description(format!(
|
||||
"Failed to parse rcptTo parameters: {err}."
|
||||
))));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
return Ok(Err(err));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description("Invalid object property.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Key::Property(EmailSubmissionProperty::Envelope), Value::Null) => {
|
||||
continue;
|
||||
}
|
||||
(Key::Property(EmailSubmissionProperty::UndoStatus), Value::Element(_)) => {
|
||||
continue;
|
||||
}
|
||||
_ => {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(property.into_owned())
|
||||
.with_description("Field could not be set.")));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we have all required fields.
|
||||
if submission.email_id == u32::MAX || submission.identity_id == u32::MAX {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_properties([
|
||||
EmailSubmissionProperty::EmailId,
|
||||
EmailSubmissionProperty::IdentityId,
|
||||
])
|
||||
.with_description(
|
||||
"emailId and identityId properties are required.",
|
||||
)));
|
||||
}
|
||||
|
||||
// Fetch identity's mailFrom
|
||||
let identity_mail_from = if let Some(identity) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::archive(
|
||||
account_id,
|
||||
Collection::Identity,
|
||||
submission.identity_id,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
identity
|
||||
.unarchive::<Identity>()
|
||||
.caused_by(trc::location!())?
|
||||
.email
|
||||
.to_string()
|
||||
} else {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::IdentityId)
|
||||
.with_description("Identity not found.")));
|
||||
};
|
||||
|
||||
// Make sure the envelope address matches the identity email address
|
||||
let mail_from = if let Some(mail_from) = mail_from {
|
||||
if !mail_from.address.eq_ignore_ascii_case(&identity_mail_from) {
|
||||
return Ok(Err(SetError::new(SetErrorType::ForbiddenFrom)
|
||||
.with_description(
|
||||
"Envelope mailFrom does not match identity email address.",
|
||||
)));
|
||||
}
|
||||
mail_from
|
||||
} else {
|
||||
submission.envelope.mail_from = Address {
|
||||
email: identity_mail_from.clone(),
|
||||
parameters: None,
|
||||
};
|
||||
MailFrom {
|
||||
address: Cow::Owned(identity_mail_from),
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
|
||||
// Obtain message metadata
|
||||
let metadata_ = if let Some(metadata) = self
|
||||
.store()
|
||||
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
|
||||
account_id,
|
||||
Collection::Email,
|
||||
submission.email_id,
|
||||
EmailField::Metadata,
|
||||
))
|
||||
.await?
|
||||
{
|
||||
metadata
|
||||
} else {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::EmailId)
|
||||
.with_description("Email not found.")));
|
||||
};
|
||||
let metadata = metadata_
|
||||
.unarchive::<MessageMetadata>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Add recipients to envelope if missing
|
||||
let mut bcc_header = None;
|
||||
if rcpt_to.is_empty() {
|
||||
for header in metadata.contents[0].parts[0].headers.iter() {
|
||||
if matches!(
|
||||
header.name,
|
||||
ArchivedMetadataHeaderName::To
|
||||
| ArchivedMetadataHeaderName::Cc
|
||||
| ArchivedMetadataHeaderName::Bcc
|
||||
) {
|
||||
if matches!(header.name, ArchivedMetadataHeaderName::Bcc) {
|
||||
bcc_header = Some(header);
|
||||
}
|
||||
match &header.value {
|
||||
ArchivedMetadataHeaderValue::AddressList(addr) => {
|
||||
for address in addr.iter() {
|
||||
if let Some(address) = address
|
||||
.address
|
||||
.as_ref()
|
||||
.map(|v| v.as_ref())
|
||||
.and_then(sanitize_email)
|
||||
&& !rcpt_to.iter().any(|rcpt| rcpt.address == address)
|
||||
{
|
||||
submission.envelope.rcpt_to.push(Address {
|
||||
email: address.to_string(),
|
||||
parameters: None,
|
||||
});
|
||||
rcpt_to.push(RcptTo {
|
||||
address: Cow::Owned(address),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
ArchivedMetadataHeaderValue::AddressGroup(groups) => {
|
||||
for group in groups.iter() {
|
||||
for address in group.addresses.iter() {
|
||||
if let Some(address) = address
|
||||
.address
|
||||
.as_ref()
|
||||
.map(|v| v.as_ref())
|
||||
.and_then(sanitize_email)
|
||||
&& !rcpt_to.iter().any(|rcpt| rcpt.address == address)
|
||||
{
|
||||
submission.envelope.rcpt_to.push(Address {
|
||||
email: address.to_string(),
|
||||
parameters: None,
|
||||
});
|
||||
rcpt_to.push(RcptTo {
|
||||
address: Cow::Owned(address),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rcpt_to.is_empty() {
|
||||
return Ok(Err(SetError::new(SetErrorType::NoRecipients)
|
||||
.with_description("No recipients found in email.")));
|
||||
}
|
||||
} else {
|
||||
bcc_header = metadata.contents[0].parts[0]
|
||||
.headers
|
||||
.iter()
|
||||
.find(|header| matches!(header.name, ArchivedMetadataHeaderName::Bcc));
|
||||
}
|
||||
|
||||
// Update sendAt
|
||||
submission.send_at = if mail_from.hold_until > 0 {
|
||||
mail_from.hold_until
|
||||
} else if mail_from.hold_for > 0 {
|
||||
mail_from.hold_for + now()
|
||||
} else {
|
||||
now()
|
||||
};
|
||||
|
||||
// Obtain raw message
|
||||
let mut message = if let Some(message) = self
|
||||
.blob_store()
|
||||
.get_blob(metadata.blob_hash.0.as_slice(), 0..usize::MAX)
|
||||
.await?
|
||||
{
|
||||
if message.len() > self.core.email.mail_max_size {
|
||||
return Ok(Err(SetError::new(SetErrorType::InvalidEmail)
|
||||
.with_description(format!(
|
||||
"Message exceeds maximum size of {} bytes.",
|
||||
self.core.email.mail_max_size
|
||||
))));
|
||||
}
|
||||
|
||||
message
|
||||
} else {
|
||||
return Ok(Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::EmailId)
|
||||
.with_description("Blob for email not found.")));
|
||||
};
|
||||
|
||||
// Remove BCC header if present
|
||||
if let Some(bcc_header) = bcc_header {
|
||||
let mut new_message = Vec::with_capacity(message.len());
|
||||
let range = bcc_header.name_value_range();
|
||||
new_message.extend_from_slice(&message[..range.start]);
|
||||
new_message.extend_from_slice(&message[range.end..]);
|
||||
message = new_message;
|
||||
}
|
||||
|
||||
// Begin local SMTP session
|
||||
let mut session = Session::<NullIo>::local(
|
||||
self.clone(),
|
||||
instance.clone(),
|
||||
SessionData::local(
|
||||
self.account_info(account_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?,
|
||||
None,
|
||||
vec![],
|
||||
vec![],
|
||||
0,
|
||||
),
|
||||
);
|
||||
|
||||
// Spawn SMTP session to avoid overflowing the stack
|
||||
let handle = tokio::spawn(async move {
|
||||
// MAIL FROM
|
||||
let _ = session.handle_mail_from(mail_from).await;
|
||||
if let Some(error) = session.has_failed() {
|
||||
return Err(SetError::new(SetErrorType::ForbiddenMailFrom)
|
||||
.with_description(format!("Server rejected MAIL-FROM: {}", error.trim())));
|
||||
}
|
||||
|
||||
// RCPT TO
|
||||
let mut responses = Vec::new();
|
||||
let mut has_success = false;
|
||||
session.params.rcpt_errors_wait = Duration::from_secs(0);
|
||||
for rcpt in rcpt_to {
|
||||
let addr = rcpt.address.clone();
|
||||
let _ = session.handle_rcpt_to(rcpt).await;
|
||||
let response = session.has_failed();
|
||||
if response.is_none() {
|
||||
has_success = true;
|
||||
}
|
||||
responses.push((addr, response));
|
||||
}
|
||||
|
||||
// DATA
|
||||
if has_success {
|
||||
session.data.message = message;
|
||||
let response = session.queue_message().await;
|
||||
if let smtp::core::State::Accepted(queue_id) = session.state {
|
||||
Ok((responses, Some(queue_id)))
|
||||
} else {
|
||||
Err(
|
||||
SetError::new(SetErrorType::ForbiddenToSend).with_description(format!(
|
||||
"Server rejected DATA: {}",
|
||||
std::str::from_utf8(&response).unwrap().trim()
|
||||
)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Ok((responses, None))
|
||||
}
|
||||
});
|
||||
|
||||
match handle.await {
|
||||
Ok(Ok((responses, queue_id))) => {
|
||||
// Set queue ID
|
||||
if let Some(queue_id) = queue_id {
|
||||
submission.queue_id = Some(queue_id);
|
||||
}
|
||||
|
||||
// Set responses
|
||||
submission.undo_status = if submission.queue_id.is_some() {
|
||||
UndoStatus::Pending
|
||||
} else {
|
||||
UndoStatus::Final
|
||||
};
|
||||
submission.delivery_status = responses
|
||||
.into_iter()
|
||||
.map(|(addr, response)| {
|
||||
(
|
||||
addr.to_string(),
|
||||
DeliveryStatus {
|
||||
delivered: if response.is_none() {
|
||||
Delivered::Unknown
|
||||
} else {
|
||||
Delivered::No
|
||||
},
|
||||
smtp_reply: response
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "250 2.1.5 Queued".to_string()),
|
||||
displayed: false,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Ok(submission))
|
||||
}
|
||||
Ok(Err(err)) => Ok(Err(err)),
|
||||
Err(err) => Err(trc::EventType::Server(trc::ServerEvent::ThreadError)
|
||||
.reason(err)
|
||||
.caused_by(trc::location!())
|
||||
.details("Join Error")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn parse_envelope_address(
|
||||
envelope: Value<'_, EmailSubmissionProperty, EmailSubmissionValue>,
|
||||
) -> Result<
|
||||
(
|
||||
String,
|
||||
Option<VecMap<String, Option<String>>>,
|
||||
Option<String>,
|
||||
),
|
||||
SetError<EmailSubmissionProperty>,
|
||||
> {
|
||||
if let Value::Object(mut envelope) = envelope {
|
||||
if let Some(Value::Str(addr)) =
|
||||
envelope.remove(&Key::Property(EmailSubmissionProperty::Email))
|
||||
{
|
||||
if let Some(addr) = sanitize_email(&addr) {
|
||||
if let Some(Value::Object(params)) =
|
||||
envelope.remove(&Key::Property(EmailSubmissionProperty::Parameters))
|
||||
{
|
||||
let mut params_text = String::new();
|
||||
let mut params_list = VecMap::with_capacity(params.len());
|
||||
|
||||
for (k, v) in params.into_vec() {
|
||||
let k = k.into_string();
|
||||
if !k.is_empty() {
|
||||
if !params_text.is_empty() {
|
||||
params_text.push(' ');
|
||||
}
|
||||
params_text.push_str(&k);
|
||||
if let Value::Str(v) = v {
|
||||
params_text.push('=');
|
||||
params_text.push_str(&v);
|
||||
params_list.append(k, Some(v.into_owned()));
|
||||
} else {
|
||||
params_list.append(k, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
params_text.push('\n');
|
||||
|
||||
Ok((addr.to_string(), Some(params_list), Some(params_text)))
|
||||
} else {
|
||||
Ok((addr.to_string(), None, None))
|
||||
}
|
||||
} else {
|
||||
Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description(format!("Invalid e-mail address {addr:?}.")))
|
||||
}
|
||||
} else {
|
||||
Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description("Missing e-mail address field."))
|
||||
}
|
||||
} else {
|
||||
Err(SetError::invalid_properties()
|
||||
.with_property(EmailSubmissionProperty::Envelope)
|
||||
.with_description("Invalid envelope object."))
|
||||
}
|
||||
}
|
||||
|
||||
fn from_into_static(from: MailFrom<Cow<'_, str>>) -> MailFrom<Cow<'static, str>> {
|
||||
MailFrom {
|
||||
address: from.address.into_owned().into(),
|
||||
flags: from.flags,
|
||||
size: from.size,
|
||||
trans_id: from.trans_id.map(Cow::into_owned).map(Cow::Owned),
|
||||
by: from.by,
|
||||
env_id: from.env_id.map(Cow::into_owned).map(Cow::Owned),
|
||||
solicit: from.solicit.map(Cow::into_owned).map(Cow::Owned),
|
||||
mtrk: from
|
||||
.mtrk
|
||||
.map(smtp_proto::Mtrk::into_owned)
|
||||
.map(|v| smtp_proto::Mtrk {
|
||||
certifier: Cow::Owned(v.certifier),
|
||||
timeout: v.timeout,
|
||||
}),
|
||||
auth: from.auth.map(Cow::into_owned).map(Cow::Owned),
|
||||
hold_for: from.hold_for,
|
||||
hold_until: from.hold_until,
|
||||
mt_priority: from.mt_priority,
|
||||
}
|
||||
}
|
||||
|
||||
fn rcpt_into_static(rcpt: RcptTo<Cow<'_, str>>) -> RcptTo<Cow<'static, str>> {
|
||||
RcptTo {
|
||||
address: rcpt.address.into_owned().into(),
|
||||
orcpt: rcpt.orcpt.map(Cow::into_owned).map(Cow::Owned),
|
||||
rrvs: rcpt.rrvs,
|
||||
flags: rcpt.flags,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user