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 const META_DIRTY = 1;
48 const DATA_DIRTY = 2;
50 const SCHEDULED = 4;
52 const FORCE_SYNC = 8;
54 }
55}
56
57declare_metric! {
58static MESSAGE_COUNT: IntGauge("message_count");
65}
66
67declare_metric! {
68static META_COUNT: IntGauge("message_meta_resident_count");
75}
76
77declare_metric! {
78static 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! {
93static SAVE_HIST: Histogram("message_save_latency");
103}
104
105declare_metric! {
106static LOAD_DATA_HIST: Histogram("message_data_load_latency");
117}
118
119declare_metric! {
120static LOAD_META_HIST: Histogram("message_meta_load_latency");
126}
127
128#[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#[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#[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#[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#[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 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 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 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 resident.value = data;
272 } else {
273 self.data = Some(ResidentData::new(data));
274 }
275 }
276
277 fn set_metadata(&mut self, metadata: Box<MetaData>) {
279 if let Some(resident) = &mut self.metadata {
280 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
301pub 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 pub fn new() -> Self {
319 Self {
320 list: LinkedList::new(MessageWithIdAdapter::default()),
321 len: 0,
322 }
323 }
324
325 pub fn len(&self) -> usize {
327 self.len
328 }
329
330 pub fn is_empty(&self) -> bool {
331 self.len == 0
332 }
333
334 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 pub fn push_back(&mut self, message: Message) {
347 self.list.push_back(message.msg_and_id);
348 self.len += 1;
349 }
350
351 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 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 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 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 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 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 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 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 let (data_res, meta_res) = tokio::join!(data_fut, meta_fut);
761
762 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 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 pub async fn save_and_shrink(&self) -> anyhow::Result<bool> {
823 self.save(None).await?;
824 self.shrink()
825 }
826
827 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 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 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 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 Exact(String),
1604 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 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 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 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 {
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 {
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 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 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 }
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 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}