message/
message.rs

1use crate::address::HeaderAddressList;
2#[cfg(feature = "impl")]
3use crate::dkim::Signer;
4#[cfg(feature = "impl")]
5use crate::dkim::SIGN_POOL;
6pub use crate::queue_name::QueueNameComponents;
7use crate::scheduling::Scheduling;
8use anyhow::Context;
9use bstr::{BString, ByteSlice, ByteVec};
10use chrono::{DateTime, Utc};
11#[cfg(feature = "impl")]
12use config::{any_err, from_lua_value, serialize_options, SerdeWrappedValue};
13use futures::{FutureExt, TryFutureExt};
14use intrusive_collections::{intrusive_adapter, LinkedList, LinkedListAtomicLink};
15use kumo_chrono_helper::*;
16#[cfg(feature = "impl")]
17use kumo_dkim::arc::ARC;
18use kumo_log_types::rfc3464::Report;
19use kumo_log_types::rfc5965::ARFReport;
20use kumo_prometheus::declare_metric;
21#[cfg(feature = "impl")]
22use mailparsing::{AuthenticationResult, AuthenticationResults, EncodeHeaderValue};
23use mailparsing::{
24    CheckFixSettings, DecodedBody, Header, HeaderParseResult, MessageConformance, MimePart,
25};
26#[cfg(feature = "impl")]
27use mlua::{IntoLua, LuaSerdeExt, UserData, UserDataMethods};
28#[cfg(feature = "impl")]
29use mod_dns_resolver::get_resolver_instance;
30use parking_lot::Mutex;
31use rfc5321::parser::EnvelopeAddress;
32use serde::{Deserialize, Serialize};
33use serde_with::formats::PreferOne;
34use serde_with::{serde_as, OneOrMany};
35use spool::{get_data_spool, get_meta_spool, Spool, SpoolId};
36use std::collections::{BTreeMap, HashSet};
37use std::hash::Hash;
38use std::ops::{Deref, DerefMut};
39use std::sync::{Arc, LazyLock, Weak};
40use std::time::{Duration, Instant};
41use timeq::TimerEntryWithDelay;
42
43bitflags::bitflags! {
44    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
45    struct MessageFlags: u8 {
46        /// true if Metadata needs to be saved
47        const META_DIRTY = 1;
48        /// true if Data needs to be saved
49        const DATA_DIRTY = 2;
50        /// true if scheduling restrictions are present in the metadata
51        const SCHEDULED = 4;
52        /// true if high durability writes should always be used
53        const FORCE_SYNC = 8;
54    }
55}
56
57declare_metric! {
58/// Total number of Message objects.
59///
60/// This encompasses all Message objects in various states, whether
61/// they are in a queue, moving between queues, being built as part
62/// of an injection, pending logging, message metadata and/or data
63/// may be either resident or offloaded to spool.
64static MESSAGE_COUNT: IntGauge("message_count");
65}
66
67declare_metric! {
68/// Total number of Message objects with metadata loaded.
69///
70/// Tracks how many messages have their `meta` data resident
71/// in memory.  This may be because they have not yet saved
72/// it, or because the message is being processed and the
73/// metadata is required for that processing.
74static META_COUNT: IntGauge("message_meta_resident_count");
75}
76
77declare_metric! {
78/// Total number of Message objects with body data loaded.
79///
80/// Tracks how many messages have their `data` resident
81/// in memory.  This may be because they have not yet saved
82/// it, or because the message is being processed and the
83/// data is either required to be in memory in order to
84/// deliver the message, or because logging or other
85/// post-injection policy is configured to operate on
86/// the message.
87static DATA_COUNT: IntGauge("message_data_resident_count");
88}
89
90static NO_DATA: LazyLock<Arc<Box<[u8]>>> = LazyLock::new(|| Arc::new(vec![].into_boxed_slice()));
91
92declare_metric! {
93/// How many seconds it takes to save a message to spool.
94///
95/// This metric encompasses the elapsed time to saved
96/// either or both the `meta` and `data` portions of a
97/// message to spool.
98///
99/// High values indicate IO pressure which may be
100/// alleviated by tuning other constraints and/or
101/// [RocksDB Parameters](../../kumo/define_spool/rocks_params.md)
102static SAVE_HIST: Histogram("message_save_latency");
103}
104
105declare_metric! {
106/// How many seconds it takes to load message data from spool.
107///
108/// High values indicate IO pressure which may be caused
109/// by policy that operates on the message body post-reception.
110/// We recommend *avoiding* logging header values as that is
111/// the most common cause of this metric spiking and has
112/// the biggest impact in resolving it.
113///
114/// IO pressure may also be alleviated by tuning other constraints and/or
115/// [RocksDB Parameters](../../kumo/define_spool/rocks_params.md)
116static LOAD_DATA_HIST: Histogram("message_data_load_latency");
117}
118
119declare_metric! {
120/// How long it takes to load message metadata from spool
121///
122/// High values indicate IO pressure which may be
123/// alleviated by tuning other constraints and/or
124/// [RocksDB Parameters](../../kumo/define_spool/rocks_params.md)
125static LOAD_META_HIST: Histogram("message_meta_load_latency");
126}
127
128/// Keeps MESSAGE_COUNT incremented for the lifetime of a `MessageInner`.
129///
130/// Binding the count to construction and `Drop` makes it impossible to
131/// create or destroy a message without the gauge tracking it. The guard is
132/// zero-sized and costs no storage.
133#[derive(Debug)]
134struct MessageGuard;
135
136impl MessageGuard {
137    fn new() -> Self {
138        MESSAGE_COUNT.inc();
139        Self
140    }
141}
142
143impl Drop for MessageGuard {
144    fn drop(&mut self) {
145        MESSAGE_COUNT.dec();
146    }
147}
148
149/// Keeps META_COUNT incremented while resident metadata is held. See
150/// [`MessageGuard`] for the zero-sized guard pattern.
151#[derive(Debug)]
152struct MetaGuard;
153
154impl MetaGuard {
155    fn new() -> Self {
156        META_COUNT.inc();
157        Self
158    }
159}
160
161impl Drop for MetaGuard {
162    fn drop(&mut self) {
163        META_COUNT.dec();
164    }
165}
166
167/// Keeps DATA_COUNT incremented while resident body data is held. See
168/// [`MessageGuard`] for the zero-sized guard pattern.
169#[derive(Debug)]
170struct DataGuard;
171
172impl DataGuard {
173    fn new() -> Self {
174        DATA_COUNT.inc();
175        Self
176    }
177}
178
179impl Drop for DataGuard {
180    fn drop(&mut self) {
181        DATA_COUNT.dec();
182    }
183}
184
185/// Message metadata resident in memory. Holding one keeps META_COUNT
186/// incremented. Dropping it releases the count.
187#[derive(Debug)]
188struct ResidentMeta {
189    value: Box<MetaData>,
190    _guard: MetaGuard,
191}
192
193impl ResidentMeta {
194    fn new(value: Box<MetaData>) -> Self {
195        Self {
196            value,
197            _guard: MetaGuard::new(),
198        }
199    }
200}
201
202impl Deref for ResidentMeta {
203    type Target = MetaData;
204    fn deref(&self) -> &MetaData {
205        &self.value
206    }
207}
208
209impl DerefMut for ResidentMeta {
210    fn deref_mut(&mut self) -> &mut MetaData {
211        &mut self.value
212    }
213}
214
215/// Message body data resident in memory, counted in DATA_COUNT while held. A
216/// resident body is always non-empty. An empty body is the absence of a
217/// `ResidentData` (`None`).
218#[derive(Debug)]
219struct ResidentData {
220    value: Arc<Box<[u8]>>,
221    _guard: DataGuard,
222}
223
224impl ResidentData {
225    fn new(value: Arc<Box<[u8]>>) -> Self {
226        Self {
227            value,
228            _guard: DataGuard::new(),
229        }
230    }
231
232    /// Wraps `value` as a resident body, or yields `None` when it is empty.
233    fn maybe(value: Arc<Box<[u8]>>) -> Option<Self> {
234        if value.is_empty() {
235            None
236        } else {
237            Some(Self::new(value))
238        }
239    }
240}
241
242#[derive(Debug)]
243struct MessageInner {
244    metadata: Option<ResidentMeta>,
245    data: Option<ResidentData>,
246    flags: MessageFlags,
247    num_attempts: u16,
248    due: Option<DateTime<Utc>>,
249    _live: MessageGuard,
250}
251
252impl MessageInner {
253    /// Returns the resident body, or the shared empty buffer when the body is
254    /// not resident. The returned `Arc` is a handle to the bytes and does not
255    /// affect DATA_COUNT.
256    fn data_arc(&self) -> Arc<Box<[u8]>> {
257        match &self.data {
258            Some(resident) => resident.value.clone(),
259            None => NO_DATA.clone(),
260        }
261    }
262
263    /// Installs `data` as the resident body, or clears it when `data` is empty.
264    fn set_data(&mut self, data: Arc<Box<[u8]>>) {
265        if data.is_empty() {
266            self.data = None;
267        } else if let Some(resident) = &mut self.data {
268            // Reuse the existing DATA_COUNT guard instead of dropping and
269            // recreating it. A resident-to-resident update never touches the
270            // gauge.
271            resident.value = data;
272        } else {
273            self.data = Some(ResidentData::new(data));
274        }
275    }
276
277    /// Installs `metadata` as the resident metadata.
278    fn set_metadata(&mut self, metadata: Box<MetaData>) {
279        if let Some(resident) = &mut self.metadata {
280            // Reuse the existing META_COUNT guard instead of dropping and
281            // recreating it. A resident-to-resident update never touches the
282            // gauge.
283            resident.value = metadata;
284        } else {
285            self.metadata = Some(ResidentMeta::new(metadata));
286        }
287    }
288}
289
290#[derive(Debug)]
291pub(crate) struct MessageWithId {
292    id: SpoolId,
293    inner: Mutex<MessageInner>,
294    link: LinkedListAtomicLink,
295}
296
297intrusive_adapter!(
298    pub(crate) MessageWithIdAdapter = Arc<MessageWithId>: MessageWithId { link: LinkedListAtomicLink }
299);
300
301/// A list of messages with an O(1) list overhead; no additional
302/// memory per-message is required to track this list.
303/// However, a given Message can only belong to one instance
304/// of such a list at a time.
305pub struct MessageList {
306    list: LinkedList<MessageWithIdAdapter>,
307    len: usize,
308}
309
310impl Default for MessageList {
311    fn default() -> Self {
312        Self::new()
313    }
314}
315
316impl MessageList {
317    /// Create a new MessageList
318    pub fn new() -> Self {
319        Self {
320            list: LinkedList::new(MessageWithIdAdapter::default()),
321            len: 0,
322        }
323    }
324
325    /// Returns the number of elements contained in the list
326    pub fn len(&self) -> usize {
327        self.len
328    }
329
330    pub fn is_empty(&self) -> bool {
331        self.len == 0
332    }
333
334    /// Take all of the elements from this list and return them
335    /// in a new separate instance of MessageList.
336    pub fn take(&mut self) -> Self {
337        let new_list = Self {
338            list: self.list.take(),
339            len: self.len,
340        };
341        self.len = 0;
342        new_list
343    }
344
345    /// Push a message to the back of the list
346    pub fn push_back(&mut self, message: Message) {
347        self.list.push_back(message.msg_and_id);
348        self.len += 1;
349    }
350
351    /// Pop a message from the front of the list
352    pub fn pop_front(&mut self) -> Option<Message> {
353        self.list.pop_front().map(|msg_and_id| {
354            self.len -= 1;
355            Message { msg_and_id }
356        })
357    }
358
359    /// Pop a message from the back of the list
360    pub fn pop_back(&mut self) -> Option<Message> {
361        self.list.pop_back().map(|msg_and_id| {
362            self.len -= 1;
363            Message { msg_and_id }
364        })
365    }
366
367    /// Pop all of the messages from this list into a vector
368    /// of messages.
369    /// Memory usage is O(number-of-messages).
370    pub fn drain(&mut self) -> Vec<Message> {
371        let mut messages = Vec::with_capacity(self.len);
372        while let Some(msg) = self.pop_front() {
373            messages.push(msg);
374        }
375        messages
376    }
377
378    pub fn extend_from_iter<I>(&mut self, iter: I)
379    where
380        I: Iterator<Item = Message>,
381    {
382        for msg in iter {
383            self.push_back(msg)
384        }
385    }
386}
387
388impl IntoIterator for MessageList {
389    type Item = Message;
390    type IntoIter = MessageListIter;
391    fn into_iter(self) -> MessageListIter {
392        MessageListIter { list: self.list }
393    }
394}
395
396pub struct MessageListIter {
397    list: LinkedList<MessageWithIdAdapter>,
398}
399
400impl Iterator for MessageListIter {
401    type Item = Message;
402    fn next(&mut self) -> Option<Message> {
403        self.list
404            .pop_front()
405            .map(|msg_and_id| Message { msg_and_id })
406    }
407}
408
409#[derive(Clone, Debug)]
410#[cfg_attr(feature = "impl", derive(mlua::FromLua))]
411pub struct Message {
412    pub(crate) msg_and_id: Arc<MessageWithId>,
413}
414
415impl PartialEq for Message {
416    fn eq(&self, other: &Self) -> bool {
417        self.id() == other.id()
418    }
419}
420impl Eq for Message {}
421
422impl Hash for Message {
423    fn hash<H>(&self, hasher: &mut H)
424    where
425        H: std::hash::Hasher,
426    {
427        self.id().hash(hasher)
428    }
429}
430
431#[derive(Clone, Debug)]
432pub struct WeakMessage {
433    weak: Weak<MessageWithId>,
434}
435
436impl WeakMessage {
437    pub fn upgrade(&self) -> Option<Message> {
438        Some(Message {
439            msg_and_id: self.weak.upgrade()?,
440        })
441    }
442}
443
444#[serde_as]
445#[derive(Debug, Serialize, Deserialize, Clone)]
446pub(crate) struct MetaData {
447    pub sender: EnvelopeAddress,
448    #[serde_as(as = "OneOrMany<_, PreferOne>")]
449    pub recipient: Vec<EnvelopeAddress>,
450    pub meta: serde_json::Value,
451    #[serde(default)]
452    pub schedule: Option<Scheduling>,
453}
454
455impl Message {
456    /// Create a new message with the supplied data.
457    /// The message meta and data are marked as dirty
458    pub fn new_dirty(
459        id: SpoolId,
460        sender: EnvelopeAddress,
461        recipient: Vec<EnvelopeAddress>,
462        meta: serde_json::Value,
463        data: Arc<Box<[u8]>>,
464    ) -> anyhow::Result<Self> {
465        anyhow::ensure!(meta.is_object(), "metadata must be a json object");
466        Ok(Self {
467            msg_and_id: Arc::new(MessageWithId {
468                id,
469                inner: Mutex::new(MessageInner {
470                    metadata: Some(ResidentMeta::new(Box::new(MetaData {
471                        sender,
472                        recipient,
473                        meta,
474                        schedule: None,
475                    }))),
476                    data: ResidentData::maybe(data),
477                    flags: MessageFlags::META_DIRTY | MessageFlags::DATA_DIRTY,
478                    num_attempts: 0,
479                    due: None,
480                    _live: MessageGuard::new(),
481                }),
482                link: LinkedListAtomicLink::default(),
483            }),
484        })
485    }
486
487    pub fn weak(&self) -> WeakMessage {
488        WeakMessage {
489            weak: Arc::downgrade(&self.msg_and_id),
490        }
491    }
492
493    /// Helper for creating a message based on spool enumeration.
494    /// Given a spool id and the serialized metadata blob, returns
495    /// a message holding the deserialized version of that metadata.
496    pub fn new_from_spool(id: SpoolId, metadata: Vec<u8>) -> anyhow::Result<Self> {
497        let metadata: MetaData = serde_json::from_slice(&metadata)?;
498
499        let flags = if metadata.schedule.is_some() {
500            MessageFlags::SCHEDULED
501        } else {
502            MessageFlags::empty()
503        };
504
505        Ok(Self {
506            msg_and_id: Arc::new(MessageWithId {
507                id,
508                inner: Mutex::new(MessageInner {
509                    metadata: Some(ResidentMeta::new(Box::new(metadata))),
510                    data: None,
511                    flags,
512                    num_attempts: 0,
513                    due: None,
514                    _live: MessageGuard::new(),
515                }),
516                link: LinkedListAtomicLink::default(),
517            }),
518        })
519    }
520
521    pub(crate) fn new_from_parts(id: SpoolId, metadata: MetaData, data: Arc<Box<[u8]>>) -> Self {
522        let flags = if metadata.schedule.is_some() {
523            MessageFlags::SCHEDULED
524        } else {
525            MessageFlags::empty()
526        };
527
528        Self {
529            msg_and_id: Arc::new(MessageWithId {
530                id,
531                inner: Mutex::new(MessageInner {
532                    metadata: Some(ResidentMeta::new(Box::new(metadata))),
533                    data: ResidentData::maybe(data),
534                    flags: flags | MessageFlags::META_DIRTY | MessageFlags::DATA_DIRTY,
535                    num_attempts: 0,
536                    due: None,
537                    _live: MessageGuard::new(),
538                }),
539                link: LinkedListAtomicLink::default(),
540            }),
541        }
542    }
543
544    pub async fn new_with_id(id: SpoolId) -> anyhow::Result<Self> {
545        let meta_spool = get_meta_spool();
546        let data = meta_spool.load(id).await?;
547        Self::new_from_spool(id, data)
548    }
549
550    pub fn get_num_attempts(&self) -> u16 {
551        let inner = self.msg_and_id.inner.lock();
552        inner.num_attempts
553    }
554
555    pub fn set_num_attempts(&self, num_attempts: u16) {
556        let mut inner = self.msg_and_id.inner.lock();
557        inner.num_attempts = num_attempts;
558    }
559
560    pub fn increment_num_attempts(&self) {
561        let mut inner = self.msg_and_id.inner.lock();
562        inner.num_attempts += 1;
563    }
564
565    pub async fn set_scheduling(
566        &self,
567        scheduling: Option<Scheduling>,
568    ) -> anyhow::Result<Option<Scheduling>> {
569        self.load_meta_if_needed().await?;
570        let mut inner = self.msg_and_id.inner.lock();
571        match &mut inner.metadata {
572            None => anyhow::bail!("set_scheduling: metadata must be loaded first"),
573            Some(meta) => {
574                meta.schedule = scheduling;
575                inner.flags.set(MessageFlags::META_DIRTY, true);
576                inner
577                    .flags
578                    .set(MessageFlags::SCHEDULED, scheduling.is_some());
579                if let Some(sched) = scheduling {
580                    let due = inner.due.unwrap_or_else(Utc::now);
581                    inner.due = Some(sched.adjust_for_schedule(due));
582                }
583                Ok(scheduling)
584            }
585        }
586    }
587
588    pub async fn get_scheduling(&self) -> anyhow::Result<Option<Scheduling>> {
589        self.load_meta_if_needed().await?;
590        let inner = self.msg_and_id.inner.lock();
591        Ok(inner.metadata.as_ref().and_then(|meta| meta.schedule))
592    }
593
594    pub fn get_due(&self) -> Option<DateTime<Utc>> {
595        let inner = self.msg_and_id.inner.lock();
596        inner.due
597    }
598
599    pub async fn delay_with_jitter(&self, limit: i64) -> anyhow::Result<Option<DateTime<Utc>>> {
600        let scale = rand::random::<f32>();
601        let value = (scale * limit as f32) as i64;
602        self.delay_by(seconds(value)?).await
603    }
604
605    pub async fn delay_by(
606        &self,
607        duration: chrono::Duration,
608    ) -> anyhow::Result<Option<DateTime<Utc>>> {
609        let due = Utc::now()
610            .checked_add_signed(duration)
611            .ok_or_else(|| anyhow::anyhow!("due time out of range: now + {duration}"))?;
612        self.set_due(Some(due)).await
613    }
614
615    /// Delay by requested duration, and add up to 1 minute of jitter
616    pub async fn delay_by_and_jitter(
617        &self,
618        duration: chrono::Duration,
619    ) -> anyhow::Result<Option<DateTime<Utc>>> {
620        let scale = rand::random::<f32>();
621        let value = (scale * 60.) as i64;
622        let duration = duration
623            .checked_add(&seconds(value)?)
624            .ok_or_else(|| anyhow::anyhow!("due time out of range: duration + jitter"))?;
625        let due = Utc::now()
626            .checked_add_signed(duration)
627            .ok_or_else(|| anyhow::anyhow!("due time out of range: now + {duration}"))?;
628        self.set_due(Some(due)).await
629    }
630
631    pub async fn set_due(
632        &self,
633        due: Option<DateTime<Utc>>,
634    ) -> anyhow::Result<Option<DateTime<Utc>>> {
635        let due = {
636            let mut inner = self.msg_and_id.inner.lock();
637
638            if !inner.flags.contains(MessageFlags::SCHEDULED) {
639                // This is the simple, fast-path, common case
640                inner.due = due;
641                return Ok(inner.due);
642            }
643
644            let due = due.unwrap_or_else(Utc::now);
645
646            if let Some(meta) = &inner.metadata {
647                inner.due = match &meta.schedule {
648                    Some(sched) => Some(sched.adjust_for_schedule(due)),
649                    None => Some(due),
650                };
651                return Ok(inner.due);
652            }
653
654            // We'll need to load the metadata to correctly
655            // update the schedule for this message
656            due
657        };
658
659        self.load_meta().await?;
660
661        {
662            let mut inner = self.msg_and_id.inner.lock();
663            match &inner.metadata {
664                Some(meta) => {
665                    inner.due = match &meta.schedule {
666                        Some(sched) => Some(sched.adjust_for_schedule(due)),
667                        None => Some(due),
668                    };
669                    Ok(inner.due)
670                }
671                None => anyhow::bail!("loaded metadata, but metadata is not set!?"),
672            }
673        }
674    }
675
676    fn get_data_if_dirty(&self) -> Option<Arc<Box<[u8]>>> {
677        let inner = self.msg_and_id.inner.lock();
678        if inner.flags.contains(MessageFlags::DATA_DIRTY) {
679            Some(inner.data_arc())
680        } else {
681            None
682        }
683    }
684
685    fn get_meta_if_dirty(&self) -> Option<MetaData> {
686        let inner = self.msg_and_id.inner.lock();
687        if inner.flags.contains(MessageFlags::META_DIRTY) {
688            inner.metadata.as_ref().map(|md| (**md).clone())
689        } else {
690            None
691        }
692    }
693
694    pub(crate) async fn clone_meta_data(&self) -> anyhow::Result<MetaData> {
695        self.load_meta_if_needed().await?;
696        let inner = self.msg_and_id.inner.lock();
697        inner
698            .metadata
699            .as_ref()
700            .ok_or_else(|| anyhow::anyhow!("metadata not loaded even though we just loaded it"))
701            .map(|md| (**md).clone())
702    }
703
704    pub fn set_force_sync(&self, force: bool) {
705        let mut inner = self.msg_and_id.inner.lock();
706        inner.flags.set(MessageFlags::FORCE_SYNC, force);
707    }
708
709    pub fn needs_save(&self) -> bool {
710        let inner = self.msg_and_id.inner.lock();
711        inner.flags.contains(MessageFlags::META_DIRTY)
712            || inner.flags.contains(MessageFlags::DATA_DIRTY)
713    }
714
715    pub async fn save(&self, deadline: Option<Instant>) -> anyhow::Result<()> {
716        let _timer = SAVE_HIST.start_timer();
717        self.save_to(&**get_meta_spool(), &**get_data_spool(), deadline)
718            .await
719    }
720
721    pub async fn save_to(
722        &self,
723        meta_spool: &(dyn Spool + Send + Sync),
724        data_spool: &(dyn Spool + Send + Sync),
725        deadline: Option<Instant>,
726    ) -> anyhow::Result<()> {
727        let force_sync = self
728            .msg_and_id
729            .inner
730            .lock()
731            .flags
732            .contains(MessageFlags::FORCE_SYNC);
733
734        let data_fut: futures::future::BoxFuture<'_, anyhow::Result<bool>> =
735            if let Some(data) = self.get_data_if_dirty() {
736                anyhow::ensure!(!data.is_empty(), "message data must not be empty");
737                data_spool
738                    .store(self.msg_and_id.id, data, force_sync, deadline)
739                    .map_ok(|_| true)
740                    .boxed()
741            } else {
742                futures::future::ready(Ok(false)).boxed()
743            };
744        let meta_fut: futures::future::BoxFuture<'_, anyhow::Result<bool>> =
745            if let Some(meta) = self.get_meta_if_dirty() {
746                let meta = Arc::new(serde_json::to_vec(&meta)?.into_boxed_slice());
747                meta_spool
748                    .store(self.msg_and_id.id, meta, force_sync, deadline)
749                    .map_ok(|_| true)
750                    .boxed()
751            } else {
752                futures::future::ready(Ok(false)).boxed()
753            };
754
755        // NOTE: if we have a deadline, it is tempting to want to use
756        // timeout_at here to enforce it, but the underlying spool
757        // futures are not guaranteed to be fully cancel safe, which
758        // is why we pass the deadline down to the save method to allow
759        // them to handle timeouts internally.
760        let (data_res, meta_res) = tokio::join!(data_fut, meta_fut);
761
762        // Clear the dirty flag for each spool that successfully
763        // stored (Ok(true)).  Even when the other spool failed, we
764        // must still record the partial success: the caller may
765        // unwind by removing from both spools, and the remove on a
766        // spool that did not receive the data is harmless.
767        if matches!(data_res, Ok(true)) {
768            self.msg_and_id
769                .inner
770                .lock()
771                .flags
772                .remove(MessageFlags::DATA_DIRTY);
773        }
774        if matches!(meta_res, Ok(true)) {
775            self.msg_and_id
776                .inner
777                .lock()
778                .flags
779                .remove(MessageFlags::META_DIRTY);
780        }
781
782        // Propagate spool errors so callers (notably the SMTP
783        // ingress path and load-shedding-aware code) can react
784        // rather than silently accepting a message whose persistent
785        // storage failed.
786        //
787        // For the both-failed case we preserve the typed
788        // SpoolUnhealthyError on the returned error chain when
789        // either side has one, so callers that match on
790        // root_cause (e.g. the SMTP server's user-facing 421
791        // shaping) still see the actionable reason.  The
792        // non-preferred error is folded into the context message so
793        // it is still visible to anything that formats the error
794        // with `:#`.
795        match (data_res, meta_res) {
796            (Ok(_), Ok(_)) => Ok(()),
797            (Err(data_err), Ok(_)) => Err(data_err.context("data spool store")),
798            (Ok(_), Err(meta_err)) => Err(meta_err.context("meta spool store")),
799            (Err(data_err), Err(meta_err)) => {
800                let data_unhealthy = data_err.root_cause().is::<::spool::SpoolUnhealthyError>();
801                let meta_unhealthy = meta_err.root_cause().is::<::spool::SpoolUnhealthyError>();
802                match (data_unhealthy, meta_unhealthy) {
803                    (true, _) => Err(data_err.context(format!(
804                        "data spool store; meta spool store also failed: {meta_err:#}"
805                    ))),
806                    (false, true) => Err(meta_err.context(format!(
807                        "meta spool store; data spool store also failed: {data_err:#}"
808                    ))),
809                    (false, false) => Err(anyhow::anyhow!(
810                        "data spool store: {data_err:#}; meta spool store: {meta_err:#}"
811                    )),
812                }
813            }
814        }
815    }
816
817    pub fn id(&self) -> &SpoolId {
818        &self.msg_and_id.id
819    }
820
821    /// Save the data+meta if needed, then release both
822    pub async fn save_and_shrink(&self) -> anyhow::Result<bool> {
823        self.save(None).await?;
824        self.shrink()
825    }
826
827    /// Save the data+meta if needed, then release just the data
828    pub async fn save_and_shrink_data(&self) -> anyhow::Result<bool> {
829        self.save(None).await?;
830        self.shrink_data()
831    }
832
833    pub fn shrink_data(&self) -> anyhow::Result<bool> {
834        let mut inner = self.msg_and_id.inner.lock();
835        let mut did_shrink = false;
836        if inner.flags.contains(MessageFlags::DATA_DIRTY) {
837            anyhow::bail!("Cannot shrink message: DATA_DIRTY");
838        }
839        if inner.data.take().is_some() {
840            did_shrink = true;
841        }
842        Ok(did_shrink)
843    }
844
845    pub fn shrink(&self) -> anyhow::Result<bool> {
846        let mut inner = self.msg_and_id.inner.lock();
847        let mut did_shrink = false;
848        if inner.flags.contains(MessageFlags::DATA_DIRTY) {
849            anyhow::bail!("Cannot shrink message: DATA_DIRTY");
850        }
851        if inner.flags.contains(MessageFlags::META_DIRTY) {
852            anyhow::bail!("Cannot shrink message: META_DIRTY");
853        }
854        if inner.metadata.take().is_some() {
855            did_shrink = true;
856        }
857        if inner.data.take().is_some() {
858            did_shrink = true;
859        }
860        Ok(did_shrink)
861    }
862
863    pub async fn sender(&self) -> anyhow::Result<EnvelopeAddress> {
864        self.load_meta_if_needed().await?;
865        let inner = self.msg_and_id.inner.lock();
866        match &inner.metadata {
867            Some(meta) => Ok(meta.sender.clone()),
868            None => anyhow::bail!("Message::sender metadata is not loaded"),
869        }
870    }
871
872    pub async fn set_sender(&self, sender: EnvelopeAddress) -> anyhow::Result<()> {
873        self.load_meta_if_needed().await?;
874        let mut inner = self.msg_and_id.inner.lock();
875        match &mut inner.metadata {
876            Some(meta) => {
877                meta.sender = sender;
878                inner.flags.set(MessageFlags::META_DIRTY, true);
879                Ok(())
880            }
881            None => anyhow::bail!("Message::set_sender: metadata is not loaded"),
882        }
883    }
884
885    #[deprecated = "use recipient_list or first_recipient instead"]
886    pub async fn recipient(&self) -> anyhow::Result<EnvelopeAddress> {
887        self.first_recipient().await
888    }
889
890    pub async fn first_recipient(&self) -> anyhow::Result<EnvelopeAddress> {
891        self.load_meta_if_needed().await?;
892        let inner = self.msg_and_id.inner.lock();
893        match &inner.metadata {
894            Some(meta) => match meta.recipient.first() {
895                Some(recip) => Ok(recip.clone()),
896                None => anyhow::bail!("recipient list is empty!?"),
897            },
898            None => anyhow::bail!("Message::first_recipient: metadata is not loaded"),
899        }
900    }
901
902    pub async fn recipient_list(&self) -> anyhow::Result<Vec<EnvelopeAddress>> {
903        self.load_meta_if_needed().await?;
904        let inner = self.msg_and_id.inner.lock();
905        match &inner.metadata {
906            Some(meta) => Ok(meta.recipient.clone()),
907            None => anyhow::bail!("Message::recipient_list: metadata is not loaded"),
908        }
909    }
910
911    pub async fn recipient_list_string(&self) -> anyhow::Result<Vec<String>> {
912        self.load_meta_if_needed().await?;
913        let inner = self.msg_and_id.inner.lock();
914        match &inner.metadata {
915            Some(meta) => Ok(meta.recipient.iter().map(|a| a.to_string()).collect()),
916            None => anyhow::bail!("Message::recipient_list_string: metadata is not loaded"),
917        }
918    }
919
920    #[deprecated = "use set_recipient_list instead"]
921    pub async fn set_recipient(&self, recipient: EnvelopeAddress) -> anyhow::Result<()> {
922        self.set_recipient_list(vec![recipient]).await
923    }
924
925    pub async fn set_recipient_list(&self, recipient: Vec<EnvelopeAddress>) -> anyhow::Result<()> {
926        self.load_meta_if_needed().await?;
927
928        let mut inner = self.msg_and_id.inner.lock();
929        match &mut inner.metadata {
930            Some(meta) => {
931                meta.recipient = recipient;
932                inner.flags.set(MessageFlags::META_DIRTY, true);
933                Ok(())
934            }
935            None => anyhow::bail!("Message::set_recipient_list: metadata is not loaded"),
936        }
937    }
938
939    pub fn is_meta_loaded(&self) -> bool {
940        self.msg_and_id.inner.lock().metadata.is_some()
941    }
942
943    pub fn is_data_loaded(&self) -> bool {
944        self.msg_and_id.inner.lock().data.is_some()
945    }
946
947    pub async fn load_meta_if_needed(&self) -> anyhow::Result<()> {
948        if self.is_meta_loaded() {
949            return Ok(());
950        }
951        self.load_meta().await
952    }
953
954    pub async fn data(&self) -> anyhow::Result<Arc<Box<[u8]>>> {
955        self.load_data_if_needed().await
956    }
957
958    async fn load_data_if_needed(&self) -> anyhow::Result<Arc<Box<[u8]>>> {
959        if self.is_data_loaded() {
960            return Ok(self.get_data_maybe_not_loaded());
961        }
962        self.load_data().await
963    }
964
965    pub async fn load_meta(&self) -> anyhow::Result<()> {
966        let _timer = LOAD_META_HIST.start_timer();
967        self.load_meta_from(&**get_meta_spool()).await
968    }
969
970    async fn load_meta_from(&self, meta_spool: &(dyn Spool + Send + Sync)) -> anyhow::Result<()> {
971        let id = self.id();
972        let data = meta_spool.load(*id).await?;
973        let mut inner = self.msg_and_id.inner.lock();
974        let metadata: MetaData = serde_json::from_slice(&data)?;
975        inner.set_metadata(Box::new(metadata));
976        Ok(())
977    }
978
979    pub async fn load_data(&self) -> anyhow::Result<Arc<Box<[u8]>>> {
980        let _timer = LOAD_DATA_HIST.start_timer();
981        self.load_data_from(&**get_data_spool()).await
982    }
983
984    async fn load_data_from(
985        &self,
986        data_spool: &(dyn Spool + Send + Sync),
987    ) -> anyhow::Result<Arc<Box<[u8]>>> {
988        let data = Arc::new(data_spool.load(*self.id()).await?.into_boxed_slice());
989        let mut inner = self.msg_and_id.inner.lock();
990        inner.set_data(data.clone());
991        Ok(data)
992    }
993
994    pub fn assign_data(&self, data: Vec<u8>) {
995        let mut inner = self.msg_and_id.inner.lock();
996        inner.set_data(Arc::new(data.into_boxed_slice()));
997        inner.flags.set(MessageFlags::DATA_DIRTY, true);
998    }
999
1000    pub fn get_data_maybe_not_loaded(&self) -> Arc<Box<[u8]>> {
1001        let inner = self.msg_and_id.inner.lock();
1002        inner.data_arc()
1003    }
1004
1005    pub async fn set_meta<S: AsRef<str>, V: Into<serde_json::Value>>(
1006        &self,
1007        key: S,
1008        value: V,
1009    ) -> anyhow::Result<()> {
1010        self.load_meta_if_needed().await?;
1011        let mut inner = self.msg_and_id.inner.lock();
1012        match &mut inner.metadata {
1013            None => anyhow::bail!("set_meta: metadata must be loaded first"),
1014            Some(meta) => {
1015                let key = key.as_ref();
1016                let value = value.into();
1017
1018                match &mut meta.meta {
1019                    serde_json::Value::Object(map) => {
1020                        map.insert(key.to_string(), value);
1021                    }
1022                    _ => anyhow::bail!("metadata is somehow not a json object"),
1023                }
1024
1025                inner.flags.set(MessageFlags::META_DIRTY, true);
1026                Ok(())
1027            }
1028        }
1029    }
1030
1031    pub async fn unset_meta<S: AsRef<str>>(&self, key: S) -> anyhow::Result<()> {
1032        self.load_meta_if_needed().await?;
1033        let mut inner = self.msg_and_id.inner.lock();
1034        match &mut inner.metadata {
1035            None => anyhow::bail!("set_meta: metadata must be loaded first"),
1036            Some(meta) => {
1037                let key = key.as_ref();
1038
1039                match &mut meta.meta {
1040                    serde_json::Value::Object(map) => {
1041                        map.remove(key);
1042                    }
1043                    _ => anyhow::bail!("metadata is somehow not a json object"),
1044                }
1045
1046                inner.flags.set(MessageFlags::META_DIRTY, true);
1047                Ok(())
1048            }
1049        }
1050    }
1051
1052    /// Retrieve `key` as a String.
1053    pub async fn get_meta_string<S: serde_json::value::Index + std::fmt::Display + Copy>(
1054        &self,
1055        key: S,
1056    ) -> anyhow::Result<Option<String>> {
1057        match self.get_meta(key).await {
1058            Ok(serde_json::Value::String(value)) => Ok(Some(value.to_string())),
1059            Ok(serde_json::Value::Null) => Ok(None),
1060            hmm => {
1061                anyhow::bail!("expected '{key}' to be a string value, got {hmm:?}");
1062            }
1063        }
1064    }
1065
1066    pub async fn get_meta_obj(&self) -> anyhow::Result<serde_json::Value> {
1067        self.load_meta_if_needed().await?;
1068        let inner = self.msg_and_id.inner.lock();
1069        match &inner.metadata {
1070            None => anyhow::bail!("get_meta_obj: metadata must be loaded first"),
1071            Some(meta) => Ok(meta.meta.clone()),
1072        }
1073    }
1074
1075    pub async fn get_meta<S: serde_json::value::Index>(
1076        &self,
1077        key: S,
1078    ) -> anyhow::Result<serde_json::Value> {
1079        self.load_meta_if_needed().await?;
1080        let inner = self.msg_and_id.inner.lock();
1081        match &inner.metadata {
1082            None => anyhow::bail!("get_meta: metadata must be loaded first"),
1083            Some(meta) => match meta.meta.get(key) {
1084                Some(value) => Ok(value.clone()),
1085                None => Ok(serde_json::Value::Null),
1086            },
1087        }
1088    }
1089
1090    pub fn age(&self, now: DateTime<Utc>) -> chrono::Duration {
1091        self.msg_and_id.id.age(now)
1092    }
1093
1094    pub async fn get_queue_name(&self) -> anyhow::Result<String> {
1095        Ok(match self.get_meta_string("queue").await? {
1096            Some(name) => name,
1097            None => {
1098                let name = QueueNameComponents::format(
1099                    self.get_meta_string("campaign").await?,
1100                    self.get_meta_string("tenant").await?,
1101                    self.first_recipient()
1102                        .await?
1103                        .domain()
1104                        .to_string()
1105                        .to_lowercase(),
1106                    self.get_meta_string("routing_domain").await?,
1107                );
1108                name.to_string()
1109            }
1110        })
1111    }
1112
1113    #[cfg(feature = "impl")]
1114    pub async fn arc_verify(
1115        &self,
1116        opt_resolver_name: Option<String>,
1117    ) -> anyhow::Result<AuthenticationResult> {
1118        let resolver = get_resolver_instance(&opt_resolver_name)?;
1119        let data = self.data().await?;
1120        let bytes = mailparsing::SharedString::from(data.as_ref().as_ref());
1121
1122        let parsed = mailparsing::Header::parse_headers(bytes.clone())?;
1123        let message = kumo_dkim::ParsedEmail::HeaderOnlyParse { bytes, parsed };
1124
1125        let arc = ARC::verify(&message, &**resolver).await;
1126        Ok(arc.authentication_result())
1127    }
1128
1129    #[cfg(feature = "impl")]
1130    pub async fn arc_seal(
1131        &self,
1132        signer: Signer,
1133        auth_results: AuthenticationResults,
1134        opt_resolver_name: Option<String>,
1135    ) -> anyhow::Result<()> {
1136        let resolver = get_resolver_instance(&opt_resolver_name)?;
1137        let data = self.data().await?;
1138        let bytes = mailparsing::SharedString::from(data.as_ref().as_ref());
1139        let parsed = mailparsing::Header::parse_headers(bytes.clone())?;
1140        let message = kumo_dkim::ParsedEmail::HeaderOnlyParse { bytes, parsed };
1141        let arc = ARC::verify(&message, &**resolver).await;
1142
1143        let headers = arc.seal(&message, auth_results, signer.signer())?;
1144        if !headers.is_empty() {
1145            let mut new_data = Vec::<u8>::with_capacity(data.len() + 1024);
1146
1147            for hdr in headers {
1148                hdr.write_header(&mut new_data).ok();
1149            }
1150            new_data.extend_from_slice(&data);
1151            self.assign_data(new_data);
1152        }
1153
1154        Ok(())
1155    }
1156
1157    #[cfg(feature = "impl")]
1158    pub async fn dkim_verify(
1159        &self,
1160        opt_resolver_name: Option<String>,
1161    ) -> anyhow::Result<Vec<AuthenticationResult>> {
1162        let resolver = get_resolver_instance(&opt_resolver_name)?;
1163        let data = self.data().await?;
1164        let bytes = mailparsing::SharedString::from(data.as_ref().as_ref());
1165
1166        let parsed = mailparsing::Header::parse_headers(bytes.clone())?;
1167        if parsed
1168            .overall_conformance
1169            .contains(MessageConformance::NON_CANONICAL_LINE_ENDINGS)
1170        {
1171            return Ok(vec![AuthenticationResult {
1172                method: "dkim".into(),
1173                method_version: None,
1174                result: "permerror".into(),
1175                reason: Some("message has non-canonical line endings".into()),
1176                props: Default::default(),
1177            }]);
1178        }
1179        let message = kumo_dkim::ParsedEmail::HeaderOnlyParse { bytes, parsed };
1180
1181        let results = kumo_dkim::verify_email_with_resolver(&message, &**resolver).await?;
1182        Ok(results)
1183    }
1184
1185    /// Parses the content into an owned MimePart.
1186    /// Changes to that MimePart are NOT reflected in the underlying
1187    /// message; you must re-assign the message data if you wish to modify
1188    /// the message content.
1189    pub async fn parse(&self) -> anyhow::Result<MimePart<'static>> {
1190        let data = self.data().await?;
1191        let owned_data = String::from_utf8_lossy(data.as_ref().as_ref()).to_string();
1192        Ok(MimePart::parse(owned_data)?)
1193    }
1194
1195    pub async fn parse_rfc3464(&self) -> anyhow::Result<Option<Report>> {
1196        let data = self.data().await?;
1197        Report::parse(&data)
1198    }
1199
1200    pub async fn parse_rfc5965(&self) -> anyhow::Result<Option<ARFReport>> {
1201        let data = self.data().await?;
1202        ARFReport::parse(&data)
1203    }
1204
1205    pub async fn prepend_header(&self, name: Option<&str>, value: &[u8]) -> anyhow::Result<()> {
1206        let data = self.data().await?;
1207        let mut new_data = Vec::with_capacity(size_header(name, value) + 2 + data.len());
1208        emit_header(&mut new_data, name, value);
1209        new_data.extend_from_slice(&data);
1210        self.assign_data(new_data);
1211        Ok(())
1212    }
1213
1214    pub async fn append_header(&self, name: Option<&str>, value: &[u8]) -> anyhow::Result<()> {
1215        let data = self.data().await?;
1216        let mut new_data = Vec::with_capacity(size_header(name, value.as_bytes()) + 2 + data.len());
1217        for (idx, window) in data.windows(4).enumerate() {
1218            if window == b"\r\n\r\n" {
1219                let headers = &data[0..idx + 2];
1220                let body = &data[idx + 2..];
1221
1222                new_data.extend_from_slice(headers);
1223                emit_header(&mut new_data, name, value);
1224                new_data.extend_from_slice(body);
1225                self.assign_data(new_data);
1226                return Ok(());
1227            }
1228        }
1229
1230        anyhow::bail!("append_header could not find the end of the header block");
1231    }
1232
1233    pub async fn get_address_header(
1234        &self,
1235        header_name: &str,
1236    ) -> anyhow::Result<Option<HeaderAddressList>> {
1237        let data = self.data().await?;
1238        let HeaderParseResult { headers, .. } =
1239            mailparsing::Header::parse_headers(data.as_ref().as_ref())?;
1240
1241        match headers.get_first(header_name) {
1242            Some(hdr) => {
1243                let list = hdr.as_address_list()?;
1244                let result: HeaderAddressList = list.into();
1245                Ok(Some(result))
1246            }
1247            None => Ok(None),
1248        }
1249    }
1250
1251    pub async fn get_first_named_header_value(
1252        &self,
1253        name: &str,
1254    ) -> anyhow::Result<Option<BString>> {
1255        let data = self.data().await?;
1256        let HeaderParseResult { headers, .. } = Header::parse_headers(data.as_ref().as_ref())?;
1257
1258        match headers.get_first(name) {
1259            Some(hdr) => Ok(Some(
1260                hdr.as_unstructured()
1261                    .unwrap_or_else(|_| hdr.get_raw_value().into()),
1262            )),
1263            None => Ok(None),
1264        }
1265    }
1266
1267    pub async fn get_all_named_header_values(&self, name: &str) -> anyhow::Result<Vec<BString>> {
1268        let data = self.data().await?;
1269        let HeaderParseResult { headers, .. } = Header::parse_headers(data.as_ref().as_ref())?;
1270
1271        let mut values = vec![];
1272        for hdr in headers.iter_named(name) {
1273            values.push(
1274                hdr.as_unstructured()
1275                    .unwrap_or_else(|_| hdr.get_raw_value().into()),
1276            );
1277        }
1278        Ok(values)
1279    }
1280
1281    pub async fn get_all_headers(&self) -> anyhow::Result<Vec<(BString, BString)>> {
1282        let data = self.data().await?;
1283        let HeaderParseResult { headers, .. } = Header::parse_headers(data.as_ref().as_ref())?;
1284
1285        let mut values = vec![];
1286        for hdr in headers.iter() {
1287            values.push((
1288                hdr.get_name().into(),
1289                hdr.as_unstructured()
1290                    .unwrap_or_else(|_| hdr.get_raw_value().into()),
1291            ));
1292        }
1293        Ok(values)
1294    }
1295
1296    pub async fn retain_headers<F: FnMut(usize, &Header) -> bool>(
1297        &self,
1298        mut func: F,
1299    ) -> anyhow::Result<()> {
1300        let data = self.data().await?;
1301        let mut new_data = Vec::with_capacity(data.len());
1302        let HeaderParseResult {
1303            headers,
1304            body_offset,
1305            ..
1306        } = Header::parse_headers(data.as_ref().as_ref())?;
1307        for (idx, hdr) in headers.iter().enumerate() {
1308            let retain = (func)(idx, hdr);
1309            if !retain {
1310                continue;
1311            }
1312            hdr.write_header(&mut new_data)?;
1313        }
1314        new_data.extend_from_slice(b"\r\n");
1315        new_data.extend_from_slice(&data[body_offset..]);
1316        self.assign_data(new_data);
1317        Ok(())
1318    }
1319
1320    pub async fn remove_first_named_header(&self, name: &str) -> anyhow::Result<()> {
1321        let mut removed = false;
1322        self.retain_headers(|_, hdr| {
1323            if hdr.get_name().eq_ignore_ascii_case(name.as_bytes()) && !removed {
1324                removed = true;
1325                false
1326            } else {
1327                true
1328            }
1329        })
1330        .await
1331    }
1332
1333    pub async fn import_x_headers(&self, names: Vec<String>) -> anyhow::Result<()> {
1334        let specs = if names.is_empty() {
1335            vec![ImportHeaderSpec {
1336                name: "X-*".to_string(),
1337                ..ImportHeaderSpec::default()
1338            }]
1339        } else {
1340            names
1341                .into_iter()
1342                .map(|name| ImportHeaderSpec {
1343                    name,
1344                    ..ImportHeaderSpec::default()
1345                })
1346                .collect()
1347        };
1348        self.import_headers(specs).await
1349    }
1350
1351    pub async fn import_headers(&self, specs: Vec<ImportHeaderSpec>) -> anyhow::Result<()> {
1352        let compiled: Vec<CompiledImportHeaderSpec> = specs
1353            .into_iter()
1354            .map(CompiledImportHeaderSpec::compile)
1355            .collect::<anyhow::Result<_>>()?;
1356
1357        let data = self.data().await?;
1358        let HeaderParseResult { headers, .. } = Header::parse_headers(data.as_ref().as_ref())?;
1359
1360        let mut accumulators: Vec<PerSpecAccumulator> = compiled
1361            .iter()
1362            .map(|s| PerSpecAccumulator::new(s.match_mode))
1363            .collect();
1364        let mut indices_to_remove: HashSet<usize> = HashSet::new();
1365
1366        for (idx, hdr) in headers.iter().enumerate() {
1367            let hdr_name = hdr.get_name();
1368            for (spec_idx, spec) in compiled.iter().enumerate() {
1369                if spec.matches(hdr_name) {
1370                    let key = spec.target_key(&hdr.get_name_lossy());
1371                    let value = hdr.as_unstructured()?.to_str_lossy().to_string();
1372                    accumulators[spec_idx].record(key, value);
1373                    if spec.remove {
1374                        indices_to_remove.insert(idx);
1375                    }
1376                    break;
1377                }
1378            }
1379        }
1380
1381        for acc in accumulators {
1382            acc.write_to(self).await?;
1383        }
1384
1385        if !indices_to_remove.is_empty() {
1386            self.retain_headers(|idx, _| !indices_to_remove.contains(&idx))
1387                .await?;
1388        }
1389
1390        Ok(())
1391    }
1392
1393    pub async fn remove_x_headers(&self, names: Vec<String>) -> anyhow::Result<()> {
1394        self.retain_headers(|_, hdr| {
1395            if names.is_empty() {
1396                !is_x_header(hdr.get_name())
1397            } else {
1398                !names
1399                    .iter()
1400                    .any(|n| hdr.get_name().eq_ignore_ascii_case(n.as_bytes()))
1401            }
1402        })
1403        .await
1404    }
1405
1406    pub async fn remove_all_named_headers(&self, name: &str) -> anyhow::Result<()> {
1407        self.retain_headers(|_, hdr| !hdr.get_name().eq_ignore_ascii_case(name.as_bytes()))
1408            .await
1409    }
1410
1411    #[cfg(feature = "impl")]
1412    pub async fn dkim_sign(&self, signer: Signer) -> anyhow::Result<()> {
1413        let data = self.data().await?;
1414        let header = if let Some(runtime) = SIGN_POOL.get() {
1415            runtime.spawn_blocking(move || signer.sign(&data)).await??
1416        } else {
1417            signer.sign(&data)?
1418        };
1419        self.prepend_header(None, header.as_bytes()).await?;
1420        Ok(())
1421    }
1422
1423    pub async fn import_scheduling_header(
1424        &self,
1425        header_name: &str,
1426        remove: bool,
1427    ) -> anyhow::Result<Option<Scheduling>> {
1428        if let Some(value) = self.get_first_named_header_value(header_name).await? {
1429            let sched: Scheduling = serde_json::from_slice(&value).with_context(|| {
1430                format!("{value} from header {header_name} is not a valid Scheduling header")
1431            })?;
1432            let result = self.set_scheduling(Some(sched)).await?;
1433
1434            if remove {
1435                self.remove_all_named_headers(header_name).await?;
1436            }
1437
1438            Ok(result)
1439        } else {
1440            Ok(None)
1441        }
1442    }
1443
1444    pub async fn append_text_plain(&self, content: &str) -> anyhow::Result<bool> {
1445        let data = self.data().await?;
1446        let mut msg = MimePart::parse(data.as_ref().as_ref())?;
1447        let parts = msg.simplified_structure_pointers()?;
1448        if let Some(p) = parts.text_part.and_then(|p| msg.resolve_ptr_mut(p)) {
1449            match p.body()? {
1450                DecodedBody::Text(text) => {
1451                    let mut text = text.as_bytes().to_vec();
1452                    text.push_str("\r\n");
1453                    text.push_str(content);
1454                    p.replace_text_body("text/plain", &*text)?;
1455
1456                    let new_data = msg.to_message_bytes()?;
1457                    self.assign_data(new_data);
1458                    Ok(true)
1459                }
1460                DecodedBody::Binary(_) => {
1461                    anyhow::bail!("expected text/plain part to be text, but it is binary");
1462                }
1463            }
1464        } else {
1465            Ok(false)
1466        }
1467    }
1468
1469    pub async fn append_text_html(&self, content: &str) -> anyhow::Result<bool> {
1470        let data = self.data().await?;
1471        let mut msg = MimePart::parse(data.as_ref().as_ref())?;
1472        let parts = msg.simplified_structure_pointers()?;
1473        if let Some(p) = parts.html_part.and_then(|p| msg.resolve_ptr_mut(p)) {
1474            match p.body()? {
1475                DecodedBody::Text(text) => {
1476                    let mut text = text.as_bytes().to_vec();
1477
1478                    match text.rfind("</body>").or_else(|| text.rfind("</BODY>")) {
1479                        Some(idx) => {
1480                            text.insert_str(idx, content);
1481                            text.insert_str(idx, "\r\n");
1482                        }
1483                        None => {
1484                            // Just append
1485                            text.push_str("\r\n");
1486                            text.push_str(content);
1487                        }
1488                    }
1489
1490                    p.replace_text_body("text/html", &*text)?;
1491
1492                    let new_data = msg.to_message_bytes()?;
1493                    self.assign_data(new_data);
1494                    Ok(true)
1495                }
1496                DecodedBody::Binary(_) => {
1497                    anyhow::bail!("expected text/html part to be text, but it is binary");
1498                }
1499            }
1500        } else {
1501            Ok(false)
1502        }
1503    }
1504
1505    pub async fn check_fix_conformance(
1506        &self,
1507        check: MessageConformance,
1508        fix: MessageConformance,
1509        settings: Option<&CheckFixSettings>,
1510    ) -> anyhow::Result<()> {
1511        let data = self.data().await?;
1512        let data_bytes = data.as_ref().as_ref();
1513        let msg = MimePart::parse(data_bytes)?;
1514
1515        let mut settings = settings.cloned().unwrap_or_default();
1516
1517        if fix.contains(MessageConformance::MISSING_MESSAGE_ID_HEADER)
1518            && settings.message_id.is_none()
1519            && matches!(msg.headers().message_id(), Err(_) | Ok(None))
1520        {
1521            let sender = self.sender().await?;
1522            let domain = sender.domain();
1523            let id = *self.id();
1524            settings.message_id.replace(format!("{id}@{domain}"));
1525        }
1526
1527        if settings.detect_encoding {
1528            settings.data_bytes.replace(data.clone());
1529        }
1530
1531        let opt_msg = msg.check_fix_conformance(check, fix, settings)?;
1532
1533        if let Some(msg) = opt_msg {
1534            let new_data = msg.to_message_bytes()?;
1535            self.assign_data(new_data);
1536        }
1537
1538        Ok(())
1539    }
1540}
1541
1542fn imported_header_name(name: &str) -> String {
1543    name.chars()
1544        .map(|c| match c.to_ascii_lowercase() {
1545            '-' => '_',
1546            c => c,
1547        })
1548        .collect()
1549}
1550
1551fn is_x_header(name: &[u8]) -> bool {
1552    name.starts_with_str("X-") || name.starts_with_str("x-")
1553}
1554
1555#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
1556#[serde(rename_all = "snake_case")]
1557pub enum MatchMode {
1558    First,
1559    #[default]
1560    Last,
1561    All,
1562}
1563
1564#[derive(Default, Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
1565#[serde(rename_all = "snake_case")]
1566pub enum NameTransform {
1567    #[default]
1568    SnakeCase,
1569    KebabCase,
1570    CamelCase,
1571    PascalCase,
1572}
1573
1574#[derive(Debug, Clone, Deserialize)]
1575#[serde(deny_unknown_fields)]
1576pub struct ImportHeaderSpec {
1577    pub name: String,
1578    #[serde(default, rename = "match")]
1579    pub match_mode: MatchMode,
1580    #[serde(default)]
1581    pub transform: NameTransform,
1582    #[serde(default)]
1583    pub target: Option<String>,
1584    #[serde(default)]
1585    pub remove: bool,
1586}
1587
1588impl Default for ImportHeaderSpec {
1589    fn default() -> Self {
1590        Self {
1591            name: String::new(),
1592            match_mode: MatchMode::default(),
1593            transform: NameTransform::default(),
1594            target: None,
1595            remove: false,
1596        }
1597    }
1598}
1599
1600#[derive(Debug, Clone)]
1601enum HeaderPattern {
1602    /// Case-insensitive exact match.
1603    Exact(String),
1604    /// Matches any header whose name (case-insensitively) starts with the
1605    /// given prefix. Built from a pattern like `X-*`.
1606    Prefix(String),
1607}
1608
1609#[derive(Debug, Clone)]
1610struct CompiledImportHeaderSpec {
1611    pattern: HeaderPattern,
1612    match_mode: MatchMode,
1613    transform: NameTransform,
1614    target: Option<String>,
1615    remove: bool,
1616}
1617
1618impl CompiledImportHeaderSpec {
1619    fn compile(spec: ImportHeaderSpec) -> anyhow::Result<Self> {
1620        let pattern = compile_header_pattern(&spec.name)?;
1621        if spec.target.is_some() && matches!(pattern, HeaderPattern::Prefix(_)) {
1622            anyhow::bail!(
1623                "import_headers: `target` cannot be used with wildcard pattern {:?}",
1624                spec.name
1625            );
1626        }
1627        Ok(Self {
1628            pattern,
1629            match_mode: spec.match_mode,
1630            transform: spec.transform,
1631            target: spec.target,
1632            remove: spec.remove,
1633        })
1634    }
1635
1636    fn matches(&self, hdr_name: &[u8]) -> bool {
1637        match &self.pattern {
1638            HeaderPattern::Exact(s) => hdr_name.eq_ignore_ascii_case(s.as_bytes()),
1639            HeaderPattern::Prefix(p) => {
1640                hdr_name.len() >= p.len() && hdr_name[..p.len()].eq_ignore_ascii_case(p.as_bytes())
1641            }
1642        }
1643    }
1644
1645    fn target_key(&self, matched_name: &str) -> String {
1646        if let Some(target) = &self.target {
1647            return target.clone();
1648        }
1649        apply_name_transform(matched_name, self.transform)
1650    }
1651}
1652
1653fn compile_header_pattern(pat: &str) -> anyhow::Result<HeaderPattern> {
1654    if pat.is_empty() {
1655        anyhow::bail!("import_headers: header name pattern must not be empty");
1656    }
1657    let star_count = pat.chars().filter(|c| *c == '*').count();
1658    if star_count == 0 {
1659        return Ok(HeaderPattern::Exact(pat.to_string()));
1660    }
1661    let Some(prefix) = pat.strip_suffix('*') else {
1662        anyhow::bail!(
1663            "import_headers: only a single trailing `*` is supported in pattern {:?}",
1664            pat
1665        );
1666    };
1667    if star_count > 1 {
1668        anyhow::bail!(
1669            "import_headers: only a single trailing `*` is supported in pattern {:?}",
1670            pat
1671        );
1672    }
1673    if prefix.is_empty() {
1674        anyhow::bail!("import_headers: bare `*` patterns are not supported");
1675    }
1676    Ok(HeaderPattern::Prefix(prefix.to_string()))
1677}
1678
1679fn apply_name_transform(name: &str, transform: NameTransform) -> String {
1680    match transform {
1681        NameTransform::SnakeCase => imported_header_name(name),
1682        NameTransform::KebabCase => name
1683            .split('-')
1684            .map(|p| p.to_ascii_lowercase())
1685            .collect::<Vec<_>>()
1686            .join("-"),
1687        NameTransform::CamelCase => name
1688            .split('-')
1689            .enumerate()
1690            .map(|(i, p)| {
1691                if i == 0 {
1692                    p.to_ascii_lowercase()
1693                } else {
1694                    titlecase_ascii(p)
1695                }
1696            })
1697            .collect::<Vec<_>>()
1698            .join(""),
1699        NameTransform::PascalCase => name
1700            .split('-')
1701            .map(titlecase_ascii)
1702            .collect::<Vec<_>>()
1703            .join(""),
1704    }
1705}
1706
1707fn titlecase_ascii(s: &str) -> String {
1708    let mut chars = s.chars();
1709    match chars.next() {
1710        None => String::new(),
1711        Some(first) => {
1712            let mut out = String::with_capacity(s.len());
1713            out.push(first.to_ascii_uppercase());
1714            for c in chars {
1715                out.push(c.to_ascii_lowercase());
1716            }
1717            out
1718        }
1719    }
1720}
1721
1722enum AccValue {
1723    Str(String),
1724    Arr(Vec<String>),
1725}
1726
1727struct PerSpecAccumulator {
1728    mode: MatchMode,
1729    by_key: BTreeMap<String, AccValue>,
1730}
1731
1732impl PerSpecAccumulator {
1733    fn new(mode: MatchMode) -> Self {
1734        Self {
1735            mode,
1736            by_key: BTreeMap::new(),
1737        }
1738    }
1739
1740    fn record(&mut self, key: String, value: String) {
1741        match self.mode {
1742            MatchMode::First => {
1743                self.by_key.entry(key).or_insert(AccValue::Str(value));
1744            }
1745            MatchMode::Last => {
1746                self.by_key.insert(key, AccValue::Str(value));
1747            }
1748            MatchMode::All => {
1749                let entry = self
1750                    .by_key
1751                    .entry(key)
1752                    .or_insert_with(|| AccValue::Arr(Vec::new()));
1753                if let AccValue::Arr(v) = entry {
1754                    v.push(value);
1755                }
1756            }
1757        }
1758    }
1759
1760    async fn write_to(self, msg: &Message) -> anyhow::Result<()> {
1761        for (key, value) in self.by_key {
1762            match value {
1763                AccValue::Str(s) => msg.set_meta(key, s).await?,
1764                AccValue::Arr(arr) => {
1765                    let json = serde_json::Value::Array(
1766                        arr.into_iter().map(serde_json::Value::String).collect(),
1767                    );
1768                    msg.set_meta(key, json).await?;
1769                }
1770            }
1771        }
1772        Ok(())
1773    }
1774}
1775
1776fn size_header(name: Option<&str>, value: &[u8]) -> usize {
1777    name.map(|name| name.len() + 2).unwrap_or(0) + value.len()
1778}
1779
1780fn emit_header(dest: &mut Vec<u8>, name: Option<&str>, value: &[u8]) {
1781    if let Some(name) = name {
1782        dest.extend_from_slice(name.as_bytes());
1783        dest.extend_from_slice(b": ");
1784    }
1785    dest.extend_from_slice(value);
1786    if !value.ends_with_str("\r\n") {
1787        dest.extend_from_slice(b"\r\n");
1788    }
1789}
1790
1791#[cfg(feature = "impl")]
1792impl UserData for Message {
1793    fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
1794        methods.add_async_method(
1795            "set_meta",
1796            move |_, this, (name, value): (String, mlua::Value)| async move {
1797                let value = serde_json::value::to_value(value).map_err(any_err)?;
1798                this.set_meta(name, value).await.map_err(any_err)?;
1799                Ok(())
1800            },
1801        );
1802        methods.add_async_method("get_meta", move |lua, this, name: String| async move {
1803            let value = this.get_meta(name).await.map_err(any_err)?;
1804            Ok(Some(lua.to_value_with(&value, serialize_options())?))
1805        });
1806        methods.add_async_method("get_data", |lua, this, _: ()| async move {
1807            let data = this.data().await.map_err(any_err)?;
1808            lua.create_string(&*data)
1809        });
1810        methods.add_method("set_data", move |_lua, this, data: mlua::String| {
1811            this.assign_data(data.as_bytes().to_vec());
1812            Ok(())
1813        });
1814
1815        methods.add_async_method("parse_mime", |_lua, this, _: ()| async move {
1816            let data = this.data().await.map_err(any_err)?;
1817            let owned_data = BString::new(data.as_ref().to_vec());
1818            let part = MimePart::parse(owned_data).map_err(any_err)?;
1819            Ok(mod_mimepart::PartRef::new(part))
1820        });
1821
1822        methods.add_async_method("append_text_plain", |_lua, this, data: String| async move {
1823            this.append_text_plain(&data).await.map_err(any_err)
1824        });
1825
1826        methods.add_async_method("append_text_html", |_lua, this, data: String| async move {
1827            this.append_text_html(&data).await.map_err(any_err)
1828        });
1829
1830        methods.add_method("id", move |_, this, _: ()| Ok(this.id().to_string()));
1831        methods.add_async_method("sender", move |_, this, _: ()| async move {
1832            this.sender().await.map_err(any_err)
1833        });
1834
1835        methods.add_method("num_attempts", move |_, this, _: ()| {
1836            Ok(this.get_num_attempts())
1837        });
1838        methods.add_method("increment_num_attempts", move |_, this, _: ()| {
1839            let _: () = this.increment_num_attempts();
1840            Ok(())
1841        });
1842
1843        methods.add_async_method("queue_name", move |_, this, _: ()| async move {
1844            this.get_queue_name().await.map_err(any_err)
1845        });
1846
1847        methods.add_async_method("set_due", move |lua, this, due: mlua::Value| async move {
1848            let due: Option<DateTime<Utc>> = lua.from_value(due)?;
1849            let revised_due = this.set_due(due).await.map_err(any_err)?;
1850            lua.to_value(&revised_due)
1851        });
1852
1853        methods.add_async_method(
1854            "set_sender",
1855            move |lua, this, value: mlua::Value| async move {
1856                let sender = match value {
1857                    mlua::Value::String(s) => {
1858                        let s = s.to_str()?;
1859                        EnvelopeAddress::parse(&s).map_err(any_err)?
1860                    }
1861                    _ => lua.from_value::<EnvelopeAddress>(value.clone())?,
1862                };
1863                this.set_sender(sender).await.map_err(any_err)
1864            },
1865        );
1866
1867        methods.add_async_method("recipient", move |lua, this, _: ()| async move {
1868            let mut recipients = this.recipient_list().await.map_err(any_err)?;
1869            match recipients.len() {
1870                0 => Ok(mlua::Value::Nil),
1871                1 => {
1872                    let recip: EnvelopeAddress = recipients.pop().expect("have 1");
1873                    recip.into_lua(&lua)
1874                }
1875                _ => recipients.into_lua(&lua),
1876            }
1877        });
1878
1879        methods.add_async_method("recipient_list", move |lua, this, _: ()| async move {
1880            let recipients = this.recipient_list().await.map_err(any_err)?;
1881            recipients.into_lua(&lua)
1882        });
1883
1884        methods.add_async_method(
1885            "set_recipient",
1886            move |lua, this, value: mlua::Value| async move {
1887                let recipients = match value {
1888                    mlua::Value::String(s) => {
1889                        let s = s.to_str()?;
1890                        vec![EnvelopeAddress::parse(&s).map_err(any_err)?]
1891                    }
1892                    _ => {
1893                        if let Ok(recips) = lua.from_value::<Vec<EnvelopeAddress>>(value.clone()) {
1894                            recips
1895                        } else {
1896                            vec![lua.from_value::<EnvelopeAddress>(value.clone())?]
1897                        }
1898                    }
1899                };
1900                this.set_recipient_list(recipients).await.map_err(any_err)
1901            },
1902        );
1903
1904        #[cfg(feature = "impl")]
1905        methods.add_async_method("dkim_sign", |_, this, signer: Signer| async move {
1906            this.dkim_sign(signer).await.map_err(any_err)
1907        });
1908
1909        methods.add_async_method("shrink", |_, this, _: ()| async move {
1910            if this.needs_save() {
1911                this.save(None).await.map_err(any_err)?;
1912            }
1913            this.shrink().map_err(any_err)
1914        });
1915
1916        methods.add_async_method("shrink_data", |_, this, _: ()| async move {
1917            if this.needs_save() {
1918                this.save(None).await.map_err(any_err)?;
1919            }
1920            this.shrink_data().map_err(any_err)
1921        });
1922
1923        methods.add_async_method(
1924            "add_authentication_results",
1925            |lua, this, (serv_id, results): (mlua::String, mlua::Value)| async move {
1926                let results: Vec<AuthenticationResult> = lua.from_value(results)?;
1927                let results = AuthenticationResults {
1928                    serv_id: serv_id.as_bytes().as_ref().into(),
1929                    version: None,
1930                    results,
1931                };
1932
1933                this.prepend_header(
1934                    Some("Authentication-Results"),
1935                    results.encode_value().as_bytes(),
1936                )
1937                .await
1938                .map_err(any_err)?;
1939
1940                Ok(())
1941            },
1942        );
1943
1944        #[cfg(feature = "impl")]
1945        methods.add_async_method(
1946            "arc_verify",
1947            |lua, this, opt_resolver_name: Option<String>| async move {
1948                let results = this.arc_verify(opt_resolver_name).await.map_err(any_err)?;
1949                lua.to_value_with(&results, serialize_options())
1950            },
1951        );
1952
1953        #[cfg(feature = "impl")]
1954        methods.add_async_method(
1955            "arc_seal",
1956            |lua,
1957             this,
1958             (signer, serv_id, auth_res, opt_resolver_name): (
1959                Signer,
1960                mlua::String,
1961                mlua::Value,
1962                Option<String>,
1963            )| async move {
1964                let results: Vec<AuthenticationResult> = lua.from_value(auth_res)?;
1965                this.arc_seal(
1966                    signer,
1967                    AuthenticationResults {
1968                        serv_id: serv_id.as_bytes().as_ref().into(),
1969                        version: None,
1970                        results,
1971                    },
1972                    opt_resolver_name,
1973                )
1974                .await
1975                .map_err(any_err)
1976            },
1977        );
1978
1979        #[cfg(feature = "impl")]
1980        methods.add_async_method(
1981            "dkim_verify",
1982            |lua, this, opt_resolver_name: Option<String>| async move {
1983                let results = this.dkim_verify(opt_resolver_name).await.map_err(any_err)?;
1984                lua.to_value_with(&results, serialize_options())
1985            },
1986        );
1987
1988        methods.add_async_method(
1989            "prepend_header",
1990            |_, this, (name, value, encode): (String, String, Option<bool>)| async move {
1991                let encode = encode.unwrap_or(false);
1992                if encode {
1993                    let header = Header::new_unstructured(name.clone(), value);
1994                    this.prepend_header(Some(&name), header.get_raw_value())
1995                        .await
1996                        .map_err(any_err)?;
1997                } else {
1998                    this.prepend_header(Some(&name), value.as_bytes())
1999                        .await
2000                        .map_err(any_err)?;
2001                }
2002                Ok(())
2003            },
2004        );
2005        methods.add_async_method(
2006            "append_header",
2007            |_, this, (name, value, encode): (String, String, Option<bool>)| async move {
2008                let encode = encode.unwrap_or(false);
2009                if encode {
2010                    let header = Header::new_unstructured(name.clone(), value);
2011                    this.append_header(Some(&name), header.get_raw_value())
2012                        .await
2013                        .map_err(any_err)?;
2014                } else {
2015                    this.append_header(Some(&name), value.as_bytes())
2016                        .await
2017                        .map_err(any_err)?;
2018                }
2019                Ok(())
2020            },
2021        );
2022        methods.add_async_method("get_address_header", |_, this, name: String| async move {
2023            this.get_address_header(&name).await.map_err(any_err)
2024        });
2025        methods.add_async_method("from_header", |_, this, ()| async move {
2026            this.get_address_header("From").await.map_err(any_err)
2027        });
2028        methods.add_async_method("to_header", |_, this, ()| async move {
2029            this.get_address_header("To").await.map_err(any_err)
2030        });
2031
2032        methods.add_async_method(
2033            "get_first_named_header_value",
2034            |_, this, name: String| async move {
2035                this.get_first_named_header_value(&name)
2036                    .await
2037                    .map_err(any_err)
2038            },
2039        );
2040        methods.add_async_method(
2041            "get_all_named_header_values",
2042            |_, this, name: String| async move {
2043                this.get_all_named_header_values(&name)
2044                    .await
2045                    .map_err(any_err)
2046            },
2047        );
2048        methods.add_async_method("get_all_headers", |_, this, _: ()| async move {
2049            Ok(this
2050                .get_all_headers()
2051                .await
2052                .map_err(any_err)?
2053                .into_iter()
2054                .map(|(name, value)| vec![name, value])
2055                .collect::<Vec<Vec<BString>>>())
2056        });
2057        methods.add_async_method(
2058            "import_x_headers",
2059            |_, this, names: Option<Vec<String>>| async move {
2060                this.import_x_headers(names.unwrap_or_default())
2061                    .await
2062                    .map_err(any_err)
2063            },
2064        );
2065        methods.add_async_method(
2066            "import_headers",
2067            |_, this, specs: SerdeWrappedValue<Vec<ImportHeaderSpec>>| async move {
2068                this.import_headers(specs.0).await.map_err(any_err)
2069            },
2070        );
2071
2072        methods.add_async_method(
2073            "remove_x_headers",
2074            |_, this, names: Option<Vec<String>>| async move {
2075                this.remove_x_headers(names.unwrap_or_default())
2076                    .await
2077                    .map_err(any_err)
2078            },
2079        );
2080        methods.add_async_method(
2081            "remove_all_named_headers",
2082            |_, this, name: String| async move {
2083                this.remove_all_named_headers(&name).await.map_err(any_err)
2084            },
2085        );
2086
2087        methods.add_async_method(
2088            "import_scheduling_header",
2089            |lua, this, (header_name, remove): (String, bool)| async move {
2090                let opt_schedule = this
2091                    .import_scheduling_header(&header_name, remove)
2092                    .await
2093                    .map_err(any_err)?;
2094                lua.to_value(&opt_schedule)
2095            },
2096        );
2097
2098        methods.add_async_method(
2099            "set_scheduling",
2100            move |lua, this, params: mlua::Value| async move {
2101                let sched: Option<Scheduling> = from_lua_value(&lua, params)?;
2102                let opt_schedule = this.set_scheduling(sched).await.map_err(any_err)?;
2103                lua.to_value(&opt_schedule)
2104            },
2105        );
2106
2107        methods.add_async_method("parse_rfc3464", |lua, this, _: ()| async move {
2108            let report = this.parse_rfc3464().await.map_err(any_err)?;
2109            match report {
2110                Some(report) => lua.to_value_with(&report, serialize_options()),
2111                None => Ok(mlua::Value::Nil),
2112            }
2113        });
2114
2115        methods.add_async_method("parse_rfc5965", |lua, this, _: ()| async move {
2116            let report = this.parse_rfc5965().await.map_err(any_err)?;
2117            match report {
2118                Some(report) => lua.to_value_with(&report, serialize_options()),
2119                None => Ok(mlua::Value::Nil),
2120            }
2121        });
2122
2123        methods.add_async_method("save", |_, this, ()| async move {
2124            this.save(None).await.map_err(any_err)
2125        });
2126
2127        methods.add_method("set_force_sync", move |_, this, force: bool| {
2128            this.set_force_sync(force);
2129            Ok(())
2130        });
2131
2132        methods.add_async_method(
2133            "check_fix_conformance",
2134            |lua, this, (check, fix, settings): (String, String, Option<mlua::Value>)| async move {
2135                use std::str::FromStr;
2136                let check = MessageConformance::from_str(&check).map_err(any_err)?;
2137                let fix = MessageConformance::from_str(&fix).map_err(any_err)?;
2138
2139                let settings = match settings {
2140                    Some(v) => Some(lua.from_value(v).map_err(any_err)?),
2141                    None => None,
2142                };
2143
2144                match this
2145                    .check_fix_conformance(check, fix, settings.as_ref())
2146                    .await
2147                {
2148                    Ok(_) => Ok(None),
2149                    Err(err) => Ok(Some(format!("{err:#}"))),
2150                }
2151            },
2152        );
2153    }
2154}
2155
2156impl TimerEntryWithDelay for WeakMessage {
2157    fn delay(&self) -> Duration {
2158        match self.upgrade() {
2159            None => {
2160                // Dangling/Cancelled. Make it appear due immediately
2161                Duration::from_millis(0)
2162            }
2163            Some(msg) => msg.delay(),
2164        }
2165    }
2166}
2167
2168impl TimerEntryWithDelay for Message {
2169    fn delay(&self) -> Duration {
2170        let inner = self.msg_and_id.inner.lock();
2171        match inner.due {
2172            Some(time) => {
2173                let now = Utc::now();
2174                let delta = time - now;
2175                delta.to_std().unwrap_or(Duration::from_millis(0))
2176            }
2177            None => Duration::from_millis(0),
2178        }
2179    }
2180}
2181
2182#[cfg(test)]
2183pub(crate) mod test {
2184    use super::*;
2185    use serde_json::json;
2186
2187    pub fn new_msg_body<S: AsRef<[u8]>>(s: S) -> Message {
2188        Message::new_dirty(
2189            SpoolId::new(),
2190            EnvelopeAddress::parse("sender@example.com").unwrap(),
2191            vec![EnvelopeAddress::parse("recip@example.com").unwrap()],
2192            serde_json::json!({}),
2193            Arc::new(s.as_ref().to_vec().into_boxed_slice()),
2194        )
2195        .unwrap()
2196    }
2197
2198    fn data_as_string(msg: &Message) -> String {
2199        String::from_utf8(msg.get_data_maybe_not_loaded().to_vec()).unwrap()
2200    }
2201
2202    const X_HDR_CONTENT: &str =
2203        "X-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nFrom :Someone\r\n\r\nBody";
2204
2205    #[tokio::test]
2206    async fn import_all_x_headers() {
2207        let msg = new_msg_body(X_HDR_CONTENT);
2208
2209        msg.import_x_headers(vec![]).await.unwrap();
2210        k9::assert_equal!(
2211            msg.get_meta_obj().await.unwrap(),
2212            json!({
2213                "x_hello": "there",
2214                "x_header": "value",
2215            })
2216        );
2217    }
2218
2219    #[tokio::test]
2220    async fn meta_and_nil() {
2221        let msg = new_msg_body(X_HDR_CONTENT);
2222        // Ensure that json null round-trips
2223        msg.set_meta("test", serde_json::Value::Null).await.unwrap();
2224        k9::assert_equal!(msg.get_meta("test").await.unwrap(), serde_json::Value::Null);
2225
2226        // and that it is exposed to lua as nil
2227        let lua = mlua::Lua::new();
2228        lua.globals().set("msg", msg).unwrap();
2229        lua.load("assert(msg:get_meta('test') == nil)")
2230            .exec()
2231            .unwrap();
2232    }
2233
2234    #[tokio::test]
2235    async fn set_sender_marks_meta_dirty() {
2236        let msg = new_msg_body(X_HDR_CONTENT);
2237        // Simulate a message that has already been persisted by clearing
2238        // the dirty flags that new_dirty() sets on construction.
2239        {
2240            let mut inner = msg.msg_and_id.inner.lock();
2241            inner
2242                .flags
2243                .remove(MessageFlags::DATA_DIRTY | MessageFlags::META_DIRTY);
2244        }
2245
2246        msg.set_sender(EnvelopeAddress::parse("new-sender@example.com").unwrap())
2247            .await
2248            .unwrap();
2249
2250        let inner = msg.msg_and_id.inner.lock();
2251        assert!(
2252            inner.flags.contains(MessageFlags::META_DIRTY),
2253            "changing the sender must mark the metadata dirty"
2254        );
2255        assert!(
2256            !inner.flags.contains(MessageFlags::DATA_DIRTY),
2257            "changing the sender must not mark the data dirty"
2258        );
2259    }
2260
2261    #[tokio::test]
2262    async fn set_recipient_list_marks_meta_dirty() {
2263        let msg = new_msg_body(X_HDR_CONTENT);
2264        {
2265            let mut inner = msg.msg_and_id.inner.lock();
2266            inner
2267                .flags
2268                .remove(MessageFlags::DATA_DIRTY | MessageFlags::META_DIRTY);
2269        }
2270
2271        msg.set_recipient_list(vec![
2272            EnvelopeAddress::parse("new-recip@example.com").unwrap()
2273        ])
2274        .await
2275        .unwrap();
2276
2277        let inner = msg.msg_and_id.inner.lock();
2278        assert!(
2279            inner.flags.contains(MessageFlags::META_DIRTY),
2280            "changing the recipient list must mark the metadata dirty"
2281        );
2282        assert!(
2283            !inner.flags.contains(MessageFlags::DATA_DIRTY),
2284            "changing the recipient list must not mark the data dirty"
2285        );
2286    }
2287
2288    #[tokio::test]
2289    async fn set_sender_after_shrink_does_not_flag_empty_data() {
2290        let msg = new_msg_body(X_HDR_CONTENT);
2291        // Simulate a persisted-then-shrunk message: the dirty flags are cleared
2292        // and the resident data buffer is empty (the body lives only on the
2293        // spool).
2294        {
2295            let mut inner = msg.msg_and_id.inner.lock();
2296            inner
2297                .flags
2298                .remove(MessageFlags::DATA_DIRTY | MessageFlags::META_DIRTY);
2299            inner.data = None;
2300        }
2301
2302        msg.set_sender(EnvelopeAddress::parse("new-sender@example.com").unwrap())
2303            .await
2304            .unwrap();
2305
2306        // Because an envelope change only dirties the metadata, save_to must
2307        // not try to persist the empty data buffer, which would otherwise
2308        // fail with "message data must not be empty".
2309        assert!(
2310            msg.get_data_if_dirty().is_none(),
2311            "an envelope change must not flag the empty data buffer for saving"
2312        );
2313        assert!(
2314            msg.needs_save(),
2315            "the metadata change must still require a save"
2316        );
2317    }
2318
2319    #[tokio::test]
2320    async fn set_scheduling_marks_meta_dirty() {
2321        let msg = new_msg_body(X_HDR_CONTENT);
2322        {
2323            let mut inner = msg.msg_and_id.inner.lock();
2324            inner
2325                .flags
2326                .remove(MessageFlags::DATA_DIRTY | MessageFlags::META_DIRTY);
2327        }
2328
2329        msg.set_scheduling(Some(Scheduling {
2330            restriction: None,
2331            first_attempt: None,
2332            expires: None,
2333        }))
2334        .await
2335        .unwrap();
2336
2337        let inner = msg.msg_and_id.inner.lock();
2338        assert!(
2339            inner.flags.contains(MessageFlags::META_DIRTY),
2340            "changing the schedule must mark the metadata dirty"
2341        );
2342        assert!(
2343            !inner.flags.contains(MessageFlags::DATA_DIRTY),
2344            "changing the schedule must not mark the data dirty"
2345        );
2346    }
2347
2348    #[tokio::test]
2349    async fn import_some_x_headers() {
2350        let msg = new_msg_body(X_HDR_CONTENT);
2351
2352        msg.import_x_headers(vec!["x-hello".to_string()])
2353            .await
2354            .unwrap();
2355        k9::assert_equal!(
2356            msg.get_meta_obj().await.unwrap(),
2357            json!({
2358                "x_hello": "there",
2359            })
2360        );
2361    }
2362
2363    #[tokio::test]
2364    async fn import_headers_wildcard_remove() {
2365        let msg = new_msg_body(X_HDR_CONTENT);
2366
2367        msg.import_headers(vec![ImportHeaderSpec {
2368            name: "X-*".to_string(),
2369            remove: true,
2370            ..ImportHeaderSpec::default()
2371        }])
2372        .await
2373        .unwrap();
2374        k9::assert_equal!(
2375            msg.get_meta_obj().await.unwrap(),
2376            json!({
2377                "x_hello": "there",
2378                "x_header": "value",
2379            })
2380        );
2381        k9::assert_equal!(
2382            data_as_string(&msg),
2383            "Subject: Hello\r\nFrom :Someone\r\n\r\nBody"
2384        );
2385    }
2386
2387    #[tokio::test]
2388    async fn import_headers_match_modes() {
2389        let body =
2390            "Received: from a\r\nReceived: from b\r\nReceived: from c\r\nSubject: hi\r\n\r\nBody";
2391        for (mode, expected) in [
2392            (MatchMode::First, json!("from a")),
2393            (MatchMode::Last, json!("from c")),
2394            (MatchMode::All, json!(["from a", "from b", "from c"])),
2395        ] {
2396            let msg = new_msg_body(body);
2397            msg.import_headers(vec![ImportHeaderSpec {
2398                name: "Received".to_string(),
2399                match_mode: mode,
2400                ..ImportHeaderSpec::default()
2401            }])
2402            .await
2403            .unwrap();
2404            k9::assert_equal!(
2405                msg.get_meta_obj().await.unwrap(),
2406                json!({ "received": expected })
2407            );
2408        }
2409    }
2410
2411    #[tokio::test]
2412    async fn import_headers_no_match_skips_meta() {
2413        let msg = new_msg_body(X_HDR_CONTENT);
2414        msg.import_headers(vec![ImportHeaderSpec {
2415            name: "Nonexistent".to_string(),
2416            match_mode: MatchMode::All,
2417            ..ImportHeaderSpec::default()
2418        }])
2419        .await
2420        .unwrap();
2421        k9::assert_equal!(msg.get_meta_obj().await.unwrap(), json!({}));
2422    }
2423
2424    #[tokio::test]
2425    async fn import_headers_specific_before_wildcard() {
2426        let body = "X-Campaign-Id: 42\r\nX-Mailer: foo\r\n\r\nBody";
2427        let msg = new_msg_body(body);
2428        msg.import_headers(vec![
2429            ImportHeaderSpec {
2430                name: "X-Campaign-Id".to_string(),
2431                target: Some("campaign".to_string()),
2432                ..ImportHeaderSpec::default()
2433            },
2434            ImportHeaderSpec {
2435                name: "X-*".to_string(),
2436                ..ImportHeaderSpec::default()
2437            },
2438        ])
2439        .await
2440        .unwrap();
2441        k9::assert_equal!(
2442            msg.get_meta_obj().await.unwrap(),
2443            json!({
2444                "campaign": "42",
2445                "x_mailer": "foo",
2446            })
2447        );
2448    }
2449
2450    #[test]
2451    fn name_transforms() {
2452        let n = "X-Campaign-Id";
2453        k9::assert_equal!(
2454            apply_name_transform(n, NameTransform::SnakeCase),
2455            "x_campaign_id"
2456        );
2457        k9::assert_equal!(
2458            apply_name_transform(n, NameTransform::KebabCase),
2459            "x-campaign-id"
2460        );
2461        k9::assert_equal!(
2462            apply_name_transform(n, NameTransform::CamelCase),
2463            "xCampaignId"
2464        );
2465        k9::assert_equal!(
2466            apply_name_transform(n, NameTransform::PascalCase),
2467            "XCampaignId"
2468        );
2469    }
2470
2471    #[test]
2472    fn pattern_compile_rejects_bad_patterns() {
2473        compile_header_pattern("").unwrap_err();
2474        compile_header_pattern("*").unwrap_err();
2475        compile_header_pattern("*-Id").unwrap_err();
2476        compile_header_pattern("X-*-Id").unwrap_err();
2477        compile_header_pattern("X-**").unwrap_err();
2478        assert!(matches!(
2479            compile_header_pattern("Subject").unwrap(),
2480            HeaderPattern::Exact(_)
2481        ));
2482        assert!(matches!(
2483            compile_header_pattern("X-*").unwrap(),
2484            HeaderPattern::Prefix(_)
2485        ));
2486    }
2487
2488    #[tokio::test]
2489    async fn import_headers_target_with_wildcard_rejected() {
2490        let msg = new_msg_body(X_HDR_CONTENT);
2491        msg.import_headers(vec![ImportHeaderSpec {
2492            name: "X-*".to_string(),
2493            target: Some("oops".to_string()),
2494            ..ImportHeaderSpec::default()
2495        }])
2496        .await
2497        .unwrap_err();
2498    }
2499
2500    #[tokio::test]
2501    async fn remove_all_x_headers() {
2502        let msg = new_msg_body(X_HDR_CONTENT);
2503
2504        msg.remove_x_headers(vec![]).await.unwrap();
2505        k9::assert_equal!(
2506            data_as_string(&msg),
2507            "Subject: Hello\r\nFrom :Someone\r\n\r\nBody"
2508        );
2509    }
2510
2511    #[tokio::test]
2512    async fn prepend_header_2_params() {
2513        let msg = new_msg_body(X_HDR_CONTENT);
2514
2515        msg.prepend_header(Some("Date"), b"Today").await.unwrap();
2516        k9::assert_equal!(
2517            data_as_string(&msg),
2518            "Date: Today\r\nX-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nFrom :Someone\r\n\r\nBody"
2519        );
2520    }
2521
2522    #[tokio::test]
2523    async fn prepend_header_1_params() {
2524        let msg = new_msg_body(X_HDR_CONTENT);
2525
2526        msg.prepend_header(None, b"Date: Today").await.unwrap();
2527        k9::assert_equal!(
2528            data_as_string(&msg),
2529            "Date: Today\r\nX-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nFrom :Someone\r\n\r\nBody"
2530        );
2531    }
2532
2533    #[tokio::test]
2534    async fn append_header_2_params() {
2535        let msg = new_msg_body(X_HDR_CONTENT);
2536
2537        msg.append_header(Some("Date"), b"Today").await.unwrap();
2538        k9::assert_equal!(
2539            data_as_string(&msg),
2540            "X-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nFrom :Someone\r\nDate: Today\r\n\r\nBody"
2541        );
2542    }
2543
2544    #[tokio::test]
2545    async fn append_header_1_params() {
2546        let msg = new_msg_body(X_HDR_CONTENT);
2547
2548        msg.append_header(None, b"Date: Today").await.unwrap();
2549        k9::assert_equal!(
2550            data_as_string(&msg),
2551            "X-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nFrom :Someone\r\nDate: Today\r\n\r\nBody"
2552        );
2553    }
2554
2555    const MULTI_HEADER_CONTENT: &str =
2556        "X-Hello: there\r\nX-Header: value\r\nSubject: Hello\r\nX-Header: another value\r\nFrom :Someone@somewhere\r\n\r\nBody";
2557
2558    #[tokio::test]
2559    async fn get_first_header() {
2560        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2561        k9::assert_equal!(
2562            msg.get_first_named_header_value("X-header")
2563                .await
2564                .unwrap()
2565                .unwrap(),
2566            "value"
2567        );
2568    }
2569
2570    #[tokio::test]
2571    async fn get_all_header() {
2572        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2573        k9::assert_equal!(
2574            msg.get_all_named_header_values("X-header").await.unwrap(),
2575            vec!["value".to_string(), "another value".to_string()]
2576        );
2577    }
2578
2579    #[tokio::test]
2580    async fn remove_first() {
2581        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2582        msg.remove_first_named_header("X-header").await.unwrap();
2583        k9::assert_equal!(
2584            data_as_string(&msg),
2585            "X-Hello: there\r\nSubject: Hello\r\nX-Header: another value\r\nFrom :Someone@somewhere\r\n\r\nBody"
2586        );
2587    }
2588
2589    #[tokio::test]
2590    async fn remove_all() {
2591        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2592        msg.remove_all_named_headers("X-header").await.unwrap();
2593        k9::assert_equal!(
2594            data_as_string(&msg),
2595            "X-Hello: there\r\nSubject: Hello\r\nFrom :Someone@somewhere\r\n\r\nBody"
2596        );
2597    }
2598
2599    #[tokio::test]
2600    async fn append_text_plain() {
2601        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2602        msg.append_text_plain("I am at the bottom").await.unwrap();
2603        k9::assert_equal!(
2604            data_as_string(&msg),
2605            "X-Hello: there\r\n\
2606             X-Header: value\r\n\
2607             Subject: Hello\r\n\
2608             X-Header: another value\r\n\
2609             From :Someone@somewhere\r\n\
2610             Content-Type: text/plain;\r\n\
2611             \tcharset=\"us-ascii\"\r\n\
2612             \r\n\
2613             Body\r\n\
2614             I am at the bottom\r\n"
2615        );
2616    }
2617
2618    const MIXED_CONTENT: &str = "Content-Type: multipart/mixed;\r\n\
2619\tboundary=\"my-boundary\"\r\n\
2620\r\n\
2621--my-boundary\r\n\
2622Content-Type: text/plain;\r\n\
2623\tcharset=\"us-ascii\"\r\n\
2624\r\n\
2625plain text\r\n\
2626--my-boundary\r\n\
2627Content-Type: text/html;\r\n\
2628\tcharset=\"us-ascii\"\r\n\
2629\r\n\
2630<b>rich</b> text\r\n\
2631--my-boundary\r\n\
2632Content-Type: application/octet-stream\r\n\
2633Content-Transfer-Encoding: base64\r\n\
2634Content-Disposition: attachment;\r\n\
2635\tfilename=\"woot.bin\"\r\n\
2636Content-ID: <woot.id@somewhere>\r\n\
2637\r\n\
2638AAECAw==\r\n\
2639--my-boundary--\r\n\
2640\r\n";
2641
2642    const MIXED_CONTENT_ENCLOSING_BODY: &str = "Content-Type: multipart/mixed;\r\n\
2643\tboundary=\"my-boundary\"\r\n\
2644\r\n\
2645--my-boundary\r\n\
2646Content-Type: text/plain;\r\n\
2647\tcharset=\"us-ascii\"\r\n\
2648\r\n\
2649plain text\r\n\
2650--my-boundary\r\n\
2651Content-Type: text/html;\r\n\
2652\tcharset=\"us-ascii\"\r\n\
2653\r\n\
2654<BODY>\r\n\
2655<b>rich</b> text\r\n\
2656</BODY>\r\n\
2657--my-boundary\r\n\
2658Content-Type: application/octet-stream\r\n\
2659Content-Transfer-Encoding: base64\r\n\
2660Content-Disposition: attachment;\r\n\
2661\tfilename=\"woot.bin\"\r\n\
2662Content-ID: <woot.id>\r\n\
2663\r\n\
2664AAECAw==\r\n\
2665--my-boundary--\r\n\
2666\r\n";
2667
2668    #[tokio::test]
2669    async fn append_text_html() {
2670        let msg = new_msg_body(MIXED_CONTENT);
2671        msg.append_text_html("bottom html").await.unwrap();
2672        k9::snapshot!(
2673            data_as_string(&msg),
2674            r#"
2675Content-Type: multipart/mixed;\r
2676\tboundary="my-boundary"\r
2677\r
2678--my-boundary\r
2679Content-Type: text/plain;\r
2680\tcharset="us-ascii"\r
2681\r
2682plain text\r
2683--my-boundary\r
2684Content-Type: text/html;\r
2685\tcharset="us-ascii"\r
2686\r
2687<b>rich</b> text\r
2688\r
2689bottom html\r
2690--my-boundary\r
2691Content-Type: application/octet-stream\r
2692Content-Transfer-Encoding: base64\r
2693Content-Disposition: attachment;\r
2694\tfilename="woot.bin"\r
2695Content-ID: <woot.id@somewhere>\r
2696\r
2697AAECAw==\r
2698--my-boundary--\r
2699\r
2700
2701"#
2702        );
2703
2704        let msg = new_msg_body(MIXED_CONTENT_ENCLOSING_BODY);
2705        msg.append_text_html("bottom html 👻").await.unwrap();
2706        k9::snapshot!(
2707            data_as_string(&msg),
2708            r#"
2709Content-Type: multipart/mixed;\r
2710\tboundary="my-boundary"\r
2711\r
2712--my-boundary\r
2713Content-Type: text/plain;\r
2714\tcharset="us-ascii"\r
2715\r
2716plain text\r
2717--my-boundary\r
2718Content-Type: text/html;\r
2719\tcharset="utf-8"\r
2720Content-Transfer-Encoding: quoted-printable\r
2721\r
2722<BODY>\r
2723<b>rich</b> text\r
2724\r
2725bottom html =F0=9F=91=BB</BODY>\r
2726--my-boundary\r
2727Content-Type: application/octet-stream\r
2728Content-Transfer-Encoding: base64\r
2729Content-Disposition: attachment;\r
2730\tfilename="woot.bin"\r
2731Content-ID: <woot.id>\r
2732\r
2733AAECAw==\r
2734--my-boundary--\r
2735\r
2736
2737"#
2738        );
2739    }
2740
2741    #[tokio::test]
2742    async fn append_text_plain_mixed() {
2743        let msg = new_msg_body(MIXED_CONTENT);
2744        msg.append_text_plain("bottom text 👾").await.unwrap();
2745        k9::snapshot!(
2746            data_as_string(&msg),
2747            r#"
2748Content-Type: multipart/mixed;\r
2749\tboundary="my-boundary"\r
2750\r
2751--my-boundary\r
2752Content-Type: text/plain;\r
2753\tcharset="utf-8"\r
2754Content-Transfer-Encoding: quoted-printable\r
2755\r
2756plain text\r
2757\r
2758bottom text =F0=9F=91=BE\r
2759--my-boundary\r
2760Content-Type: text/html;\r
2761\tcharset="us-ascii"\r
2762\r
2763<b>rich</b> text\r
2764--my-boundary\r
2765Content-Type: application/octet-stream\r
2766Content-Transfer-Encoding: base64\r
2767Content-Disposition: attachment;\r
2768\tfilename="woot.bin"\r
2769Content-ID: <woot.id@somewhere>\r
2770\r
2771AAECAw==\r
2772--my-boundary--\r
2773\r
2774
2775"#
2776        );
2777    }
2778
2779    #[tokio::test]
2780    async fn check_conformance_angle_msg_id() {
2781        const DOUBLE_ANGLE_ONLY: &str = "Subject: hello\r
2782Message-ID: <<1234@example.com>>\r
2783\r
2784Hello";
2785        let msg = new_msg_body(DOUBLE_ANGLE_ONLY);
2786        k9::snapshot!(
2787            msg.check_fix_conformance(
2788                MessageConformance::MISSING_MESSAGE_ID_HEADER,
2789                MessageConformance::empty(),
2790                None,
2791            )
2792            .await
2793            .unwrap_err()
2794            .to_string(),
2795            "Message has conformance issues: MISSING_MESSAGE_ID_HEADER"
2796        );
2797
2798        msg.check_fix_conformance(
2799            MessageConformance::MISSING_MESSAGE_ID_HEADER,
2800            MessageConformance::MISSING_MESSAGE_ID_HEADER,
2801            None,
2802        )
2803        .await
2804        .unwrap();
2805
2806        // Can't use a snapshot test here because the fixed header
2807        // has a unique random component
2808        /*
2809                k9::snapshot!(
2810                    data_as_string(&msg),
2811                    r#"
2812        Subject: hello\r
2813        Message-ID: <4106566d2ce911ef9dcd0242289ea0df@example.com>\r
2814        \r
2815        Hello
2816        "#
2817                );
2818        */
2819
2820        const DOUBLE_ANGLE_AND_LONG_LINE: &str = "Subject: hello\r
2821Message-ID: <<1234@example.com>>\r
2822\r
2823Hello this is a really long line Hello this is a really long line \
2824Hello this is a really long line Hello this is a really long line \
2825Hello this is a really long line Hello this is a really long line \
2826Hello this is a really long line Hello this is a really long line \
2827Hello this is a really long line Hello this is a really long line \
2828Hello this is a really long line Hello this is a really long line \
2829Hello this is a really long line Hello this is a really long line
2830";
2831        let msg = new_msg_body(DOUBLE_ANGLE_AND_LONG_LINE);
2832        msg.check_fix_conformance(
2833            MessageConformance::MISSING_COLON_VALUE,
2834            MessageConformance::MISSING_MESSAGE_ID_HEADER | MessageConformance::LINE_TOO_LONG,
2835            None,
2836        )
2837        .await
2838        .unwrap();
2839
2840        // Can't use a snapshot test here because the fixed header
2841        // has a random component
2842        /*
2843                k9::snapshot!(
2844                    data_as_string(&msg),
2845                    r#"
2846        Content-Type: text/plain;\r
2847        \tcharset="us-ascii"\r
2848        Content-Transfer-Encoding: quoted-printable\r
2849        Subject: hello\r
2850        Message-ID: <749fc87e2cea11ef96a50242289ea0df@example.com>\r
2851        \r
2852        Hello this is a really long line Hello this is a really long line Hello thi=\r
2853        s is a really long line Hello this is a really long line Hello this is a re=\r
2854        ally long line Hello this is a really long line Hello this is a really long=\r
2855         line Hello this is a really long line Hello this is a really long line Hel=\r
2856        lo this is a really long line Hello this is a really long line Hello this i=\r
2857        s a really long line Hello this is a really long line Hello this is a reall=\r
2858        y long line=0A\r
2859
2860        "#
2861                );
2862        */
2863    }
2864
2865    #[tokio::test]
2866    async fn check_conformance() {
2867        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2868        msg.check_fix_conformance(
2869            MessageConformance::default(),
2870            MessageConformance::MISSING_MIME_VERSION,
2871            None,
2872        )
2873        .await
2874        .unwrap();
2875        k9::snapshot!(
2876            data_as_string(&msg),
2877            r#"
2878X-Hello: there\r
2879X-Header: value\r
2880Subject: Hello\r
2881X-Header: another value\r
2882From :Someone@somewhere\r
2883MIME-Version: 1.0\r
2884\r
2885Body
2886"#
2887        );
2888
2889        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2890        msg.check_fix_conformance(
2891            MessageConformance::default(),
2892            MessageConformance::MISSING_MIME_VERSION | MessageConformance::NAME_ENDS_WITH_SPACE,
2893            None,
2894        )
2895        .await
2896        .unwrap();
2897        k9::snapshot!(
2898            data_as_string(&msg),
2899            r#"
2900Content-Type: text/plain;\r
2901\tcharset="us-ascii"\r
2902X-Hello: there\r
2903X-Header: value\r
2904Subject: Hello\r
2905X-Header: another value\r
2906From: <Someone@somewhere>\r
2907MIME-Version: 1.0\r
2908\r
2909Body\r
2910
2911"#
2912        );
2913    }
2914
2915    #[tokio::test]
2916    async fn check_fix_latin_input() {
2917        const POUNDS: &[u8] = b"Subject: \xa3\r\n\r\nGBP\r\n";
2918        let msg = new_msg_body(&*POUNDS);
2919        msg.check_fix_conformance(
2920            MessageConformance::default(),
2921            MessageConformance::NEEDS_TRANSFER_ENCODING,
2922            Some(&CheckFixSettings {
2923                detect_encoding: true,
2924                include_encodings: vec!["iso-8859-1".to_string()],
2925                ..Default::default()
2926            }),
2927        )
2928        .await
2929        .unwrap();
2930
2931        let subject = msg
2932            .get_first_named_header_value("subject")
2933            .await
2934            .unwrap()
2935            .unwrap();
2936        assert_eq!(subject, "£");
2937    }
2938
2939    #[tokio::test]
2940    async fn set_scheduling() -> anyhow::Result<()> {
2941        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2942        assert!(msg.get_due().is_none(), "due is implicitly now");
2943
2944        let now = Utc::now();
2945        let one_day = chrono::Duration::try_days(1).expect("1 day to be valid");
2946
2947        msg.set_scheduling(Some(Scheduling {
2948            restriction: None,
2949            first_attempt: Some((now + one_day).into()),
2950            expires: None,
2951        }))
2952        .await?;
2953
2954        let due = msg.get_due().expect("due to now be set");
2955        assert!(due - now >= one_day, "due time is at least 1 day away");
2956
2957        Ok(())
2958    }
2959
2960    #[tokio::test]
2961    async fn delay_by_out_of_range_errors() {
2962        let msg = new_msg_body(MULTI_HEADER_CONTENT);
2963        let max = chrono::Duration::MAX;
2964        let err = msg.delay_by(max).await.unwrap_err();
2965        k9::assert_equal!(
2966            err.to_string(),
2967            "due time out of range: now + PT9223372036854775.807S"
2968        );
2969        // The jitter addition must not panic either. Jitter is 0 to 59 random
2970        // seconds. At 0 the duration stays at Duration::MAX and the overflow
2971        // instead comes from adding it to now, giving the other message.
2972        let err = msg.delay_by_and_jitter(max).await.unwrap_err().to_string();
2973        assert!(
2974            err == "due time out of range: duration + jitter"
2975                || err == "due time out of range: now + PT9223372036854775.807S",
2976            "unexpected error: {err}"
2977        );
2978        k9::assert_equal!(msg.get_due(), None);
2979    }
2980
2981    #[cfg(all(test, target_pointer_width = "64"))]
2982    #[test]
2983    fn sizes() {
2984        assert_eq!(std::mem::size_of::<Message>(), 8);
2985        assert_eq!(std::mem::size_of::<MessageInner>(), 32);
2986        assert_eq!(std::mem::size_of::<MessageWithId>(), 72);
2987    }
2988}