Skip to main content

use_email_message/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::fmt;
5use std::error::Error;
6
7use use_email_header::{HeaderField, HeaderParseError};
8
9/// Error returned by message builders.
10#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum MessageBuildError {
12    /// The message body was not supplied.
13    MissingBody,
14}
15
16impl fmt::Display for MessageBuildError {
17    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            Self::MissingBody => formatter.write_str("email message body is required"),
20        }
21    }
22}
23
24impl Error for MessageBuildError {}
25
26/// Message kind metadata.
27#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
28pub enum MessageKind {
29    /// Plain text body.
30    #[default]
31    PlainText,
32    /// HTML body.
33    Html,
34    /// Multipart message metadata.
35    Multipart,
36    /// Raw message metadata.
37    Raw,
38}
39
40impl MessageKind {
41    /// Returns the default content type for the kind when one is known.
42    #[must_use]
43    pub const fn default_content_type(self) -> Option<&'static str> {
44        match self {
45            Self::PlainText => Some("text/plain; charset=utf-8"),
46            Self::Html => Some("text/html; charset=utf-8"),
47            Self::Multipart | Self::Raw => None,
48        }
49    }
50}
51
52/// Message body text.
53#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct MessageBody(String);
55
56impl MessageBody {
57    /// Creates a message body.
58    #[must_use]
59    pub fn new(value: impl Into<String>) -> Self {
60        Self(value.into())
61    }
62
63    /// Returns the body text.
64    #[must_use]
65    pub fn as_str(&self) -> &str {
66        &self.0
67    }
68}
69
70impl fmt::Display for MessageBody {
71    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
72        formatter.write_str(self.as_str())
73    }
74}
75
76/// Header collection for a message.
77#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub struct MessageHeaders {
79    fields: Vec<HeaderField>,
80}
81
82impl MessageHeaders {
83    /// Creates an empty message-header collection.
84    #[must_use]
85    pub const fn new() -> Self {
86        Self { fields: Vec::new() }
87    }
88
89    /// Adds a header field and returns the updated collection.
90    #[must_use]
91    pub fn with_field(mut self, field: HeaderField) -> Self {
92        self.fields.push(field);
93        self
94    }
95
96    /// Appends a field.
97    pub fn push(&mut self, field: HeaderField) {
98        self.fields.push(field);
99    }
100
101    /// Returns fields.
102    #[must_use]
103    pub fn fields(&self) -> &[HeaderField] {
104        &self.fields
105    }
106
107    /// Finds the first header value by case-insensitive name.
108    #[must_use]
109    pub fn first_value(&self, name: &str) -> Option<&str> {
110        self.fields
111            .iter()
112            .find(|field| field.name().as_str().eq_ignore_ascii_case(name))
113            .map(|field| field.value().as_str())
114    }
115}
116
117impl fmt::Display for MessageHeaders {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        for (index, field) in self.fields.iter().enumerate() {
120            if index > 0 {
121                formatter.write_str("\r\n")?;
122            }
123            write!(formatter, "{field}")?;
124        }
125        Ok(())
126    }
127}
128
129/// Structured email message primitive.
130#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct EmailMessage {
132    headers: MessageHeaders,
133    body: MessageBody,
134    kind: MessageKind,
135}
136
137impl EmailMessage {
138    /// Creates a plain text message with subject and body.
139    #[must_use]
140    pub fn plain_text(subject: impl Into<String>, body: impl Into<String>) -> Self {
141        Self::with_kind_subject_body(MessageKind::PlainText, subject, body)
142    }
143
144    /// Creates an HTML message with subject and body.
145    #[must_use]
146    pub fn html(subject: impl Into<String>, body: impl Into<String>) -> Self {
147        Self::with_kind_subject_body(MessageKind::Html, subject, body)
148    }
149
150    /// Creates a message from parts.
151    #[must_use]
152    pub const fn new(headers: MessageHeaders, body: MessageBody, kind: MessageKind) -> Self {
153        Self {
154            headers,
155            body,
156            kind,
157        }
158    }
159
160    /// Adds a header and returns the updated message.
161    pub fn with_header(
162        mut self,
163        name: impl AsRef<str>,
164        value: impl AsRef<str>,
165    ) -> Result<Self, HeaderParseError> {
166        self.headers.push(HeaderField::new(name, value)?);
167        Ok(self)
168    }
169
170    /// Returns message headers.
171    #[must_use]
172    pub const fn headers(&self) -> &MessageHeaders {
173        &self.headers
174    }
175
176    /// Returns the message body.
177    #[must_use]
178    pub const fn body(&self) -> &MessageBody {
179        &self.body
180    }
181
182    /// Returns the message kind.
183    #[must_use]
184    pub const fn kind(&self) -> MessageKind {
185        self.kind
186    }
187
188    /// Returns the Subject header, when present.
189    #[must_use]
190    pub fn subject(&self) -> Option<&str> {
191        self.headers.first_value("Subject")
192    }
193
194    /// Parses the Content-Type header using the shared `use-mime` primitive.
195    #[must_use]
196    pub fn body_mime(&self) -> Option<use_mime::MimeType> {
197        self.headers
198            .first_value("Content-Type")
199            .and_then(use_mime::parse_mime)
200    }
201
202    fn with_kind_subject_body(
203        kind: MessageKind,
204        subject: impl Into<String>,
205        body: impl Into<String>,
206    ) -> Self {
207        let mut headers = MessageHeaders::new().with_field(
208            HeaderField::new("Subject", subject.into()).expect("Subject header is valid"),
209        );
210        if let Some(content_type) = kind.default_content_type() {
211            headers.push(content_type_field(content_type));
212        }
213        Self {
214            headers,
215            body: MessageBody::new(body),
216            kind,
217        }
218    }
219}
220
221/// Raw message text container.
222#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
223pub struct RawMessage(String);
224
225impl RawMessage {
226    /// Creates raw message text.
227    #[must_use]
228    pub fn new(value: impl Into<String>) -> Self {
229        Self(value.into())
230    }
231
232    /// Returns raw message text.
233    #[must_use]
234    pub fn as_str(&self) -> &str {
235        &self.0
236    }
237}
238
239/// Parsed message wrapper.
240#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct ParsedMessage(EmailMessage);
242
243impl ParsedMessage {
244    /// Creates parsed message metadata from a structured message.
245    #[must_use]
246    pub const fn new(message: EmailMessage) -> Self {
247        Self(message)
248    }
249
250    /// Returns the structured message.
251    #[must_use]
252    pub const fn message(&self) -> &EmailMessage {
253        &self.0
254    }
255}
256
257/// Builder for simple structured messages.
258#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct MessageBuilder {
260    kind: MessageKind,
261    headers: MessageHeaders,
262    body: Option<MessageBody>,
263}
264
265impl MessageBuilder {
266    /// Creates a builder for the requested message kind.
267    #[must_use]
268    pub const fn new(kind: MessageKind) -> Self {
269        Self {
270            kind,
271            headers: MessageHeaders::new(),
272            body: None,
273        }
274    }
275
276    /// Adds a Subject header.
277    pub fn subject(self, value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
278        self.header("Subject", value)
279    }
280
281    /// Adds a header field.
282    pub fn header(
283        mut self,
284        name: impl AsRef<str>,
285        value: impl AsRef<str>,
286    ) -> Result<Self, HeaderParseError> {
287        self.headers.push(HeaderField::new(name, value)?);
288        Ok(self)
289    }
290
291    /// Sets the body.
292    #[must_use]
293    pub fn body(mut self, value: impl Into<String>) -> Self {
294        self.body = Some(MessageBody::new(value));
295        self
296    }
297
298    /// Builds the message.
299    pub fn build(mut self) -> Result<EmailMessage, MessageBuildError> {
300        if self.headers.first_value("Content-Type").is_none()
301            && let Some(content_type) = self.kind.default_content_type()
302        {
303            self.headers.push(content_type_field(content_type));
304        }
305        Ok(EmailMessage::new(
306            self.headers,
307            self.body.ok_or(MessageBuildError::MissingBody)?,
308            self.kind,
309        ))
310    }
311}
312
313fn content_type_field(content_type: &str) -> HeaderField {
314    HeaderField::new("Content-Type", content_type).expect("Content-Type header is valid")
315}
316
317#[cfg(test)]
318mod tests {
319    use super::{EmailMessage, MessageBuildError, MessageBuilder, MessageKind};
320
321    #[test]
322    fn creates_plain_text_and_html_messages() {
323        let plain = EmailMessage::plain_text("Hello", "A short note.");
324        let html = EmailMessage::html("Hello", "<p>A short note.</p>");
325
326        assert_eq!(plain.subject(), Some("Hello"));
327        assert_eq!(plain.kind(), MessageKind::PlainText);
328        assert_eq!(plain.body_mime().expect("mime").subtype, "plain");
329        assert_eq!(html.body_mime().expect("mime").subtype, "html");
330    }
331
332    #[test]
333    fn builds_messages_with_headers() -> Result<(), Box<dyn std::error::Error>> {
334        let message = MessageBuilder::new(MessageKind::Html)
335            .subject("Hello")?
336            .header("From", "jane@example.com")?
337            .body("<p>Hello</p>")
338            .build()?;
339
340        assert_eq!(message.subject(), Some("Hello"));
341        assert_eq!(
342            message.headers().first_value("from"),
343            Some("jane@example.com")
344        );
345        Ok(())
346    }
347
348    #[test]
349    fn builder_requires_body() {
350        assert_eq!(
351            MessageBuilder::new(MessageKind::PlainText).build(),
352            Err(MessageBuildError::MissingBody)
353        );
354    }
355}