#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::fmt;
use std::error::Error;
use use_email_header::{HeaderField, HeaderParseError};
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MessageBuildError {
MissingBody,
}
impl fmt::Display for MessageBuildError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingBody => formatter.write_str("email message body is required"),
}
}
}
impl Error for MessageBuildError {}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum MessageKind {
#[default]
PlainText,
Html,
Multipart,
Raw,
}
impl MessageKind {
#[must_use]
pub const fn default_content_type(self) -> Option<&'static str> {
match self {
Self::PlainText => Some("text/plain; charset=utf-8"),
Self::Html => Some("text/html; charset=utf-8"),
Self::Multipart | Self::Raw => None,
}
}
}
#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct MessageBody(String);
impl MessageBody {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for MessageBody {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct MessageHeaders {
fields: Vec<HeaderField>,
}
impl MessageHeaders {
#[must_use]
pub const fn new() -> Self {
Self { fields: Vec::new() }
}
#[must_use]
pub fn with_field(mut self, field: HeaderField) -> Self {
self.fields.push(field);
self
}
pub fn push(&mut self, field: HeaderField) {
self.fields.push(field);
}
#[must_use]
pub fn fields(&self) -> &[HeaderField] {
&self.fields
}
#[must_use]
pub fn first_value(&self, name: &str) -> Option<&str> {
self.fields
.iter()
.find(|field| field.name().as_str().eq_ignore_ascii_case(name))
.map(|field| field.value().as_str())
}
}
impl fmt::Display for MessageHeaders {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, field) in self.fields.iter().enumerate() {
if index > 0 {
formatter.write_str("\r\n")?;
}
write!(formatter, "{field}")?;
}
Ok(())
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EmailMessage {
headers: MessageHeaders,
body: MessageBody,
kind: MessageKind,
}
impl EmailMessage {
#[must_use]
pub fn plain_text(subject: impl Into<String>, body: impl Into<String>) -> Self {
Self::with_kind_subject_body(MessageKind::PlainText, subject, body)
}
#[must_use]
pub fn html(subject: impl Into<String>, body: impl Into<String>) -> Self {
Self::with_kind_subject_body(MessageKind::Html, subject, body)
}
#[must_use]
pub const fn new(headers: MessageHeaders, body: MessageBody, kind: MessageKind) -> Self {
Self {
headers,
body,
kind,
}
}
pub fn with_header(
mut self,
name: impl AsRef<str>,
value: impl AsRef<str>,
) -> Result<Self, HeaderParseError> {
self.headers.push(HeaderField::new(name, value)?);
Ok(self)
}
#[must_use]
pub const fn headers(&self) -> &MessageHeaders {
&self.headers
}
#[must_use]
pub const fn body(&self) -> &MessageBody {
&self.body
}
#[must_use]
pub const fn kind(&self) -> MessageKind {
self.kind
}
#[must_use]
pub fn subject(&self) -> Option<&str> {
self.headers.first_value("Subject")
}
#[must_use]
pub fn body_mime(&self) -> Option<use_mime::MimeType> {
self.headers
.first_value("Content-Type")
.and_then(use_mime::parse_mime)
}
fn with_kind_subject_body(
kind: MessageKind,
subject: impl Into<String>,
body: impl Into<String>,
) -> Self {
let mut headers = MessageHeaders::new().with_field(
HeaderField::new("Subject", subject.into()).expect("Subject header is valid"),
);
if let Some(content_type) = kind.default_content_type() {
headers.push(content_type_field(content_type));
}
Self {
headers,
body: MessageBody::new(body),
kind,
}
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct RawMessage(String);
impl RawMessage {
#[must_use]
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedMessage(EmailMessage);
impl ParsedMessage {
#[must_use]
pub const fn new(message: EmailMessage) -> Self {
Self(message)
}
#[must_use]
pub const fn message(&self) -> &EmailMessage {
&self.0
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct MessageBuilder {
kind: MessageKind,
headers: MessageHeaders,
body: Option<MessageBody>,
}
impl MessageBuilder {
#[must_use]
pub const fn new(kind: MessageKind) -> Self {
Self {
kind,
headers: MessageHeaders::new(),
body: None,
}
}
pub fn subject(self, value: impl AsRef<str>) -> Result<Self, HeaderParseError> {
self.header("Subject", value)
}
pub fn header(
mut self,
name: impl AsRef<str>,
value: impl AsRef<str>,
) -> Result<Self, HeaderParseError> {
self.headers.push(HeaderField::new(name, value)?);
Ok(self)
}
#[must_use]
pub fn body(mut self, value: impl Into<String>) -> Self {
self.body = Some(MessageBody::new(value));
self
}
pub fn build(mut self) -> Result<EmailMessage, MessageBuildError> {
if self.headers.first_value("Content-Type").is_none()
&& let Some(content_type) = self.kind.default_content_type()
{
self.headers.push(content_type_field(content_type));
}
Ok(EmailMessage::new(
self.headers,
self.body.ok_or(MessageBuildError::MissingBody)?,
self.kind,
))
}
}
fn content_type_field(content_type: &str) -> HeaderField {
HeaderField::new("Content-Type", content_type).expect("Content-Type header is valid")
}
#[cfg(test)]
mod tests {
use super::{EmailMessage, MessageBuildError, MessageBuilder, MessageKind};
#[test]
fn creates_plain_text_and_html_messages() {
let plain = EmailMessage::plain_text("Hello", "A short note.");
let html = EmailMessage::html("Hello", "<p>A short note.</p>");
assert_eq!(plain.subject(), Some("Hello"));
assert_eq!(plain.kind(), MessageKind::PlainText);
assert_eq!(plain.body_mime().expect("mime").subtype, "plain");
assert_eq!(html.body_mime().expect("mime").subtype, "html");
}
#[test]
fn builds_messages_with_headers() -> Result<(), Box<dyn std::error::Error>> {
let message = MessageBuilder::new(MessageKind::Html)
.subject("Hello")?
.header("From", "jane@example.com")?
.body("<p>Hello</p>")
.build()?;
assert_eq!(message.subject(), Some("Hello"));
assert_eq!(
message.headers().first_value("from"),
Some("jane@example.com")
);
Ok(())
}
#[test]
fn builder_requires_body() {
assert_eq!(
MessageBuilder::new(MessageKind::PlainText).build(),
Err(MessageBuildError::MissingBody)
);
}
}