Fix MTA: Re-scheduling or editing a queued message reports success but persists nothing for recipients in a non-default virtual queue

This commit is contained in:
Maurus Decimus
2026-06-15 12:20:49 +02:00
parent e07fb0a7c1
commit e559cf5ab5
4 changed files with 187 additions and 13 deletions

View File

@@ -13,7 +13,7 @@ use crate::queue::{
FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT, FROM_UNAUTHENTICATED,
FROM_UNAUTHENTICATED_DMARC, MessageWrapper,
};
use ahash::AHashSet;
use ahash::{AHashMap, AHashSet};
use common::config::smtp::queue::{ArchivedQueueExpiry, QueueName};
use common::ipc::QueueEvent;
use common::network::RcptResolution;
@@ -34,7 +34,7 @@ use store::write::{
AlignedBytes, Archive, Archiver, BatchBuilder, BlobLink, BlobOp, MergeResult, Params,
QueueClass, RegistryClass, ValueClass, now,
};
use store::{Deserialize, IterateParams, Serialize, SerializeInfallible, U64_LEN, ValueKey};
use store::{Deserialize, IterateParams, Serialize, SerializeInfallible, U32_LEN, U64_LEN, ValueKey};
use trc::{AddContext, ServerEvent, SpamEvent};
use types::blob::BlobId;
use types::blob_hash::BlobHash;
@@ -787,6 +787,168 @@ impl MessageWrapper {
}
}
pub async fn save_registry_changes(
mut self,
server: &Server,
prev_events: AHashMap<QueueName, u64>,
modified_rcpts: AHashSet<usize>,
) -> bool {
let mut batch = BatchBuilder::new();
self.release_quota(&mut batch);
for (queue_name, due) in prev_events {
batch.clear(ValueClass::Queue(QueueClass::MessageEvent(
store::write::QueueEvent {
due,
queue_id: self.queue_id,
queue_name: queue_name.into_inner(),
},
)));
}
for (queue_name, due) in self.message.next_events() {
batch.set(
ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent {
due,
queue_id: self.queue_id,
queue_name: queue_name.into_inner(),
})),
Vec::new(),
);
}
let message_bytes = match Archiver::new(self.message).serialize() {
Ok(data) => data,
Err(err) => {
trc::error!(
err.details("Failed to serialize message.")
.span_id(self.span_id)
.caused_by(trc::location!())
);
return false;
}
};
let mut modified_bytes = Vec::with_capacity(modified_rcpts.len() * U32_LEN);
for idx in modified_rcpts {
modified_bytes.extend_from_slice(&(idx as u32).to_be_bytes());
}
batch.merge_fnc(
ValueClass::Queue(QueueClass::Message(self.queue_id)),
Params::with_capacity(3)
.with_u64(self.queue_id)
.with_bytes(modified_bytes)
.with_bytes(message_bytes),
|params, _, bytes| {
let mut cur_message = <Archive<AlignedBytes> as Deserialize>::deserialize(
bytes.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.details("Message no longer exists.")
.caused_by(trc::location!())
.ctx(trc::Key::QueueId, params.u64(0))
})?,
)
.and_then(|archive| archive.deserialize::<Message>())
.caused_by(trc::location!())?;
let new_message_ =
<Archive<AlignedBytes> as Deserialize>::deserialize(params.bytes(2))
.caused_by(trc::location!())?;
let new_message = new_message_
.unarchive::<Message>()
.caused_by(trc::location!())?;
if cur_message.blob_hash.as_slice() == new_message.blob_hash.0.as_slice()
&& cur_message.recipients.len() == new_message.recipients.len()
{
cur_message.priority = new_message.priority.to_native();
cur_message.env_id = new_message.env_id.as_ref().map(|v| v.as_ref().into());
for idx in params.bytes(1).chunks_exact(U32_LEN) {
let rcpt_idx = u32::from_be_bytes(idx.try_into().unwrap()) as usize;
if let Some(rcpt) = new_message.recipients.get(rcpt_idx) {
cur_message.recipients[rcpt_idx] =
rkyv_deserialize(rcpt).caused_by(trc::location!())?;
}
}
Archiver::new(cur_message)
.serialize()
.caused_by(trc::location!())
.map(MergeResult::Update)
} else {
Err(trc::StoreEvent::UnexpectedError
.into_err()
.details("Message blob hash or recipient count mismatch.")
.caused_by(trc::location!())
.ctx(trc::Key::QueueId, params.u64(0)))
}
},
);
if let Err(err) = server.store().write(batch.build_all()).await {
trc::error!(
err.details("Failed to save changes.")
.span_id(self.span_id)
.caused_by(trc::location!())
);
false
} else {
true
}
}
pub async fn remove_registry(
self,
server: &Server,
prev_events: AHashMap<QueueName, u64>,
) -> bool {
let mut batch = BatchBuilder::new();
for (queue_name, due) in prev_events {
batch.clear(ValueClass::Queue(QueueClass::MessageEvent(
store::write::QueueEvent {
due,
queue_id: self.queue_id,
queue_name: queue_name.into_inner(),
},
)));
}
for quota_key in self.message.quota_keys {
match quota_key {
QuotaKey::Count { key, .. } => {
batch.add(ValueClass::Queue(QueueClass::QuotaCount(key.to_vec())), -1);
}
QuotaKey::Size { key, .. } => {
batch.add(
ValueClass::Queue(QueueClass::QuotaSize(key.to_vec())),
-(self.message.size as i64),
);
}
}
}
batch
.clear(BlobOp::Link {
hash: self.message.blob_hash.clone(),
to: BlobLink::Id { id: self.queue_id },
})
.clear(ValueClass::Queue(QueueClass::Message(self.queue_id)));
if let Err(err) = server.store().write(batch.build_all()).await {
trc::error!(
err.details("Failed to write to update queue.")
.span_id(self.span_id)
.caused_by(trc::location!())
);
false
} else {
true
}
}
pub fn has_domain(&self, domains: &[String]) -> bool {
self.message.recipients.iter().any(|r| {
let domain = r.address.domain_part();