Files
inbuxa-server/crates/jmap/src/submission/set.rs
T
jcoffey-dev 9976d52e29
ci / github (pull_request) Skipped
ci / fork-checks (pull_request) Successful in 1m8s
ci / build (pull_request) Successful in 4m30s
github/ci (branch) GitHub Actions
Shared mailboxes: a second kind of account lock
A shared mailbox (support@, legal@) belongs to no one person: nobody
signs in to it, and the people assigned open it beside their own mail
at an access level an administrator chose. An account lock already is
most of that: it keeps receiving mail, refuses every sign-in, and its
delegates reach it through real grants on every container (so IMAP,
DAV and JMAP honor them), never including Share. So a shared mailbox is
a lock of a second kind (multi-account spec, MA-S; John, 2026-10-05).

Lock gains kind: "lock" (the default, so stored locks read as before)
or "sharedMailbox", set on create and fixed after. A shared mailbox:

- needs no reason to make, change or end;
- holds up to 100 people, where a lock holds 10;
- runs its own Sieve replies and redirects, so an automatic
  acknowledgement goes out (a lock answers no one);
- records only what is sent as it (audit_send_as, which now covers it),
  not AL-9's access and per-change records, which would bury the log
  for a busy desk;
- sends only as itself (MA-S3): From and Reply-To must be its own
  addresses, so answers come back to the mailbox and not to whoever
  replied; anything else is forbiddenFrom.

The session marks it delegation: {locked: true, kind: "sharedMailbox"},
so a front end that knows no kind still treats it as a lock. The
console's layout gains Management › Directory › Shared Mailboxes
(CustomComponent/SharedMailboxes).

Tests: the account lock suite now goes on to a shared mailbox: made
without a reason with twelve people, sign-in refused, the session's
kind, its vacation reply delivered, an answer sent as it and recorded
as the agent with no per-change records, and a Reply-To naming the
agent refused; a lock unit test reads a stored lock without a kind.
account_lock_tests, jmap_tests, audit_log_tests and imap_tests pass
(RocksDB).
2026-10-05 15:27:52 -07:00

1007 lines
42 KiB
Rust

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
use common::{
Server,
auth::AccessToken,
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::{DlpRule, 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>,
access_token: &AccessToken,
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,
own_addresses_only: bool,
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>,
access_token: &AccessToken,
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,
// inbuxa: MA-S3: a shared mailbox's people send only as it
access_token.delegated_shared_mailbox(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;
// inbuxa: DLP (§2.6): the sender learns it's held
let held = match submission.queue_id {
Some(queue_id) => {
inbuxa_features::mailflow::held::is_held(self.store(), queue_id).await?
}
None => false,
};
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!())?;
// inbuxa: MA-D0a: who sent it, when it went out as someone else
self.audit_send_as(
access_token,
account_id,
document_id,
&submission.envelope.mail_from.email,
)
.await;
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)),
)
.with_key_value(EmailSubmissionProperty::DlpHeld, Value::Bool(held)),
),
);
}
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 {
// inbuxa: held for review: the review decides, not an unsend
// (dlp-and-mail-flow-rules spec, §2.6)
Some(email_submission::UndoStatus::Canceled)
if inbuxa_features::mailflow::held::is_held(self.store(), queue_id).await? =>
{
response.not_updated.append(
id,
SetError::new(SetErrorType::CannotUnsend)
.with_description("The message is held for review and can't be unsent."),
);
}
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,
own_addresses_only: bool,
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();
// inbuxa: DLP (dlp-and-mail-flow-rules spec, §2.5)
let mut dlp_override: Option<String> = None;
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;
}
// inbuxa: the sender's reason to send despite a DLP warning
(Key::Property(EmailSubmissionProperty::DlpOverride), Value::Object(value)) => {
let reason = value
.iter()
.find(|(key, _)| key.to_string() == "reason")
.and_then(|(_, value)| value.as_str().map(|r| r.trim().to_string()))
.filter(|r| !r.is_empty());
match reason {
Some(reason) => dlp_override = Some(reason.chars().take(500).collect()),
None => {
return Ok(Err(SetError::invalid_properties()
.with_property(EmailSubmissionProperty::DlpOverride)
.with_description("An override needs a reason.")));
}
}
}
(Key::Property(EmailSubmissionProperty::DlpOverride), Value::Null) => {
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!())?;
// inbuxa: MA-S3: mail that came to a shared mailbox goes out as it,
// so the answer comes back to the mailbox and not to whoever sent
// it: every From and Reply-To address must be the mailbox's own
if own_addresses_only {
let mut named = Vec::new();
for header in metadata.contents[0].parts[0].headers.iter() {
if !matches!(
header.name,
ArchivedMetadataHeaderName::From | ArchivedMetadataHeaderName::ReplyTo
) {
continue;
}
match &header.value {
ArchivedMetadataHeaderValue::AddressList(addr) => {
named.extend(addr.iter().filter_map(|a| a.address.as_ref().map(|v| v.to_string())));
}
ArchivedMetadataHeaderValue::AddressGroup(groups) => {
for group in groups.iter() {
named.extend(
group
.addresses
.iter()
.filter_map(|a| a.address.as_ref().map(|v| v.to_string())),
);
}
}
_ => {}
}
}
for address in named {
if self.account_id_from_email(&address, true).await? != Some(account_id) {
return Ok(Err(SetError::new(SetErrorType::ForbiddenFrom).with_description(
format!(
"A shared mailbox sends only as its own addresses, so replies come back to it; {address} isn't one."
),
)));
}
}
}
// 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,
),
);
session.data.dlp_override = dlp_override;
// 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 if let Some(refusal) = session.data.dlp_refusal.take() {
// inbuxa: DLP (§2.5): which rules, and what they say
let description = refusal
.rules
.iter()
.map(|(_, notice)| notice.as_str())
.collect::<Vec<_>>()
.join(" ");
Err(SetError::new(if refusal.blocked {
SetErrorType::DlpBlocked
} else {
SetErrorType::DlpWarning
})
.with_description(description)
.with_dlp_rules(
refusal
.rules
.into_iter()
.map(|(name, notice)| DlpRule { name, notice })
.collect(),
))
} 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,
}
}