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