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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
11pub enum MessageBuildError {
12 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#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
28pub enum MessageKind {
29 #[default]
31 PlainText,
32 Html,
34 Multipart,
36 Raw,
38}
39
40impl MessageKind {
41 #[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#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
54pub struct MessageBody(String);
55
56impl MessageBody {
57 #[must_use]
59 pub fn new(value: impl Into<String>) -> Self {
60 Self(value.into())
61 }
62
63 #[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub struct MessageHeaders {
79 fields: Vec<HeaderField>,
80}
81
82impl MessageHeaders {
83 #[must_use]
85 pub const fn new() -> Self {
86 Self { fields: Vec::new() }
87 }
88
89 #[must_use]
91 pub fn with_field(mut self, field: HeaderField) -> Self {
92 self.fields.push(field);
93 self
94 }
95
96 pub fn push(&mut self, field: HeaderField) {
98 self.fields.push(field);
99 }
100
101 #[must_use]
103 pub fn fields(&self) -> &[HeaderField] {
104 &self.fields
105 }
106
107 #[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#[derive(Clone, Debug, Eq, PartialEq)]
131pub struct EmailMessage {
132 headers: MessageHeaders,
133 body: MessageBody,
134 kind: MessageKind,
135}
136
137impl EmailMessage {
138 #[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 #[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 #[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 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 #[must_use]
172 pub const fn headers(&self) -> &MessageHeaders {
173 &self.headers
174 }
175
176 #[must_use]
178 pub const fn body(&self) -> &MessageBody {
179 &self.body
180 }
181
182 #[must_use]
184 pub const fn kind(&self) -> MessageKind {
185 self.kind
186 }
187
188 #[must_use]
190 pub fn subject(&self) -> Option<&str> {
191 self.headers.first_value("Subject")
192 }
193
194 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
223pub struct RawMessage(String);
224
225impl RawMessage {
226 #[must_use]
228 pub fn new(value: impl Into<String>) -> Self {
229 Self(value.into())
230 }
231
232 #[must_use]
234 pub fn as_str(&self) -> &str {
235 &self.0
236 }
237}
238
239#[derive(Clone, Debug, Eq, PartialEq)]
241pub struct ParsedMessage(EmailMessage);
242
243impl ParsedMessage {
244 #[must_use]
246 pub const fn new(message: EmailMessage) -> Self {
247 Self(message)
248 }
249
250 #[must_use]
252 pub const fn message(&self) -> &EmailMessage {
253 &self.0
254 }
255}
256
257#[derive(Clone, Debug, Eq, PartialEq)]
259pub struct MessageBuilder {
260 kind: MessageKind,
261 headers: MessageHeaders,
262 body: Option<MessageBody>,
263}
264
265impl MessageBuilder {
266 #[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 pub fn subject(self, value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
278 self.header("Subject", value)
279 }
280
281 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 #[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 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}