diff --git a/CHANGELOG.md b/CHANGELOG.md index b08b90a8..d8136c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,9 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If - ACME: - Include apex domains when requesting certificates for subdomains. - Use the public suffix list to determine the zone name when no origin is provided. -- MTA: Process reports using original `RCPT` before rewriting. +- MTA: + - Allow rescheduling recipients with permanent failures. + - Process reports using original `RCPT` before rewriting. ## [0.16.4] - 2026-05-05 diff --git a/crates/jmap/src/registry/mapping/queued_message.rs b/crates/jmap/src/registry/mapping/queued_message.rs index 40dc233a..e7c357f3 100644 --- a/crates/jmap/src/registry/mapping/queued_message.rs +++ b/crates/jmap/src/registry/mapping/queued_message.rs @@ -129,7 +129,12 @@ pub(crate) async fn queued_message_set( .iter_mut() .find(|r| r.address.as_ref() == address.as_str()) else { - continue; + set.response.not_updated.append( + id, + SetError::invalid_properties() + .with_description(format!("Recipient '{address}' does not exist")), + ); + continue 'outer; }; if rcpt.orcpt.as_deref() != queued_rcpt.orcpt.as_deref() { queued_rcpt.orcpt = rcpt.orcpt.as_deref().map(|v| v.into()); @@ -164,11 +169,12 @@ pub(crate) async fn queued_message_set( } } - if let Some(next_retry) = set_next_retry - && !matches!(queued_rcpt.status, Status::PermanentFailure(_)) - { - queued_rcpt.retry.due = next_retry.timestamp() as u64; - has_changes = true; + if let Some(next_retry) = set_next_retry { + let new_due = next_retry.timestamp() as u64; + if queued_rcpt.retry.due != new_due { + queued_rcpt.retry.due = new_due; + has_changes = true; + } } if matches!(rcpt.status, RecipientStatus::Scheduled) @@ -180,14 +186,14 @@ pub(crate) async fn queued_message_set( } if has_changes { - // Delete message if there are no pending deliveries let message = MessageWrapper::new(queued_message, queue_id, QueueName::default()); - let is_success = if message.message.recipients.iter().any(|recipient| { + let has_pending = message.message.recipients.iter().any(|recipient| { matches!( recipient.status, Status::TemporaryFailure(_) | Status::Scheduled ) - }) { + }); + let is_success = if has_pending || prev_event.is_none() { message.save_changes(set.server, prev_event).await } else { message.remove(set.server, prev_event).await