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,324 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::changes::state::StateManager;
|
||||
use common::Server;
|
||||
use email::submission::{
|
||||
ArchivedAddress, ArchivedEnvelope, ArchivedUndoStatus, Delivered, DeliveryStatus,
|
||||
EmailSubmission,
|
||||
};
|
||||
use jmap_proto::{
|
||||
method::get::{GetRequest, GetResponse},
|
||||
object::email_submission::{self, Displayed, EmailSubmissionProperty, EmailSubmissionValue},
|
||||
types::date::UTCDate,
|
||||
};
|
||||
use jmap_tools::{Key, Map, Value};
|
||||
use smtp::queue::{ArchivedError, ArchivedErrorDetails, ArchivedStatus, Message, spool::SmtpSpool};
|
||||
use smtp_proto::ArchivedResponse;
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
IterateParams, U32_LEN, ValueKey,
|
||||
rkyv::option::ArchivedOption,
|
||||
write::{
|
||||
AlignedBytes, Archive, IndexPropertyClass, ValueClass, key::DeserializeBigEndian, now,
|
||||
},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::EmailSubmissionField,
|
||||
id::Id,
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait EmailSubmissionGet: Sync + Send {
|
||||
fn email_submission_get(
|
||||
&self,
|
||||
request: GetRequest<email_submission::EmailSubmission>,
|
||||
) -> impl Future<Output = trc::Result<GetResponse<email_submission::EmailSubmission>>> + Send;
|
||||
}
|
||||
|
||||
impl EmailSubmissionGet for Server {
|
||||
async fn email_submission_get(
|
||||
&self,
|
||||
mut request: GetRequest<email_submission::EmailSubmission>,
|
||||
) -> trc::Result<GetResponse<email_submission::EmailSubmission>> {
|
||||
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
|
||||
let properties = request.unwrap_properties(&[
|
||||
EmailSubmissionProperty::Id,
|
||||
EmailSubmissionProperty::EmailId,
|
||||
EmailSubmissionProperty::IdentityId,
|
||||
EmailSubmissionProperty::ThreadId,
|
||||
EmailSubmissionProperty::Envelope,
|
||||
EmailSubmissionProperty::SendAt,
|
||||
EmailSubmissionProperty::UndoStatus,
|
||||
EmailSubmissionProperty::DeliveryStatus,
|
||||
EmailSubmissionProperty::DsnBlobIds,
|
||||
EmailSubmissionProperty::MdnBlobIds,
|
||||
]);
|
||||
let account_id = request.account_id.document_id();
|
||||
let ids = if let Some(ids) = ids {
|
||||
ids
|
||||
} else {
|
||||
let mut ids = Vec::with_capacity(16);
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::EmailSubmission.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: now() - (3 * 86400),
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::EmailSubmission.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending()
|
||||
.no_values(),
|
||||
|key, _| {
|
||||
ids.push(Id::from(key.deserialize_be_u32(key.len() - U32_LEN)?));
|
||||
|
||||
Ok(ids.len() < self.core.jmap.get_max_objects)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
ids
|
||||
};
|
||||
let mut response = GetResponse {
|
||||
account_id: request.account_id.into(),
|
||||
state: self
|
||||
.get_state(account_id, SyncCollection::EmailSubmission)
|
||||
.await?
|
||||
.into(),
|
||||
list: Vec::with_capacity(ids.len()),
|
||||
not_found: not_found_ids,
|
||||
};
|
||||
|
||||
for id in ids {
|
||||
// Obtain the email_submission object
|
||||
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
|
||||
} else {
|
||||
response.push_not_found(id);
|
||||
continue;
|
||||
};
|
||||
let submission = submission_
|
||||
.unarchive::<EmailSubmission>()
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Obtain queueId
|
||||
let mut delivery_status = submission
|
||||
.delivery_status
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), DeliveryStatus::from(v)))
|
||||
.collect::<VecMap<_, _>>();
|
||||
let mut queued_status = None;
|
||||
if let Some(queue_id) = submission.queue_id.as_ref().map(u64::from) {
|
||||
if let Some(queued_message_) = self
|
||||
.read_message_archive(queue_id)
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
let queued_message = queued_message_
|
||||
.unarchive::<Message>()
|
||||
.caused_by(trc::location!())?;
|
||||
for rcpt in queued_message.recipients.iter() {
|
||||
*delivery_status.get_mut_or_insert(rcpt.address().to_string()) =
|
||||
DeliveryStatus {
|
||||
smtp_reply: match &rcpt.status {
|
||||
ArchivedStatus::Completed(reply) => {
|
||||
format_archived_response(&reply.response)
|
||||
}
|
||||
ArchivedStatus::TemporaryFailure(reply)
|
||||
| ArchivedStatus::PermanentFailure(reply) => {
|
||||
format_archived_error_details(reply)
|
||||
}
|
||||
ArchivedStatus::Scheduled => "250 2.1.5 Queued".to_string(),
|
||||
},
|
||||
delivered: match &rcpt.status {
|
||||
ArchivedStatus::Scheduled
|
||||
| ArchivedStatus::TemporaryFailure(_) => Delivered::Queued,
|
||||
ArchivedStatus::Completed(_) => Delivered::Yes,
|
||||
ArchivedStatus::PermanentFailure(_) => Delivered::No,
|
||||
},
|
||||
displayed: false,
|
||||
};
|
||||
}
|
||||
queued_status = email_submission::UndoStatus::Pending.into();
|
||||
} else {
|
||||
queued_status = email_submission::UndoStatus::Final.into();
|
||||
}
|
||||
}
|
||||
let undo_status = match submission.undo_status {
|
||||
ArchivedUndoStatus::Canceled => email_submission::UndoStatus::Canceled,
|
||||
ArchivedUndoStatus::Pending => {
|
||||
queued_status.unwrap_or(email_submission::UndoStatus::Pending)
|
||||
}
|
||||
ArchivedUndoStatus::Final => {
|
||||
queued_status.unwrap_or(email_submission::UndoStatus::Final)
|
||||
}
|
||||
};
|
||||
|
||||
let mut result = Map::with_capacity(properties.len());
|
||||
for property in &properties {
|
||||
let value = match property {
|
||||
EmailSubmissionProperty::Id => Value::Element(id.into()),
|
||||
EmailSubmissionProperty::DeliveryStatus => {
|
||||
let mut status = Map::with_capacity(delivery_status.len());
|
||||
|
||||
for (rcpt, delivery_status) in std::mem::take(&mut delivery_status) {
|
||||
status.insert_unchecked(
|
||||
Key::Owned(rcpt),
|
||||
Map::with_capacity(3)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::Delivered,
|
||||
EmailSubmissionValue::Delivered(
|
||||
match delivery_status.delivered {
|
||||
Delivered::Queued => {
|
||||
email_submission::Delivered::Queued
|
||||
}
|
||||
Delivered::Yes => email_submission::Delivered::Yes,
|
||||
Delivered::No => email_submission::Delivered::No,
|
||||
Delivered::Unknown => {
|
||||
email_submission::Delivered::Unknown
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::SmtpReply,
|
||||
delivery_status.smtp_reply,
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::Displayed,
|
||||
Value::Element(EmailSubmissionValue::Displayed(
|
||||
Displayed::Unknown,
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Value::Object(status)
|
||||
}
|
||||
EmailSubmissionProperty::UndoStatus => {
|
||||
Value::Element(EmailSubmissionValue::UndoStatus(undo_status.clone()))
|
||||
}
|
||||
EmailSubmissionProperty::EmailId => Value::Element(
|
||||
Id::from_parts(
|
||||
u32::from(submission.thread_id),
|
||||
u32::from(submission.email_id),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
EmailSubmissionProperty::IdentityId => {
|
||||
Value::Element(Id::from(u32::from(submission.identity_id)).into())
|
||||
}
|
||||
EmailSubmissionProperty::ThreadId => {
|
||||
Value::Element(Id::from(u32::from(submission.thread_id)).into())
|
||||
}
|
||||
EmailSubmissionProperty::Envelope => build_envelope(&submission.envelope),
|
||||
EmailSubmissionProperty::SendAt => Value::Element(EmailSubmissionValue::Date(
|
||||
UTCDate::from_timestamp(u64::from(submission.send_at) as i64),
|
||||
)),
|
||||
EmailSubmissionProperty::MdnBlobIds | EmailSubmissionProperty::DsnBlobIds => {
|
||||
Value::Array(vec![])
|
||||
}
|
||||
_ => Value::Null,
|
||||
};
|
||||
|
||||
result.insert_unchecked(property.clone(), value);
|
||||
}
|
||||
response.list.push(result.into());
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn build_envelope(
|
||||
envelope: &ArchivedEnvelope,
|
||||
) -> Value<'static, EmailSubmissionProperty, EmailSubmissionValue> {
|
||||
Map::with_capacity(2)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::MailFrom,
|
||||
build_address(&envelope.mail_from),
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::RcptTo,
|
||||
Value::Array(envelope.rcpt_to.iter().map(build_address).collect()),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn build_address(
|
||||
envelope: &ArchivedAddress,
|
||||
) -> Value<'static, EmailSubmissionProperty, EmailSubmissionValue> {
|
||||
Map::with_capacity(2)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::Email,
|
||||
Value::Str(envelope.email.to_string().into()),
|
||||
)
|
||||
.with_key_value(
|
||||
EmailSubmissionProperty::Parameters,
|
||||
if let ArchivedOption::Some(params) = &envelope.parameters {
|
||||
Value::Object(Map::from_iter(
|
||||
params
|
||||
.iter()
|
||||
.map(|(k, v)| (Key::Owned(k.to_string()), v.into())),
|
||||
))
|
||||
} else {
|
||||
Value::Null
|
||||
},
|
||||
)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn format_archived_response(response: &ArchivedResponse<Box<str>>) -> String {
|
||||
format!(
|
||||
"Code: {}, Enhanced code: {}.{}.{}, Message: {}",
|
||||
response.code,
|
||||
response.esc[0],
|
||||
response.esc[1],
|
||||
response.esc[2],
|
||||
response.message.replace('\n', " "),
|
||||
)
|
||||
}
|
||||
|
||||
fn format_archived_error_details(response: &ArchivedErrorDetails) -> String {
|
||||
match &response.details {
|
||||
ArchivedError::UnexpectedResponse(response) => format_archived_response(&response.response),
|
||||
ArchivedError::DnsError(details)
|
||||
| ArchivedError::Io(details)
|
||||
| ArchivedError::ConnectionError(details)
|
||||
| ArchivedError::TlsError(details)
|
||||
| ArchivedError::DaneError(details)
|
||||
| ArchivedError::MtaStsError(details) => details.to_string(),
|
||||
ArchivedError::RateLimited => "Rate limited".to_string(),
|
||||
ArchivedError::ConcurrencyLimited => "Concurrency limited".to_string(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod get;
|
||||
pub mod query;
|
||||
pub mod set;
|
||||
@@ -0,0 +1,280 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{api::query::QueryResponseBuilder, changes::state::StateManager};
|
||||
use common::Server;
|
||||
use email::submission::UndoStatus;
|
||||
use jmap_proto::{
|
||||
method::query::{Filter, QueryRequest, QueryResponse},
|
||||
object::email_submission::{self, EmailSubmissionComparator, EmailSubmissionFilter},
|
||||
request::IntoValid,
|
||||
};
|
||||
use std::future::Future;
|
||||
use store::{
|
||||
IterateParams, U32_LEN, U64_LEN, ValueKey,
|
||||
ahash::AHashSet,
|
||||
roaring::RoaringBitmap,
|
||||
search::{SearchFilter, SearchQuery},
|
||||
write::{
|
||||
IndexPropertyClass, QueueClass, SearchIndex, ValueClass, key::DeserializeBigEndian, now,
|
||||
},
|
||||
};
|
||||
use trc::AddContext;
|
||||
use types::{
|
||||
collection::{Collection, SyncCollection},
|
||||
field::EmailSubmissionField,
|
||||
};
|
||||
|
||||
pub trait EmailSubmissionQuery: Sync + Send {
|
||||
fn email_submission_query(
|
||||
&self,
|
||||
request: QueryRequest<email_submission::EmailSubmission>,
|
||||
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
|
||||
}
|
||||
|
||||
struct Submission {
|
||||
document_id: u32,
|
||||
send_at: u64,
|
||||
email_id: u32,
|
||||
thread_id: u32,
|
||||
identity_id: u32,
|
||||
queue_id: u64,
|
||||
undo_status: u8,
|
||||
}
|
||||
|
||||
impl EmailSubmissionQuery for Server {
|
||||
async fn email_submission_query(
|
||||
&self,
|
||||
mut request: QueryRequest<email_submission::EmailSubmission>,
|
||||
) -> trc::Result<QueryResponse> {
|
||||
let account_id = request.account_id.document_id();
|
||||
|
||||
let mut submissions = Vec::with_capacity(16);
|
||||
let mut document_ids = RoaringBitmap::new();
|
||||
|
||||
self.store()
|
||||
.iterate(
|
||||
IterateParams::new(
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::EmailSubmission.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: now() - (3 * 86400),
|
||||
}),
|
||||
},
|
||||
ValueKey {
|
||||
account_id,
|
||||
collection: Collection::EmailSubmission.into(),
|
||||
document_id: 0,
|
||||
class: ValueClass::IndexProperty(IndexPropertyClass::Integer {
|
||||
property: EmailSubmissionField::Metadata.into(),
|
||||
value: u64::MAX,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.ascending(),
|
||||
|key, value| {
|
||||
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
|
||||
|
||||
submissions.push(Submission {
|
||||
document_id,
|
||||
send_at: key.deserialize_be_u64(key.len() - U32_LEN - U64_LEN)?,
|
||||
email_id: value.deserialize_be_u32(0)?,
|
||||
thread_id: value.deserialize_be_u32(U32_LEN)?,
|
||||
identity_id: value.deserialize_be_u32(U32_LEN + U32_LEN)?,
|
||||
queue_id: if value.len() == (U32_LEN * 3) + U64_LEN + 1 {
|
||||
value.deserialize_be_u64(U32_LEN * 3)?
|
||||
} else {
|
||||
0
|
||||
},
|
||||
undo_status: value.last().copied().unwrap(),
|
||||
});
|
||||
|
||||
document_ids.insert(document_id);
|
||||
|
||||
Ok(true)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
if request
|
||||
.filter
|
||||
.iter()
|
||||
.any(|cond| matches!(cond, Filter::Property(EmailSubmissionFilter::UndoStatus(_))))
|
||||
{
|
||||
let pending = UndoStatus::Pending.as_index();
|
||||
for submission in submissions
|
||||
.iter_mut()
|
||||
.filter(|s| s.undo_status == pending && s.queue_id != 0)
|
||||
{
|
||||
if !self
|
||||
.store()
|
||||
.key_exists(ValueKey::from(ValueClass::Queue(QueueClass::Message(
|
||||
submission.queue_id,
|
||||
))))
|
||||
.await
|
||||
.caused_by(trc::location!())?
|
||||
{
|
||||
submission.undo_status = UndoStatus::Final.as_index();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut filters = Vec::with_capacity(request.filter.len());
|
||||
for cond in std::mem::take(&mut request.filter) {
|
||||
match cond {
|
||||
Filter::Property(cond) => match cond {
|
||||
EmailSubmissionFilter::IdentityIds(ids) => {
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.identity_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::EmailIds(ids) => {
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.email_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::ThreadIds(ids) => {
|
||||
let ids = ids
|
||||
.into_valid()
|
||||
.map(|id| id.document_id())
|
||||
.collect::<AHashSet<_>>();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| ids.contains(&s.thread_id))
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::UndoStatus(undo_status) => {
|
||||
let undo_status = match undo_status {
|
||||
email_submission::UndoStatus::Pending => UndoStatus::Pending,
|
||||
email_submission::UndoStatus::Final => UndoStatus::Final,
|
||||
email_submission::UndoStatus::Canceled => UndoStatus::Canceled,
|
||||
}
|
||||
.as_index();
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.undo_status == undo_status)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::Before(before) => {
|
||||
let before = before.timestamp() as u64;
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.send_at < before)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
EmailSubmissionFilter::After(after) => {
|
||||
let after = after.timestamp() as u64;
|
||||
|
||||
filters.push(SearchFilter::is_in_set(RoaringBitmap::from_iter(
|
||||
submissions
|
||||
.iter()
|
||||
.filter(|s| s.send_at > after)
|
||||
.map(|s| s.document_id),
|
||||
)));
|
||||
}
|
||||
|
||||
EmailSubmissionFilter::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedFilter.into_err().details(other));
|
||||
}
|
||||
},
|
||||
Filter::And => {
|
||||
filters.push(SearchFilter::And);
|
||||
}
|
||||
Filter::Or => {
|
||||
filters.push(SearchFilter::Or);
|
||||
}
|
||||
Filter::Not => {
|
||||
filters.push(SearchFilter::Not);
|
||||
}
|
||||
Filter::Close => {
|
||||
filters.push(SearchFilter::End);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let results = SearchQuery::new(SearchIndex::InMemory)
|
||||
.with_filters(filters)
|
||||
.with_mask(document_ids)
|
||||
.filter()
|
||||
.into_bitmap();
|
||||
|
||||
let mut response = QueryResponseBuilder::new(
|
||||
results.len() as usize,
|
||||
self.core.jmap.query_max_results,
|
||||
self.get_state(account_id, SyncCollection::EmailSubmission)
|
||||
.await?,
|
||||
&request,
|
||||
);
|
||||
|
||||
if !results.is_empty() {
|
||||
if let Some(comparator) = request.sort.take().unwrap_or_default().into_iter().next() {
|
||||
match comparator.property {
|
||||
EmailSubmissionComparator::EmailId => {
|
||||
if comparator.is_ascending {
|
||||
submissions.sort_by_key(|s| s.email_id);
|
||||
} else {
|
||||
submissions.sort_by_key(|s| u32::MAX - s.email_id);
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::ThreadId => {
|
||||
if comparator.is_ascending {
|
||||
submissions.sort_by_key(|s| s.thread_id);
|
||||
} else {
|
||||
submissions.sort_by_key(|s| u32::MAX - s.thread_id);
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::SentAt => {
|
||||
if !comparator.is_ascending {
|
||||
submissions.reverse();
|
||||
}
|
||||
}
|
||||
EmailSubmissionComparator::_T(other) => {
|
||||
return Err(trc::JmapEvent::UnsupportedSort.into_err().details(other));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for submission in submissions {
|
||||
if results.contains(submission.document_id)
|
||||
&& !response.add(0, submission.document_id)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response.build()
|
||||
}
|
||||
}
|
||||
@@ -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