1use crate::mime_types::ContentType;
8#[cfg(feature = "mime")]
9use crate::mime_types::MimePart;
10use crate::{Address, EmailAddress, Mailbox, MessageId};
11use time::OffsetDateTime;
12
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
16#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
17#[derive(Clone, Debug, PartialEq, Eq, Hash)]
18pub struct Envelope {
19 #[cfg_attr(
20 feature = "serde",
21 serde(default, skip_serializing_if = "Option::is_none")
22 )]
23 mail_from: Option<EmailAddress>,
24 #[cfg_attr(
25 feature = "serde",
26 serde(default, skip_serializing_if = "Vec::is_empty")
27 )]
28 rcpt_to: Vec<EmailAddress>,
29}
30
31impl Envelope {
32 #[must_use]
34 pub const fn new(mail_from: Option<EmailAddress>, rcpt_to: Vec<EmailAddress>) -> Self {
35 Self { mail_from, rcpt_to }
36 }
37
38 #[must_use]
40 pub const fn mail_from(&self) -> Option<&EmailAddress> {
41 self.mail_from.as_ref()
42 }
43
44 #[must_use]
46 pub fn rcpt_to(&self) -> &[EmailAddress] {
47 self.rcpt_to.as_slice()
48 }
49}
50
51#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
53#[derive(Clone, Debug, PartialEq, Eq)]
54#[non_exhaustive]
55pub struct Header {
56 name: String,
57 value: String,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
62#[non_exhaustive]
63pub enum HeaderValidationError {
64 #[error("header name cannot be empty")]
66 EmptyName,
67 #[error("header name `{name}` is invalid")]
69 InvalidName {
70 name: String,
72 },
73 #[error("header `{name}` contains raw newline characters")]
75 ValueContainsRawNewline {
76 name: String,
78 },
79 #[error("header `{name}` contains invalid control characters")]
81 ValueContainsControlCharacter {
82 name: String,
84 },
85}
86
87impl Header {
88 #[must_use]
90 pub fn name(&self) -> &str {
91 &self.name
92 }
93
94 #[must_use]
96 pub fn value(&self) -> &str {
97 &self.value
98 }
99
100 pub fn new(
122 name: impl Into<String>,
123 value: impl Into<String>,
124 ) -> Result<Self, HeaderValidationError> {
125 let name = name.into();
126 let value = value.into();
127 validate_header(&name, &value)?;
128 Ok(Self { name, value })
129 }
130}
131
132fn validate_header(name: &str, value: &str) -> Result<(), HeaderValidationError> {
133 if name.is_empty() {
134 return Err(HeaderValidationError::EmptyName);
135 }
136 if !name.bytes().all(is_header_name_byte) {
137 return Err(HeaderValidationError::InvalidName {
138 name: name.to_owned(),
139 });
140 }
141 if value.contains(['\r', '\n']) {
142 return Err(HeaderValidationError::ValueContainsRawNewline {
143 name: name.to_owned(),
144 });
145 }
146 if value
147 .bytes()
148 .any(|byte| byte.is_ascii_control() && byte != b'\t')
149 {
150 return Err(HeaderValidationError::ValueContainsControlCharacter {
151 name: name.to_owned(),
152 });
153 }
154 Ok(())
155}
156
157const fn is_header_name_byte(byte: u8) -> bool {
158 matches!(byte, b'!'..=b'9' | b';'..=b'~')
159}
160
161#[cfg(feature = "serde")]
162impl serde::Serialize for Header {
163 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
164 where
165 S: serde::Serializer,
166 {
167 use serde::ser::SerializeStruct;
168
169 let mut value = serializer.serialize_struct("Header", 2)?;
170 value.serialize_field("name", self.name())?;
171 value.serialize_field("value", self.value())?;
172 value.end()
173 }
174}
175
176#[cfg(feature = "serde")]
177impl<'de> serde::Deserialize<'de> for Header {
178 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
179 where
180 D: serde::Deserializer<'de>,
181 {
182 #[derive(serde::Deserialize)]
183 struct RawHeader {
184 name: String,
185 value: String,
186 }
187
188 let raw = RawHeader::deserialize(deserializer)?;
189 Self::new(raw.name, raw.value).map_err(serde::de::Error::custom)
190 }
191}
192
193#[cfg(feature = "arbitrary")]
194impl<'a> arbitrary::Arbitrary<'a> for Header {
195 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
196 let suffix = u32::arbitrary(u)?;
197 let value = u32::arbitrary(u)?;
198 Self::new(format!("X-Arbitrary-{suffix}"), value.to_string())
199 .map_err(|_| arbitrary::Error::IncorrectFormat)
200 }
201}
202
203#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
210#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
211#[derive(Clone, Debug, PartialEq, Eq, Hash)]
212#[non_exhaustive]
213pub struct AttachmentReference {
214 reference: String,
215}
216
217impl AttachmentReference {
218 #[must_use]
220 pub fn new(reference: impl Into<String>) -> Self {
221 Self {
222 reference: reference.into(),
223 }
224 }
225
226 #[must_use]
228 pub fn as_str(&self) -> &str {
229 self.reference.as_str()
230 }
231}
232
233impl From<String> for AttachmentReference {
234 fn from(reference: String) -> Self {
235 Self { reference }
236 }
237}
238
239impl From<&str> for AttachmentReference {
240 fn from(reference: &str) -> Self {
241 Self::from(reference.to_owned())
242 }
243}
244
245impl AsRef<str> for AttachmentReference {
246 fn as_ref(&self) -> &str {
247 self.as_str()
248 }
249}
250
251#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
253#[derive(Clone, Debug, PartialEq, Eq)]
254#[non_exhaustive]
255pub enum AttachmentBody {
256 Bytes(Vec<u8>),
258 Reference(AttachmentReference),
260}
261
262#[cfg(feature = "serde")]
263impl serde::Serialize for AttachmentBody {
264 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
265 where
266 S: serde::Serializer,
267 {
268 use base64::Engine as _;
269 use serde::ser::SerializeStruct as _;
270
271 match self {
272 Self::Bytes(bytes) => {
273 let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
274 let mut value = serializer.serialize_struct("AttachmentBody", 2)?;
275 value.serialize_field("type", "bytes")?;
276 value.serialize_field("bytes", &encoded)?;
277 value.end()
278 }
279 Self::Reference(reference) => {
280 let mut value = serializer.serialize_struct("AttachmentBody", 2)?;
281 value.serialize_field("type", "reference")?;
282 value.serialize_field("reference", reference.as_str())?;
283 value.end()
284 }
285 }
286 }
287}
288
289#[cfg(feature = "serde")]
290impl<'de> serde::Deserialize<'de> for AttachmentBody {
291 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
292 where
293 D: serde::Deserializer<'de>,
294 {
295 use base64::Engine as _;
296
297 #[derive(serde::Deserialize)]
298 #[serde(tag = "type", rename_all = "snake_case")]
299 enum RawAttachmentBody {
300 Bytes { bytes: String },
301 Reference { reference: String },
302 }
303
304 Ok(match RawAttachmentBody::deserialize(deserializer)? {
305 RawAttachmentBody::Bytes { bytes } => {
306 let decoded = base64::engine::general_purpose::STANDARD
307 .decode(bytes.as_bytes())
308 .map_err(|err| {
309 serde::de::Error::custom(format!("invalid base64 attachment bytes: {err}"))
310 })?;
311 Self::Bytes(decoded)
312 }
313 RawAttachmentBody::Reference { reference } => {
314 Self::Reference(AttachmentReference::new(reference))
315 }
316 })
317 }
318}
319
320#[cfg(feature = "schemars")]
321impl schemars::JsonSchema for AttachmentBody {
322 fn schema_name() -> std::borrow::Cow<'static, str> {
323 "AttachmentBody".into()
324 }
325
326 fn schema_id() -> std::borrow::Cow<'static, str> {
327 concat!(module_path!(), "::AttachmentBody").into()
328 }
329
330 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
331 schemars::json_schema!({
332 "oneOf": [
333 {
334 "type": "object",
335 "properties": {
336 "type": {"const": "bytes"},
337 "bytes": {
338 "type": "string",
339 "contentEncoding": "base64",
340 "description": "Base64-encoded attachment bytes (RFC 4648, with padding)"
341 }
342 },
343 "required": ["type", "bytes"]
344 },
345 {
346 "type": "object",
347 "properties": {
348 "type": {"const": "reference"},
349 "reference": {"type": "string"}
350 },
351 "required": ["type", "reference"]
352 }
353 ]
354 })
355 }
356}
357
358#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
360#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
361#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
362#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
363#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
364#[non_exhaustive]
365pub enum Disposition {
366 #[default]
368 Attachment,
369 Inline,
371}
372
373impl Disposition {
374 #[must_use]
376 pub const fn is_inline(&self) -> bool {
377 matches!(self, Self::Inline)
378 }
379}
380
381#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
383#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
384#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
385#[derive(Clone, Debug, PartialEq, Eq)]
386#[non_exhaustive]
387pub struct Attachment {
388 #[cfg_attr(
389 feature = "serde",
390 serde(default, skip_serializing_if = "Option::is_none")
391 )]
392 filename: Option<String>,
393 #[cfg_attr(
394 feature = "schemars",
395 schemars(with = "String", description = "MIME content type")
396 )]
397 content_type: ContentType,
398 #[cfg_attr(
399 feature = "serde",
400 serde(default, skip_serializing_if = "Option::is_none")
401 )]
402 content_id: Option<String>,
403 #[cfg_attr(
407 feature = "serde",
408 serde(
409 default,
410 alias = "inline",
411 deserialize_with = "deserialize_disposition_compat"
412 )
413 )]
414 #[cfg_attr(feature = "schemars", schemars(default))]
415 disposition: Disposition,
416 body: AttachmentBody,
417}
418
419#[cfg(feature = "serde")]
420fn deserialize_disposition_compat<'de, D>(deserializer: D) -> Result<Disposition, D::Error>
421where
422 D: serde::Deserializer<'de>,
423{
424 use serde::Deserialize as _;
425
426 #[derive(serde::Deserialize)]
427 #[serde(untagged)]
428 enum Compat {
429 Bool(bool),
430 Tag(Disposition),
431 }
432 Ok(match Compat::deserialize(deserializer)? {
433 Compat::Bool(true) => Disposition::Inline,
434 Compat::Bool(false) => Disposition::Attachment,
435 Compat::Tag(d) => d,
436 })
437}
438
439impl Attachment {
440 #[must_use]
442 pub const fn new(content_type: ContentType, body: AttachmentBody) -> Self {
443 Self {
444 filename: None,
445 content_type,
446 content_id: None,
447 disposition: Disposition::Attachment,
448 body,
449 }
450 }
451
452 #[must_use]
454 pub fn bytes(content_type: ContentType, bytes: impl Into<Vec<u8>>) -> Self {
455 Self::new(content_type, AttachmentBody::Bytes(bytes.into()))
456 }
457
458 #[must_use]
460 pub const fn reference(content_type: ContentType, reference: AttachmentReference) -> Self {
461 Self::new(content_type, AttachmentBody::Reference(reference))
462 }
463
464 #[must_use]
466 pub fn filename(&self) -> Option<&str> {
467 self.filename.as_deref()
468 }
469
470 #[must_use]
472 pub const fn content_type(&self) -> &ContentType {
473 &self.content_type
474 }
475
476 #[must_use]
478 pub fn content_id(&self) -> Option<&str> {
479 self.content_id.as_deref()
480 }
481
482 #[must_use]
484 pub const fn disposition(&self) -> Disposition {
485 self.disposition
486 }
487
488 #[must_use]
490 pub const fn is_inline(&self) -> bool {
491 self.disposition.is_inline()
492 }
493
494 #[must_use]
496 pub const fn body(&self) -> &AttachmentBody {
497 &self.body
498 }
499
500 pub fn set_body(&mut self, body: AttachmentBody) {
502 self.body = body;
503 }
504
505 #[must_use]
507 pub fn with_body(mut self, body: AttachmentBody) -> Self {
508 self.body = body;
509 self
510 }
511
512 #[must_use]
514 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
515 self.filename = Some(filename.into());
516 self
517 }
518
519 #[must_use]
521 pub fn with_content_id(mut self, content_id: impl Into<String>) -> Self {
522 self.content_id = Some(content_id.into());
523 self
524 }
525
526 #[must_use]
528 pub const fn with_disposition(mut self, disposition: Disposition) -> Self {
529 self.disposition = disposition;
530 self
531 }
532}
533
534#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
553#[derive(Clone, Debug, PartialEq, Eq)]
554#[non_exhaustive]
555pub enum Body {
556 Text(String),
558 Html(String),
560 TextAndHtml {
562 text: String,
564 html: String,
566 },
567 #[cfg(feature = "mime")]
569 Mime(MimePart),
570}
571
572#[cfg(feature = "serde")]
573impl serde::Serialize for Body {
574 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
575 where
576 S: serde::Serializer,
577 {
578 use serde::ser::SerializeStruct as _;
579
580 match self {
581 Self::Text(text) => {
582 let mut value = serializer.serialize_struct("Body", 2)?;
583 value.serialize_field("type", "text")?;
584 value.serialize_field("text", text)?;
585 value.end()
586 }
587 Self::Html(html) => {
588 let mut value = serializer.serialize_struct("Body", 2)?;
589 value.serialize_field("type", "html")?;
590 value.serialize_field("html", html)?;
591 value.end()
592 }
593 Self::TextAndHtml { text, html } => {
594 let mut value = serializer.serialize_struct("Body", 3)?;
595 value.serialize_field("type", "text_and_html")?;
596 value.serialize_field("text", text)?;
597 value.serialize_field("html", html)?;
598 value.end()
599 }
600 #[cfg(feature = "mime")]
601 Self::Mime(part) => {
602 let mut value = serializer.serialize_struct("Body", 2)?;
603 value.serialize_field("type", "mime")?;
604 value.serialize_field("part", part)?;
605 value.end()
606 }
607 }
608 }
609}
610
611#[cfg(feature = "serde")]
612impl<'de> serde::Deserialize<'de> for Body {
613 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
614 where
615 D: serde::Deserializer<'de>,
616 {
617 #[derive(serde::Deserialize)]
618 #[serde(tag = "type", rename_all = "snake_case")]
619 enum RawBody {
620 Text {
621 text: String,
622 },
623 Html {
624 html: String,
625 },
626 TextAndHtml {
627 text: String,
628 html: String,
629 },
630 #[cfg(feature = "mime")]
631 Mime {
632 part: MimePart,
633 },
634 }
635
636 Ok(match RawBody::deserialize(deserializer)? {
637 RawBody::Text { text } => Self::Text(text),
638 RawBody::Html { html } => Self::Html(html),
639 RawBody::TextAndHtml { text, html } => Self::TextAndHtml { text, html },
640 #[cfg(feature = "mime")]
641 RawBody::Mime { part } => Self::Mime(part),
642 })
643 }
644}
645
646#[cfg(feature = "schemars")]
647impl schemars::JsonSchema for Body {
648 fn schema_name() -> std::borrow::Cow<'static, str> {
649 "Body".into()
650 }
651
652 fn schema_id() -> std::borrow::Cow<'static, str> {
653 concat!(module_path!(), "::Body").into()
654 }
655
656 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
657 #[cfg(not(feature = "mime"))]
658 let _ = generator;
659
660 let variants = vec![
661 schemars::json_schema!({
662 "type": "object",
663 "properties": {
664 "type": {"const": "text"},
665 "text": {"type": "string"}
666 },
667 "required": ["type", "text"]
668 }),
669 schemars::json_schema!({
670 "type": "object",
671 "properties": {
672 "type": {"const": "html"},
673 "html": {"type": "string"}
674 },
675 "required": ["type", "html"]
676 }),
677 schemars::json_schema!({
678 "type": "object",
679 "properties": {
680 "type": {"const": "text_and_html"},
681 "text": {"type": "string"},
682 "html": {"type": "string"}
683 },
684 "required": ["type", "text", "html"]
685 }),
686 ];
687
688 #[cfg(feature = "mime")]
689 let variants = {
690 let mut variants = variants;
691 let part = generator.subschema_for::<MimePart>();
692 variants.push(schemars::json_schema!({
693 "type": "object",
694 "properties": {
695 "type": {"const": "mime"},
696 "part": part
697 },
698 "required": ["type", "part"]
699 }));
700 variants
701 };
702
703 schemars::json_schema!({"oneOf": variants})
704 }
705}
706
707impl Body {
708 #[must_use]
710 pub fn text(value: impl Into<String>) -> Self {
711 Self::Text(value.into())
712 }
713
714 #[must_use]
716 pub fn html(value: impl Into<String>) -> Self {
717 Self::Html(value.into())
718 }
719
720 #[must_use]
722 pub fn text_and_html(text: impl Into<String>, html: impl Into<String>) -> Self {
723 Self::TextAndHtml {
724 text: text.into(),
725 html: html.into(),
726 }
727 }
728}
729
730#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
751#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
752#[derive(Clone, Debug, PartialEq, Eq)]
753#[allow(clippy::struct_field_names)]
754#[non_exhaustive]
755pub struct Message {
756 #[cfg_attr(
757 feature = "serde",
758 serde(default, skip_serializing_if = "Option::is_none")
759 )]
760 from: Option<Mailbox>,
761 #[cfg_attr(
762 feature = "serde",
763 serde(default, skip_serializing_if = "Option::is_none")
764 )]
765 sender: Option<Mailbox>,
766 #[cfg_attr(
767 feature = "serde",
768 serde(default, skip_serializing_if = "Vec::is_empty")
769 )]
770 #[cfg_attr(feature = "schemars", schemars(default))]
771 to: Vec<Address>,
772 #[cfg_attr(
773 feature = "serde",
774 serde(default, skip_serializing_if = "Vec::is_empty")
775 )]
776 #[cfg_attr(feature = "schemars", schemars(default))]
777 cc: Vec<Address>,
778 #[cfg_attr(
779 feature = "serde",
780 serde(default, skip_serializing_if = "Vec::is_empty")
781 )]
782 #[cfg_attr(feature = "schemars", schemars(default))]
783 bcc: Vec<Address>,
784 #[cfg_attr(
785 feature = "serde",
786 serde(default, skip_serializing_if = "Vec::is_empty")
787 )]
788 #[cfg_attr(feature = "schemars", schemars(default))]
789 reply_to: Vec<Address>,
790 #[cfg_attr(
791 feature = "serde",
792 serde(default, skip_serializing_if = "Option::is_none")
793 )]
794 subject: Option<String>,
795 #[cfg_attr(
796 feature = "serde",
797 serde(default, skip_serializing_if = "Option::is_none")
798 )]
799 #[cfg_attr(
800 feature = "schemars",
801 schemars(with = "Option<String>", description = "RFC 2822 date-time")
802 )]
803 date: Option<OffsetDateTime>,
804 #[cfg_attr(
805 feature = "serde",
806 serde(default, skip_serializing_if = "Option::is_none")
807 )]
808 message_id: Option<MessageId>,
809 #[cfg_attr(
810 feature = "serde",
811 serde(default, skip_serializing_if = "Vec::is_empty")
812 )]
813 #[cfg_attr(feature = "schemars", schemars(default))]
814 headers: Vec<Header>,
815 body: Body,
816 #[cfg_attr(
817 feature = "serde",
818 serde(default, skip_serializing_if = "Vec::is_empty")
819 )]
820 #[cfg_attr(feature = "schemars", schemars(default))]
821 attachments: Vec<Attachment>,
822}
823
824#[derive(Clone, Debug, PartialEq, Eq)]
830#[non_exhaustive]
831pub struct OutboundMessage {
832 inner: Message,
834 from: Mailbox,
839}
840
841#[cfg(feature = "serde")]
842impl serde::Serialize for OutboundMessage {
843 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
844 where
845 S: serde::Serializer,
846 {
847 self.inner.serialize(serializer)
850 }
851}
852
853#[cfg(feature = "serde")]
854impl<'de> serde::Deserialize<'de> for OutboundMessage {
855 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
856 where
857 D: serde::Deserializer<'de>,
858 {
859 let message = Message::deserialize(deserializer)?;
860 Self::new(message).map_err(serde::de::Error::custom)
861 }
862}
863
864#[cfg(feature = "schemars")]
865impl schemars::JsonSchema for OutboundMessage {
866 fn schema_name() -> std::borrow::Cow<'static, str> {
867 <Message as schemars::JsonSchema>::schema_name()
868 }
869
870 fn schema_id() -> std::borrow::Cow<'static, str> {
871 <Message as schemars::JsonSchema>::schema_id()
872 }
873
874 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
875 <Message as schemars::JsonSchema>::json_schema(generator)
876 }
877}
878
879impl OutboundMessage {
880 pub fn new(message: Message) -> Result<Self, MessageValidationError> {
887 message.validate_basic()?;
888 let from = message
893 .from
894 .clone()
895 .ok_or(MessageValidationError::MissingFrom)?;
896 Ok(Self {
897 inner: message,
898 from,
899 })
900 }
901
902 #[must_use]
904 pub const fn as_message(&self) -> &Message {
905 &self.inner
906 }
907
908 #[must_use]
910 pub fn into_message(self) -> Message {
911 self.inner
912 }
913
914 #[must_use]
920 pub const fn from_mailbox(&self) -> &Mailbox {
921 &self.from
922 }
923}
924
925impl TryFrom<Message> for OutboundMessage {
926 type Error = MessageValidationError;
927
928 fn try_from(value: Message) -> Result<Self, Self::Error> {
929 Self::new(value)
930 }
931}
932
933impl From<OutboundMessage> for Message {
934 fn from(value: OutboundMessage) -> Self {
935 value.inner
936 }
937}
938
939#[cfg(feature = "arbitrary")]
940impl<'a> arbitrary::Arbitrary<'a> for Message {
941 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
942 let has_date = bool::arbitrary(u)?;
943 let date = if has_date {
944 let seconds = i64::arbitrary(u)?;
945 Some(OffsetDateTime::from_unix_timestamp(seconds).unwrap_or(OffsetDateTime::UNIX_EPOCH))
946 } else {
947 None
948 };
949
950 Ok(Self {
951 from: Option::<Mailbox>::arbitrary(u)?,
952 sender: Option::<Mailbox>::arbitrary(u)?,
953 to: Vec::<Address>::arbitrary(u)?,
954 cc: Vec::<Address>::arbitrary(u)?,
955 bcc: Vec::<Address>::arbitrary(u)?,
956 reply_to: Vec::<Address>::arbitrary(u)?,
957 subject: Option::<String>::arbitrary(u)?,
958 date,
959 message_id: Option::<MessageId>::arbitrary(u)?,
960 headers: Vec::<Header>::arbitrary(u)?,
961 body: Body::arbitrary(u)?,
962 attachments: Vec::<Attachment>::arbitrary(u)?,
963 })
964 }
965}
966
967#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
1001#[non_exhaustive]
1002pub enum MessageValidationError {
1003 #[error("missing From header")]
1005 MissingFrom,
1006 #[error("sender header cannot appear without from")]
1008 SenderWithoutFrom,
1009 #[error("no recipients in To/Cc/Bcc")]
1011 MissingRecipients,
1012 #[error(
1014 "custom header `{name}` collides with a structured field; use the typed setter (Subject, Date, Message-ID, From, ...) instead"
1015 )]
1016 #[non_exhaustive]
1017 ReservedHeaderName {
1018 name: String,
1020 },
1021 #[error("subject contains raw CR, LF, or non-tab control characters")]
1023 SubjectContainsInvalidChars,
1024 #[error(
1026 "mailbox display name in `{location}` contains raw CR, LF, NUL, or non-tab control characters"
1027 )]
1028 #[non_exhaustive]
1029 MailboxDisplayNameContainsInvalidChars {
1030 location: &'static str,
1032 },
1033 #[error(
1035 "attachment metadata field `{field}` contains raw CR, LF, NUL, or non-tab control characters"
1036 )]
1037 #[non_exhaustive]
1038 AttachmentMetadataContainsInvalidChars {
1039 field: &'static str,
1041 },
1042}
1043
1044fn contains_header_unsafe_chars(value: &str) -> bool {
1045 value
1046 .bytes()
1047 .any(|byte| byte == b'\r' || byte == b'\n' || (byte != b'\t' && byte.is_ascii_control()))
1048}
1049
1050fn validate_address_mailboxes(
1055 addresses: &[Address],
1056 location: &'static str,
1057) -> Result<(), MessageValidationError> {
1058 for address in addresses {
1059 match address {
1060 Address::Mailbox(mailbox) => {
1061 if let Some(name) = mailbox.name()
1062 && contains_header_unsafe_chars(name)
1063 {
1064 return Err(
1065 MessageValidationError::MailboxDisplayNameContainsInvalidChars { location },
1066 );
1067 }
1068 }
1069 Address::Group(group) => {
1070 if contains_header_unsafe_chars(group.name()) {
1071 return Err(
1072 MessageValidationError::MailboxDisplayNameContainsInvalidChars { location },
1073 );
1074 }
1075 for member in group.members() {
1076 if let Some(name) = member.name()
1077 && contains_header_unsafe_chars(name)
1078 {
1079 return Err(
1080 MessageValidationError::MailboxDisplayNameContainsInvalidChars {
1081 location,
1082 },
1083 );
1084 }
1085 }
1086 }
1087 }
1088 }
1089 Ok(())
1090}
1091
1092fn validate_mailbox_display_name(
1093 mailbox: &Mailbox,
1094 location: &'static str,
1095) -> Result<(), MessageValidationError> {
1096 if let Some(name) = mailbox.name()
1097 && contains_header_unsafe_chars(name)
1098 {
1099 return Err(MessageValidationError::MailboxDisplayNameContainsInvalidChars { location });
1100 }
1101 Ok(())
1102}
1103
1104const RESERVED_HEADER_NAMES: &[&str] = &[
1114 "from",
1115 "sender",
1116 "reply-to",
1117 "to",
1118 "cc",
1119 "bcc",
1120 "date",
1121 "subject",
1122 "message-id",
1123];
1124
1125fn is_reserved_header_name(name: &str) -> bool {
1126 RESERVED_HEADER_NAMES
1127 .iter()
1128 .any(|reserved| name.eq_ignore_ascii_case(reserved))
1129}
1130
1131impl Message {
1132 #[must_use]
1134 pub const fn new(from: Mailbox, to: Vec<Address>, body: Body) -> Self {
1135 Self {
1136 from: Some(from),
1137 sender: None,
1138 to,
1139 cc: Vec::new(),
1140 bcc: Vec::new(),
1141 reply_to: Vec::new(),
1142 subject: None,
1143 date: None,
1144 message_id: None,
1145 headers: Vec::new(),
1146 body,
1147 attachments: Vec::new(),
1148 }
1149 }
1150
1151 #[must_use]
1153 pub const fn builder(body: Body) -> MessageBuilder {
1154 MessageBuilder::new(body)
1155 }
1156
1157 #[must_use]
1163 pub const fn from_mailbox(&self) -> Option<&Mailbox> {
1164 self.from.as_ref()
1165 }
1166
1167 #[must_use]
1168 pub const fn sender(&self) -> Option<&Mailbox> {
1170 self.sender.as_ref()
1171 }
1172
1173 #[must_use]
1174 pub fn to(&self) -> &[Address] {
1176 self.to.as_slice()
1177 }
1178
1179 #[must_use]
1180 pub fn cc(&self) -> &[Address] {
1182 self.cc.as_slice()
1183 }
1184
1185 #[must_use]
1186 pub fn bcc(&self) -> &[Address] {
1188 self.bcc.as_slice()
1189 }
1190
1191 #[must_use]
1192 pub fn reply_to(&self) -> &[Address] {
1194 self.reply_to.as_slice()
1195 }
1196
1197 #[must_use]
1198 pub fn subject(&self) -> Option<&str> {
1200 self.subject.as_deref()
1201 }
1202
1203 #[must_use]
1204 pub const fn date(&self) -> Option<&OffsetDateTime> {
1206 self.date.as_ref()
1207 }
1208
1209 #[must_use]
1210 pub const fn message_id(&self) -> Option<&MessageId> {
1212 self.message_id.as_ref()
1213 }
1214
1215 #[must_use]
1216 pub fn headers(&self) -> &[Header] {
1218 self.headers.as_slice()
1219 }
1220
1221 #[must_use]
1222 pub const fn body(&self) -> &Body {
1224 &self.body
1225 }
1226
1227 #[must_use]
1228 pub fn attachments(&self) -> &[Attachment] {
1230 self.attachments.as_slice()
1231 }
1232
1233 #[must_use]
1234 pub fn with_attachments<I>(mut self, attachments: I) -> Self
1236 where
1237 I: IntoIterator<Item = Attachment>,
1238 {
1239 self.attachments = attachments.into_iter().collect();
1240 self
1241 }
1242
1243 #[must_use]
1245 pub fn into_attachments(mut self) -> (Self, Vec<Attachment>) {
1246 let attachments = std::mem::take(&mut self.attachments);
1247 (self, attachments)
1248 }
1249
1250 pub fn validate_basic(&self) -> Result<(), MessageValidationError> {
1275 if self.sender.is_some() && self.from.is_none() {
1276 return Err(MessageValidationError::SenderWithoutFrom);
1277 }
1278
1279 if self.from.is_none() {
1280 return Err(MessageValidationError::MissingFrom);
1281 }
1282
1283 if self.to.is_empty() && self.cc.is_empty() && self.bcc.is_empty() {
1284 return Err(MessageValidationError::MissingRecipients);
1285 }
1286
1287 if let Some(subject) = self.subject.as_deref()
1288 && contains_header_unsafe_chars(subject)
1289 {
1290 return Err(MessageValidationError::SubjectContainsInvalidChars);
1291 }
1292
1293 for header in &self.headers {
1294 if is_reserved_header_name(header.name()) {
1295 return Err(MessageValidationError::ReservedHeaderName {
1296 name: header.name().to_owned(),
1297 });
1298 }
1299 }
1300
1301 if let Some(from) = self.from.as_ref() {
1302 validate_mailbox_display_name(from, "from")?;
1303 }
1304 if let Some(sender) = self.sender.as_ref() {
1305 validate_mailbox_display_name(sender, "sender")?;
1306 }
1307 validate_address_mailboxes(&self.to, "to")?;
1308 validate_address_mailboxes(&self.cc, "cc")?;
1309 validate_address_mailboxes(&self.bcc, "bcc")?;
1310 validate_address_mailboxes(&self.reply_to, "reply-to")?;
1311
1312 for attachment in &self.attachments {
1313 if let Some(filename) = attachment.filename()
1314 && contains_header_unsafe_chars(filename)
1315 {
1316 return Err(
1317 MessageValidationError::AttachmentMetadataContainsInvalidChars {
1318 field: "filename",
1319 },
1320 );
1321 }
1322 if let Some(content_id) = attachment.content_id()
1323 && contains_header_unsafe_chars(content_id)
1324 {
1325 return Err(
1326 MessageValidationError::AttachmentMetadataContainsInvalidChars {
1327 field: "content-id",
1328 },
1329 );
1330 }
1331 }
1332
1333 Ok(())
1334 }
1335
1336 pub fn derive_envelope(&self) -> Result<Envelope, MessageValidationError> {
1343 self.validate_basic()?;
1344
1345 let mail_from = self
1346 .sender
1347 .as_ref()
1348 .or(self.from.as_ref())
1349 .map(|mailbox| mailbox.email().clone());
1350
1351 let mut rcpt_to = Vec::new();
1352 extend_recipient_emails(&mut rcpt_to, &self.to);
1353 extend_recipient_emails(&mut rcpt_to, &self.cc);
1354 extend_recipient_emails(&mut rcpt_to, &self.bcc);
1355
1356 Ok(Envelope::new(mail_from, rcpt_to))
1357 }
1358}
1359
1360fn extend_recipient_emails(out: &mut Vec<EmailAddress>, addresses: &[Address]) {
1361 for address in addresses {
1362 out.extend(address.mailboxes().map(|mailbox| mailbox.email().clone()));
1363 }
1364}
1365
1366#[derive(Clone, Debug, PartialEq, Eq)]
1368#[non_exhaustive]
1369pub struct MessageBuilder {
1370 message: Message,
1371}
1372
1373impl MessageBuilder {
1374 #[must_use]
1376 pub const fn new(body: Body) -> Self {
1377 Self {
1378 message: Message {
1379 from: None,
1380 sender: None,
1381 to: Vec::new(),
1382 cc: Vec::new(),
1383 bcc: Vec::new(),
1384 reply_to: Vec::new(),
1385 subject: None,
1386 date: None,
1387 message_id: None,
1388 headers: Vec::new(),
1389 body,
1390 attachments: Vec::new(),
1391 },
1392 }
1393 }
1394
1395 #[must_use]
1400 pub fn from_mailbox(mut self, from: Mailbox) -> Self {
1401 self.message.from = Some(from);
1402 self
1403 }
1404
1405 #[must_use]
1407 pub fn sender(mut self, sender: Mailbox) -> Self {
1408 self.message.sender = Some(sender);
1409 self
1410 }
1411
1412 #[must_use]
1415 pub fn to<I>(mut self, to: I) -> Self
1416 where
1417 I: IntoIterator<Item = Address>,
1418 {
1419 self.message.to = to.into_iter().collect();
1420 self
1421 }
1422
1423 #[must_use]
1425 pub fn add_to(mut self, to: impl Into<Address>) -> Self {
1426 self.message.to.push(to.into());
1427 self
1428 }
1429
1430 #[must_use]
1432 pub fn cc<I>(mut self, cc: I) -> Self
1433 where
1434 I: IntoIterator<Item = Address>,
1435 {
1436 self.message.cc = cc.into_iter().collect();
1437 self
1438 }
1439
1440 #[must_use]
1442 pub fn add_cc(mut self, cc: impl Into<Address>) -> Self {
1443 self.message.cc.push(cc.into());
1444 self
1445 }
1446
1447 #[must_use]
1449 pub fn bcc<I>(mut self, bcc: I) -> Self
1450 where
1451 I: IntoIterator<Item = Address>,
1452 {
1453 self.message.bcc = bcc.into_iter().collect();
1454 self
1455 }
1456
1457 #[must_use]
1459 pub fn add_bcc(mut self, bcc: impl Into<Address>) -> Self {
1460 self.message.bcc.push(bcc.into());
1461 self
1462 }
1463
1464 #[must_use]
1466 pub fn reply_to<I>(mut self, reply_to: I) -> Self
1467 where
1468 I: IntoIterator<Item = Address>,
1469 {
1470 self.message.reply_to = reply_to.into_iter().collect();
1471 self
1472 }
1473
1474 #[must_use]
1476 pub fn add_reply_to(mut self, reply_to: impl Into<Address>) -> Self {
1477 self.message.reply_to.push(reply_to.into());
1478 self
1479 }
1480
1481 #[must_use]
1483 pub fn subject(mut self, subject: impl Into<String>) -> Self {
1484 self.message.subject = Some(subject.into());
1485 self
1486 }
1487
1488 #[must_use]
1490 pub const fn date(mut self, date: OffsetDateTime) -> Self {
1491 self.message.date = Some(date);
1492 self
1493 }
1494
1495 #[must_use]
1497 pub fn message_id(mut self, message_id: MessageId) -> Self {
1498 self.message.message_id = Some(message_id);
1499 self
1500 }
1501
1502 #[must_use]
1504 pub fn headers<I>(mut self, headers: I) -> Self
1505 where
1506 I: IntoIterator<Item = Header>,
1507 {
1508 self.message.headers = headers.into_iter().collect();
1509 self
1510 }
1511
1512 #[must_use]
1514 pub fn add_header(mut self, header: Header) -> Self {
1515 self.message.headers.push(header);
1516 self
1517 }
1518
1519 #[must_use]
1521 pub fn attachments<I>(mut self, attachments: I) -> Self
1522 where
1523 I: IntoIterator<Item = Attachment>,
1524 {
1525 self.message.attachments = attachments.into_iter().collect();
1526 self
1527 }
1528
1529 #[must_use]
1531 pub fn add_attachment(mut self, attachment: Attachment) -> Self {
1532 self.message.attachments.push(attachment);
1533 self
1534 }
1535
1536 #[must_use]
1550 pub fn build_unchecked(self) -> Message {
1551 self.message
1552 }
1553
1554 pub fn build(self) -> Result<Message, MessageValidationError> {
1561 self.message.validate_basic()?;
1562 Ok(self.message)
1563 }
1564
1565 pub fn build_outbound(self) -> Result<OutboundMessage, MessageValidationError> {
1572 OutboundMessage::new(self.message)
1573 }
1574}
1575
1576#[cfg(test)]
1577mod tests {
1578 use super::*;
1579 use time::format_description::well_known::Rfc2822;
1580
1581 fn mailbox(input: &str) -> Mailbox {
1582 input.parse::<Mailbox>().expect("mailbox should parse")
1583 }
1584
1585 fn address(input: &str) -> Address {
1586 input.parse::<Address>().expect("address should parse")
1587 }
1588
1589 #[test]
1590 fn validate_basic_reports_sender_without_from() {
1591 let error = Message::builder(Body::text("body"))
1592 .sender(mailbox("sender@example.com"))
1593 .add_to(address("to@example.com"))
1594 .build()
1595 .expect_err("message should be invalid");
1596
1597 assert_eq!(error, MessageValidationError::SenderWithoutFrom);
1598 }
1599
1600 #[test]
1601 fn validate_basic_rejects_reserved_header_names() {
1602 let error = Message::builder(Body::text("body"))
1603 .from_mailbox(mailbox("from@example.com"))
1604 .add_to(address("to@example.com"))
1605 .add_header(Header::new("Subject", "shadow").expect("header should validate"))
1606 .build()
1607 .expect_err("reserved header should be rejected");
1608
1609 assert!(matches!(
1610 error,
1611 MessageValidationError::ReservedHeaderName { ref name, .. } if name == "Subject"
1612 ));
1613 }
1614
1615 #[test]
1616 fn validate_basic_rejects_reserved_header_case_insensitively() {
1617 let error = Message::builder(Body::text("body"))
1618 .from_mailbox(mailbox("from@example.com"))
1619 .add_to(address("to@example.com"))
1620 .add_header(Header::new("MESSAGE-ID", "<x@y>").expect("header should validate"))
1621 .build()
1622 .expect_err("reserved header should be rejected");
1623
1624 assert!(matches!(
1625 error,
1626 MessageValidationError::ReservedHeaderName { ref name, .. } if name == "MESSAGE-ID"
1627 ));
1628 }
1629
1630 #[test]
1631 fn validate_basic_rejects_subject_with_crlf_injection() {
1632 let error = Message::builder(Body::text("body"))
1633 .from_mailbox(mailbox("from@example.com"))
1634 .add_to(address("to@example.com"))
1635 .subject("hi\r\nBcc: victim@example.com")
1636 .build()
1637 .expect_err("subject CRLF injection should be rejected");
1638
1639 assert_eq!(error, MessageValidationError::SubjectContainsInvalidChars);
1640 }
1641
1642 #[test]
1643 fn validate_basic_rejects_subject_with_bare_lf() {
1644 let error = Message::builder(Body::text("body"))
1645 .from_mailbox(mailbox("from@example.com"))
1646 .add_to(address("to@example.com"))
1647 .subject("hi\nbcc")
1648 .build()
1649 .expect_err("subject bare LF should be rejected");
1650
1651 assert_eq!(error, MessageValidationError::SubjectContainsInvalidChars);
1652 }
1653
1654 #[test]
1655 fn validate_basic_rejects_subject_with_control_char() {
1656 let error = Message::builder(Body::text("body"))
1657 .from_mailbox(mailbox("from@example.com"))
1658 .add_to(address("to@example.com"))
1659 .subject("hi\x07boss")
1660 .build()
1661 .expect_err("subject control char should be rejected");
1662
1663 assert_eq!(error, MessageValidationError::SubjectContainsInvalidChars);
1664 }
1665
1666 #[test]
1667 fn validate_basic_rejects_from_mailbox_with_crlf_in_display_name() {
1668 let email = "alice@example.com"
1669 .parse::<EmailAddress>()
1670 .expect("email parses");
1671 let hostile_from = Mailbox::from(("evil\r\nBcc: attacker@example.com".to_string(), email));
1672
1673 let error = Message::builder(Body::text("body"))
1674 .from_mailbox(hostile_from)
1675 .add_to(address("to@example.com"))
1676 .build()
1677 .expect_err("hostile From display name should be rejected");
1678
1679 assert!(matches!(
1680 error,
1681 MessageValidationError::MailboxDisplayNameContainsInvalidChars { .. }
1682 ));
1683 }
1684
1685 #[test]
1686 fn validate_basic_rejects_to_mailbox_with_lf_in_display_name() {
1687 let email = "victim@example.com"
1688 .parse::<EmailAddress>()
1689 .expect("email parses");
1690 let hostile_to = Address::Mailbox(Mailbox::from(("name\ninjection".to_string(), email)));
1691
1692 let error = Message::builder(Body::text("body"))
1693 .from_mailbox(mailbox("from@example.com"))
1694 .add_to(hostile_to)
1695 .build()
1696 .expect_err("hostile To display name should be rejected");
1697
1698 assert!(matches!(
1699 error,
1700 MessageValidationError::MailboxDisplayNameContainsInvalidChars { .. }
1701 ));
1702 }
1703
1704 #[test]
1705 fn validate_basic_rejects_group_member_with_nul_in_display_name() {
1706 let email = "member@example.com"
1712 .parse::<EmailAddress>()
1713 .expect("email parses");
1714 let hostile_cc = Address::Mailbox(Mailbox::from(("embed\0nul".to_string(), email)));
1715
1716 let error = Message::builder(Body::text("body"))
1717 .from_mailbox(mailbox("from@example.com"))
1718 .add_cc(hostile_cc)
1719 .build()
1720 .expect_err("hostile Cc display name should be rejected");
1721
1722 assert!(matches!(
1723 error,
1724 MessageValidationError::MailboxDisplayNameContainsInvalidChars { .. }
1725 ));
1726 }
1727
1728 #[test]
1729 fn validate_basic_accepts_mailbox_with_tab_in_display_name() {
1730 let email = "alice@example.com"
1731 .parse::<EmailAddress>()
1732 .expect("email parses");
1733 let from = Mailbox::from(("Alice\tBob".to_string(), email));
1734
1735 Message::builder(Body::text("body"))
1736 .from_mailbox(from)
1737 .add_to(address("to@example.com"))
1738 .build()
1739 .expect("tab in display name should be accepted");
1740 }
1741
1742 #[test]
1743 fn validate_basic_accepts_subject_with_tab() {
1744 let message = Message::builder(Body::text("body"))
1745 .from_mailbox(mailbox("from@example.com"))
1746 .add_to(address("to@example.com"))
1747 .subject("hi\tworld")
1748 .build()
1749 .expect("subject with tab should be accepted");
1750
1751 assert_eq!(message.subject(), Some("hi\tworld"));
1752 }
1753
1754 #[test]
1755 fn outbound_message_from_mailbox_returns_validated_field() {
1756 let outbound = Message::builder(Body::text("body"))
1757 .from_mailbox(mailbox("alice@example.com"))
1758 .add_to(address("bob@example.com"))
1759 .build_outbound()
1760 .expect("message should validate");
1761
1762 assert_eq!(
1763 outbound.from_mailbox().email().as_str(),
1764 "alice@example.com"
1765 );
1766 }
1767
1768 #[cfg(feature = "serde")]
1769 #[test]
1770 fn outbound_message_serde_format_matches_message() {
1771 let outbound = Message::builder(Body::text("body"))
1772 .from_mailbox(mailbox("alice@example.com"))
1773 .add_to(address("bob@example.com"))
1774 .subject("hello")
1775 .build_outbound()
1776 .expect("message should validate");
1777
1778 let outbound_json =
1779 serde_json::to_string(&outbound).expect("OutboundMessage should serialize");
1780 let message_json =
1781 serde_json::to_string(outbound.as_message()).expect("Message should serialize");
1782 assert_eq!(
1783 outbound_json, message_json,
1784 "OutboundMessage serde representation must match its inner Message"
1785 );
1786
1787 let roundtripped: OutboundMessage =
1788 serde_json::from_str(&outbound_json).expect("OutboundMessage should deserialize");
1789 assert_eq!(roundtripped, outbound);
1790 }
1791
1792 #[cfg(feature = "serde")]
1793 #[test]
1794 fn outbound_message_deserialize_rejects_invalid_payload() {
1795 let invalid_message = Message {
1798 from: None,
1799 sender: None,
1800 to: vec![Address::Mailbox(mailbox("bob@example.com"))],
1801 cc: Vec::new(),
1802 bcc: Vec::new(),
1803 reply_to: Vec::new(),
1804 subject: None,
1805 date: None,
1806 message_id: None,
1807 headers: Vec::new(),
1808 body: Body::text("hi"),
1809 attachments: Vec::new(),
1810 };
1811 let json = serde_json::to_string(&invalid_message).expect("Message should serialize");
1812 assert!(serde_json::from_str::<OutboundMessage>(&json).is_err());
1813 }
1814
1815 #[cfg(feature = "serde")]
1816 #[test]
1817 fn outbound_message_deserialize_defaults_omitted_optional_lists() {
1818 let value = serde_json::json!({
1819 "from": {"type": "mailbox", "name": null, "email": "alice@example.com"},
1820 "to": [{"type": "mailbox", "name": null, "email": "bob@example.com"}],
1821 "body": {"type": "text", "text": "hi"}
1822 });
1823
1824 let outbound: OutboundMessage = serde_json::from_value(value)
1825 .expect("defaulted collection fields should be optional on the wire");
1826
1827 assert_eq!(outbound.as_message().to().len(), 1);
1828 assert!(outbound.as_message().cc().is_empty());
1829 assert!(outbound.as_message().bcc().is_empty());
1830 assert!(outbound.as_message().reply_to().is_empty());
1831 assert!(outbound.as_message().headers().is_empty());
1832 assert!(outbound.as_message().attachments().is_empty());
1833 }
1834
1835 #[test]
1836 fn builder_constructs_valid_message() {
1837 let date = OffsetDateTime::parse("Fri, 06 Mar 2026 12:00:00 +0000", &Rfc2822)
1838 .expect("date should parse");
1839 let message_id = "<test@example.com>"
1840 .parse::<MessageId>()
1841 .expect("message id should parse");
1842
1843 let message = Message::builder(Body::text("Hello"))
1844 .from_mailbox(mailbox("Mary Smith <mary@x.test>"))
1845 .add_to(address("jdoe@one.test"))
1846 .subject("Greeting")
1847 .date(date)
1848 .message_id(message_id.clone())
1849 .add_header(Header::new("X-Test", "demo").expect("header should validate"))
1850 .build()
1851 .expect("message should validate");
1852
1853 assert!(message.from_mailbox().is_some(), "from should be set");
1854 assert_eq!(message.to().len(), 1, "expected one recipient");
1855 assert_eq!(message.date(), Some(&date));
1856 assert_eq!(message.message_id(), Some(&message_id));
1857 assert_eq!(message.headers().len(), 1);
1858 }
1859
1860 #[test]
1861 fn derive_envelope_uses_sender_and_expands_groups() {
1862 let message = Message::builder(Body::text("Hello"))
1863 .from_mailbox(mailbox("from@example.com"))
1864 .sender(mailbox("sender@example.com"))
1865 .to(vec![address("Friends: a@example.com, b@example.com;")])
1866 .add_cc(address("c@example.com"))
1867 .build()
1868 .expect("message should validate");
1869
1870 let envelope = message.derive_envelope().expect("envelope should derive");
1871
1872 assert_eq!(
1873 envelope.mail_from().map(EmailAddress::as_str),
1874 Some("sender@example.com")
1875 );
1876 assert_eq!(
1877 envelope
1878 .rcpt_to()
1879 .iter()
1880 .map(EmailAddress::as_str)
1881 .collect::<Vec<_>>(),
1882 vec!["a@example.com", "b@example.com", "c@example.com"]
1883 );
1884 }
1885
1886 #[test]
1887 fn body_convenience_constructors_create_expected_variants() {
1888 assert_eq!(Body::text("hello"), Body::Text("hello".to_owned()));
1889 assert_eq!(
1890 Body::html("<p>hello</p>"),
1891 Body::Html("<p>hello</p>".to_owned())
1892 );
1893 assert_eq!(
1894 Body::text_and_html("hello", "<p>hello</p>"),
1895 Body::TextAndHtml {
1896 text: "hello".to_owned(),
1897 html: "<p>hello</p>".to_owned(),
1898 }
1899 );
1900 }
1901
1902 #[test]
1903 fn attachment_reference_constructor_preserves_reference() {
1904 let reference = AttachmentReference::new("550e8400-e29b-41d4-a716-446655440000");
1905
1906 assert_eq!(reference.as_str(), "550e8400-e29b-41d4-a716-446655440000");
1907 }
1908
1909 #[test]
1910 fn attachment_reference_converts_from_strings() {
1911 let from_str = AttachmentReference::from("s3://bucket/key");
1912 let from_string = AttachmentReference::from(String::from("s3://bucket/key"));
1913
1914 assert_eq!(from_str, from_string);
1915 assert_eq!(from_str, AttachmentReference::new("s3://bucket/key"));
1916 assert_eq!(from_str.as_ref(), "s3://bucket/key");
1917 }
1918
1919 #[test]
1920 fn with_attachments_replaces_existing_attachments() {
1921 let message = Message::builder(Body::text("Hello"))
1922 .from_mailbox(mailbox("from@example.com"))
1923 .add_to(address("to@example.com"))
1924 .add_attachment(
1925 Attachment::bytes(
1926 ContentType::try_from("text/plain").expect("content type should parse"),
1927 b"old".to_vec(),
1928 )
1929 .with_filename("old.txt"),
1930 )
1931 .build()
1932 .expect("message should validate");
1933
1934 let updated = message.clone().with_attachments(vec![
1935 Attachment::bytes(
1936 ContentType::try_from("text/plain").expect("content type should parse"),
1937 b"new".to_vec(),
1938 )
1939 .with_filename("new.txt"),
1940 ]);
1941
1942 assert_eq!(message.attachments().len(), 1);
1943 assert_eq!(updated.attachments().len(), 1);
1944 assert_eq!(updated.attachments()[0].filename(), Some("new.txt"));
1945 }
1946}