#[forbid(unsafe_code)]
pub mod decoders;
pub mod mailbox;
pub mod parsers;
use std::{
borrow::Cow,
fmt::{self, Display},
};
use decoders::html::{html_to_text, text_to_html};
use parsers::{
fields::thread::thread_name,
preview::{preview_html, preview_text},
};
#[cfg(feature = "serde_support")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, PartialEq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Message<'x> {
#[cfg_attr(feature = "serde_support", serde(default))]
pub html_body: Vec<MessagePartId>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub text_body: Vec<MessagePartId>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub attachments: Vec<MessagePartId>,
#[cfg_attr(feature = "serde_support", serde(default))]
#[cfg_attr(feature = "serde_support", serde(borrow))]
pub parts: Vec<MessagePart<'x>>,
#[cfg_attr(feature = "serde_support", serde(skip))]
pub raw_message: Cow<'x, [u8]>,
}
#[derive(Debug, PartialEq, Default, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct MessagePart<'x> {
#[cfg_attr(feature = "serde_support", serde(default))]
pub headers: Vec<Header<'x>>,
pub is_encoding_problem: bool,
#[cfg_attr(feature = "serde_support", serde(default))]
#[cfg_attr(feature = "serde_support", serde(borrow))]
pub body: PartType<'x>,
#[cfg_attr(feature = "serde_support", serde(skip))]
pub encoding: Encoding,
pub offset_header: usize,
pub offset_body: usize,
pub offset_end: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
#[repr(u8)]
pub enum Encoding {
None = 0,
QuotedPrintable = 1,
Base64 = 2,
}
impl From<u8> for Encoding {
fn from(v: u8) -> Self {
match v {
1 => Encoding::QuotedPrintable,
2 => Encoding::Base64,
_ => Encoding::None,
}
}
}
impl Default for Encoding {
fn default() -> Self {
Encoding::None
}
}
pub type MessagePartId = usize;
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub enum PartType<'x> {
Text(Cow<'x, str>),
Html(Cow<'x, str>),
#[cfg_attr(feature = "serde_support", serde(borrow))]
Binary(Cow<'x, [u8]>),
#[cfg_attr(feature = "serde_support", serde(borrow))]
InlineBinary(Cow<'x, [u8]>),
Message(MessageAttachment<'x>),
Multipart(Vec<MessagePartId>),
}
impl<'x> Default for PartType<'x> {
fn default() -> Self {
PartType::Multipart(Vec::with_capacity(0))
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Addr<'x> {
#[cfg_attr(feature = "serde_support", serde(default))]
pub name: Option<Cow<'x, str>>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub address: Option<Cow<'x, str>>,
}
impl<'x> Addr<'x> {
pub fn new(name: Option<&'x str>, address: &'x str) -> Self {
Self {
name: name.map(|name| name.into()),
address: Some(address.into()),
}
}
pub fn into_owned<'y>(self) -> Addr<'y> {
Addr {
name: self.name.map(|s| s.into_owned().into()),
address: self.address.map(|s| s.into_owned().into()),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Group<'x> {
#[cfg_attr(feature = "serde_support", serde(default))]
pub name: Option<Cow<'x, str>>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub addresses: Vec<Addr<'x>>,
}
impl<'x> Group<'x> {
pub fn new(name: &'x str, addresses: Vec<Addr<'x>>) -> Self {
Self {
name: Some(name.into()),
addresses,
}
}
pub fn into_owned<'y>(self) -> Group<'y> {
Group {
name: self.name.map(|s| s.into_owned().into()),
addresses: self.addresses.into_iter().map(|a| a.into_owned()).collect(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct Header<'x> {
pub name: HeaderName<'x>,
pub value: HeaderValue<'x>,
pub offset_start: usize,
pub offset_end: usize,
}
impl<'x> Header<'x> {
pub fn name(&self) -> &str {
self.name.as_str()
}
pub fn value(&self) -> &HeaderValue {
&self.value
}
pub fn offset_start(&self) -> usize {
self.offset_start
}
pub fn offset_end(&self) -> usize {
self.offset_end
}
}
#[derive(Debug, Hash, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub enum HeaderName<'x> {
Rfc(RfcHeader),
Other(Cow<'x, str>),
}
impl PartialEq for HeaderName<'_> {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Rfc(a), Self::Rfc(b)) => a == b,
(Self::Other(a), Self::Other(b)) => a.eq_ignore_ascii_case(b),
_ => false,
}
}
}
impl Eq for HeaderName<'_> {}
impl<'x> HeaderName<'x> {
pub fn as_str(&self) -> &str {
match self {
HeaderName::Rfc(header) => header.as_str(),
HeaderName::Other(name) => name.as_ref(),
}
}
pub fn into_owned<'y>(&self) -> HeaderName<'y> {
match self {
HeaderName::Rfc(header) => HeaderName::Rfc(*header),
HeaderName::Other(name) => HeaderName::Other(name.clone().into_owned().into()),
}
}
pub fn unwrap(self) -> String {
match self {
HeaderName::Rfc(header) => header.as_str().to_owned(),
HeaderName::Other(name) => name.into_owned(),
}
}
pub fn is_mime_header(&self) -> bool {
match self {
HeaderName::Rfc(header) => header.is_mime_header(),
HeaderName::Other(_) => false,
}
}
}
#[repr(u8)]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, PartialOrd, Ord)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde_support", serde(rename_all = "snake_case"))]
pub enum RfcHeader {
Subject = 0,
From = 1,
To = 2,
Cc = 3,
Date = 4,
Bcc = 5,
ReplyTo = 6,
Sender = 7,
Comments = 8,
InReplyTo = 9,
Keywords = 10,
Received = 11,
MessageId = 12,
References = 13,
ReturnPath = 14,
MimeVersion = 15,
ContentDescription = 16,
ContentId = 17,
ContentLanguage = 18,
ContentLocation = 19,
ContentTransferEncoding = 20,
ContentType = 21,
ContentDisposition = 22,
ResentTo = 23,
ResentFrom = 24,
ResentBcc = 25,
ResentCc = 26,
ResentSender = 27,
ResentDate = 28,
ResentMessageId = 29,
ListArchive = 30,
ListHelp = 31,
ListId = 32,
ListOwner = 33,
ListPost = 34,
ListSubscribe = 35,
ListUnsubscribe = 36,
}
impl RfcHeader {
pub fn as_str(&self) -> &'static str {
match self {
RfcHeader::Subject => "Subject",
RfcHeader::From => "From",
RfcHeader::To => "To",
RfcHeader::Cc => "Cc",
RfcHeader::Date => "Date",
RfcHeader::Bcc => "Bcc",
RfcHeader::ReplyTo => "Reply-To",
RfcHeader::Sender => "Sender",
RfcHeader::Comments => "Comments",
RfcHeader::InReplyTo => "In-Reply-To",
RfcHeader::Keywords => "Keywords",
RfcHeader::Received => "Received",
RfcHeader::MessageId => "Message-ID",
RfcHeader::References => "References",
RfcHeader::ReturnPath => "Return-Path",
RfcHeader::MimeVersion => "MIME-Version",
RfcHeader::ContentDescription => "Content-Description",
RfcHeader::ContentId => "Content-ID",
RfcHeader::ContentLanguage => "Content-Language",
RfcHeader::ContentLocation => "Content-Location",
RfcHeader::ContentTransferEncoding => "Content-Transfer-Encoding",
RfcHeader::ContentType => "Content-Type",
RfcHeader::ContentDisposition => "Content-Disposition",
RfcHeader::ResentTo => "Resent-To",
RfcHeader::ResentFrom => "Resent-From",
RfcHeader::ResentBcc => "Resent-Bcc",
RfcHeader::ResentCc => "Resent-Cc",
RfcHeader::ResentSender => "Resent-Sender",
RfcHeader::ResentDate => "Resent-Date",
RfcHeader::ResentMessageId => "Resent-Message-ID",
RfcHeader::ListArchive => "List-Archive",
RfcHeader::ListHelp => "List-Help",
RfcHeader::ListId => "List-ID",
RfcHeader::ListOwner => "List-Owner",
RfcHeader::ListPost => "List-Post",
RfcHeader::ListSubscribe => "List-Subscribe",
RfcHeader::ListUnsubscribe => "List-Unsubscribe",
}
}
pub fn is_mime_header(&self) -> bool {
matches!(
self,
RfcHeader::ContentDescription
| RfcHeader::ContentId
| RfcHeader::ContentLanguage
| RfcHeader::ContentLocation
| RfcHeader::ContentTransferEncoding
| RfcHeader::ContentType
| RfcHeader::ContentDisposition
)
}
}
impl Display for RfcHeader {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub enum HeaderValue<'x> {
Address(Addr<'x>),
AddressList(Vec<Addr<'x>>),
Group(Group<'x>),
GroupList(Vec<Group<'x>>),
Text(Cow<'x, str>),
TextList(Vec<Cow<'x, str>>),
DateTime(DateTime),
ContentType(ContentType<'x>),
Empty,
}
impl<'x> Default for HeaderValue<'x> {
fn default() -> Self {
HeaderValue::Empty
}
}
impl<'x> HeaderValue<'x> {
pub fn is_empty(&self) -> bool {
*self == HeaderValue::Empty
}
pub fn unwrap_text(self) -> Cow<'x, str> {
match self {
HeaderValue::Text(s) => s,
_ => panic!("HeaderValue::unwrap_text called on non-Text value"),
}
}
pub fn unwrap_datetime(self) -> DateTime {
match self {
HeaderValue::DateTime(d) => d,
_ => panic!("HeaderValue::unwrap_datetime called on non-DateTime value"),
}
}
pub fn unwrap_content_type(self) -> ContentType<'x> {
match self {
HeaderValue::ContentType(c) => c,
_ => panic!("HeaderValue::unwrap_content_type called on non-ContentType value"),
}
}
pub fn as_text_ref(&self) -> Option<&str> {
match *self {
HeaderValue::Text(ref s) => Some(s),
HeaderValue::TextList(ref l) => l.last()?.as_ref().into(),
_ => None,
}
}
pub fn as_text_list(&self) -> Option<Vec<&str>> {
match *self {
HeaderValue::Text(ref s) => Some(vec![s.as_ref()]),
HeaderValue::TextList(ref l) => Some(l.iter().map(|l| l.as_ref()).collect()),
_ => None,
}
}
pub fn get_content_type(&self) -> &ContentType<'x> {
match *self {
HeaderValue::ContentType(ref ct) => ct,
_ => panic!(
"HeaderValue::get_content_type called on non-ContentType: {:?}",
self
),
}
}
pub fn as_content_type_ref(&self) -> Option<&ContentType> {
match *self {
HeaderValue::ContentType(ref c) => Some(c),
_ => None,
}
}
pub fn as_datetime_ref(&self) -> Option<&DateTime> {
match *self {
HeaderValue::DateTime(ref d) => Some(d),
_ => None,
}
}
pub fn into_owned<'y>(self) -> HeaderValue<'y> {
match self {
HeaderValue::Address(addr) => HeaderValue::Address(addr.into_owned()),
HeaderValue::AddressList(list) => {
HeaderValue::AddressList(list.into_iter().map(|addr| addr.into_owned()).collect())
}
HeaderValue::Group(group) => HeaderValue::Group(group.into_owned()),
HeaderValue::GroupList(list) => {
HeaderValue::GroupList(list.into_iter().map(|group| group.into_owned()).collect())
}
HeaderValue::Text(text) => HeaderValue::Text(text.into_owned().into()),
HeaderValue::TextList(list) => HeaderValue::TextList(
list.into_iter()
.map(|text| text.into_owned().into())
.collect(),
),
HeaderValue::DateTime(datetime) => HeaderValue::DateTime(datetime),
HeaderValue::ContentType(ct) => HeaderValue::ContentType(ContentType {
c_type: ct.c_type.into_owned().into(),
c_subtype: ct.c_subtype.map(|s| s.into_owned().into()),
attributes: ct.attributes.map(|attributes| {
attributes
.into_iter()
.map(|(k, v)| (k.into_owned().into(), v.into_owned().into()))
.collect()
}),
}),
HeaderValue::Empty => HeaderValue::Empty,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct ContentType<'x> {
pub c_type: Cow<'x, str>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub c_subtype: Option<Cow<'x, str>>,
#[cfg_attr(feature = "serde_support", serde(default))]
pub attributes: Option<Vec<(Cow<'x, str>, Cow<'x, str>)>>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))]
pub struct DateTime {
pub year: u16,
pub month: u8,
pub day: u8,
pub hour: u8,
pub minute: u8,
pub second: u8,
pub tz_before_gmt: bool,
pub tz_hour: u8,
pub tz_minute: u8,
}
impl<'x> Message<'x> {
pub fn get_root_part(&self) -> &MessagePart<'x> {
&self.parts[0]
}
pub fn get_header(&self, header: &str) -> Option<&HeaderValue> {
self.parts[0].headers.get_header(header).map(|h| &h.value)
}
pub fn remove_header(&mut self, header: &str) -> Option<HeaderValue> {
let headers = &mut self.parts[0].headers;
headers
.iter()
.position(|h| h.name.as_str() == header)
.map(|pos| headers.swap_remove(pos).value)
}
pub fn remove_header_rfc(&mut self, header: RfcHeader) -> Option<HeaderValue> {
let headers = &mut self.parts[0].headers;
headers
.iter()
.position(|h| matches!(&h.name, HeaderName::Rfc(header_name) if header_name == &header))
.map(|pos| headers.swap_remove(pos).value)
}
pub fn get_header_raw(&self, header: &str) -> Option<&str> {
self.parts[0]
.headers
.get_header(header)
.and_then(|h| std::str::from_utf8(&self.raw_message[h.offset_start..h.offset_end]).ok())
}
pub fn get_headers(&self) -> &[Header] {
&self.parts[0].headers
}
pub fn get_header_values<'y: 'x>(
&'y self,
name: RfcHeader,
) -> impl Iterator<Item = &HeaderValue<'x>> {
self.parts[0].headers.iter().filter_map(move |header| {
if matches!(&header.name, HeaderName::Rfc(rfc_name) if rfc_name == &name) {
Some(&header.value)
} else {
None
}
})
}
pub fn get_headers_raw(&self) -> impl Iterator<Item = (&str, &str)> {
self.parts[0].headers.iter().filter_map(move |header| {
Some((
header.name.as_str(),
std::str::from_utf8(&self.raw_message[header.offset_start..header.offset_end])
.ok()?,
))
})
}
pub fn get_bcc(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Bcc)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_cc(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Cc)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_comments(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Comments)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_date(&self) -> Option<&DateTime> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Date)
.and_then(|header| header.as_datetime_ref())
}
pub fn get_from(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::From)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_in_reply_to(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::InReplyTo)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_keywords(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Keywords)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_archive(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListArchive)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_help(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListHelp)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_id(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListId)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_owner(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListOwner)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_post(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListPost)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_subscribe(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListSubscribe)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_list_unsubscribe(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ListUnsubscribe)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_message_id(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::MessageId)
.and_then(|header| header.as_text_ref())
}
pub fn get_mime_version(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::MimeVersion)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_received(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Received)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_references(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::References)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_reply_to(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ReplyTo)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_bcc(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentBcc)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_cc(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentTo)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_date(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentDate)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_from(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentFrom)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_message_id(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentMessageId)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_sender(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentSender)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_resent_to(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ResentTo)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_return_path(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ReturnPath)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_return_address(&self) -> Option<&str> {
match self.parts[0].headers.get_rfc(&RfcHeader::ReturnPath) {
Some(HeaderValue::Text(text)) => Some(text.as_ref()),
Some(HeaderValue::TextList(text_list)) => text_list.last().map(|t| t.as_ref()),
_ => match self.parts[0].headers.get_rfc(&RfcHeader::From) {
Some(HeaderValue::Address(addr)) => addr.address.as_deref(),
Some(HeaderValue::AddressList(addr_list)) => {
addr_list.last().and_then(|a| a.address.as_deref())
}
_ => None,
},
}
}
pub fn get_sender(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Sender)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_subject(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::Subject)
.and_then(|header| header.as_text_ref())
}
pub fn get_thread_name(&self) -> Option<&str> {
thread_name(self.get_subject()?).into()
}
pub fn get_to(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::To)
.unwrap_or(&HeaderValue::Empty)
}
pub fn get_body_preview(&self, preview_len: usize) -> Option<Cow<'x, str>> {
if !self.text_body.is_empty() {
preview_text(self.get_text_body(0)?, preview_len).into()
} else if !self.html_body.is_empty() {
preview_html(self.get_html_body(0)?, preview_len).into()
} else {
None
}
}
pub fn get_html_body(&'x self, pos: usize) -> Option<Cow<'x, str>> {
let part = self.parts.get(*self.html_body.get(pos)?)?;
match &part.body {
PartType::Html(html) => Some(html.as_ref().into()),
PartType::Text(text) => Some(text_to_html(text.as_ref()).into()),
_ => None,
}
}
pub fn get_text_body(&'x self, pos: usize) -> Option<Cow<'x, str>> {
let part = self.parts.get(*self.html_body.get(pos)?)?;
match &part.body {
PartType::Text(text) => Some(text.as_ref().into()),
PartType::Html(html) => Some(html_to_text(html.as_ref()).into()),
_ => None,
}
}
pub fn get_part(&self, pos: usize) -> Option<&MessagePart> {
self.parts.get(pos)
}
pub fn get_html_part(&self, pos: usize) -> Option<&MessagePart> {
self.parts.get(*self.html_body.get(pos)?)
}
pub fn get_text_part(&self, pos: usize) -> Option<&MessagePart> {
self.parts.get(*self.text_body.get(pos)?)
}
pub fn get_attachment(&self, pos: usize) -> Option<&MessagePart<'x>> {
self.parts.get(*self.attachments.get(pos)?)
}
pub fn get_text_body_count(&self) -> usize {
self.text_body.len()
}
pub fn get_html_body_count(&self) -> usize {
self.html_body.len()
}
pub fn get_attachment_count(&self) -> usize {
self.attachments.len()
}
pub fn get_text_bodies(&'x self) -> BodyPartIterator<'x> {
BodyPartIterator::new(self, &self.text_body)
}
pub fn get_html_bodies(&'x self) -> BodyPartIterator<'x> {
BodyPartIterator::new(self, &self.html_body)
}
pub fn get_attachments(&'x self) -> AttachmentIterator<'x> {
AttachmentIterator::new(self)
}
}
pub trait MimeHeaders<'x> {
fn get_content_description(&self) -> Option<&str>;
fn get_content_disposition(&self) -> Option<&ContentType>;
fn get_content_id(&self) -> Option<&str>;
fn get_content_transfer_encoding(&self) -> Option<&str>;
fn get_content_type(&self) -> Option<&ContentType>;
fn get_content_language(&self) -> &HeaderValue;
fn get_content_location(&self) -> Option<&str>;
fn get_attachment_name(&self) -> Option<&str> {
self.get_content_disposition()
.and_then(|cd| cd.get_attribute("filename"))
.or_else(|| {
self.get_content_type()
.and_then(|ct| ct.get_attribute("name"))
})
}
}
impl<'x> MimeHeaders<'x> for Message<'x> {
fn get_content_description(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentDescription)
.and_then(|header| header.as_text_ref())
}
fn get_content_disposition(&self) -> Option<&ContentType> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentDisposition)
.and_then(|header| header.as_content_type_ref())
}
fn get_content_id(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentId)
.and_then(|header| header.as_text_ref())
}
fn get_content_transfer_encoding(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentTransferEncoding)
.and_then(|header| header.as_text_ref())
}
fn get_content_type(&self) -> Option<&ContentType> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentType)
.and_then(|header| header.as_content_type_ref())
}
fn get_content_language(&self) -> &HeaderValue {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentLanguage)
.unwrap_or(&HeaderValue::Empty)
}
fn get_content_location(&self) -> Option<&str> {
self.parts[0]
.headers
.get_rfc(&RfcHeader::ContentLocation)
.and_then(|header| header.as_text_ref())
}
}
impl<'x> MessagePart<'x> {
pub fn get_contents(&'x self) -> &'x [u8] {
match &self.body {
PartType::Text(text) | PartType::Html(text) => text.as_bytes(),
PartType::Binary(bin)
| PartType::InlineBinary(bin)
| PartType::Message(MessageAttachment::Raw(bin)) => bin.as_ref(),
PartType::Message(MessageAttachment::Parsed(message)) => message.raw_message.as_ref(),
PartType::Multipart(_) => b"",
}
}
pub fn get_text_contents(&'x self) -> Option<&'x str> {
match &self.body {
PartType::Text(text) | PartType::Html(text) => text.as_ref().into(),
PartType::Binary(bin)
| PartType::InlineBinary(bin)
| PartType::Message(MessageAttachment::Raw(bin)) => {
std::str::from_utf8(bin.as_ref()).ok()
}
PartType::Message(MessageAttachment::Parsed(message)) => {
std::str::from_utf8(message.raw_message.as_ref()).ok()
}
PartType::Multipart(_) => None,
}
}
pub fn get_message(&'x self) -> Option<Cow<'x, Message<'x>>> {
if let PartType::Message(message) = &self.body {
message.get_message()
} else {
None
}
}
pub fn get_sub_parts(&'x self) -> Option<&[MessagePartId]> {
if let PartType::Multipart(parts) = &self.body {
Some(parts.as_ref())
} else {
None
}
}
pub fn len(&self) -> usize {
match &self.body {
PartType::Text(text) | PartType::Html(text) => text.len(),
PartType::Binary(bin)
| PartType::InlineBinary(bin)
| PartType::Message(MessageAttachment::Raw(bin)) => bin.len(),
PartType::Message(MessageAttachment::Parsed(message)) => message.raw_message.len(),
PartType::Multipart(_) => 0,
}
}
pub fn is_text(&self) -> bool {
matches!(self.body, PartType::Text(_) | PartType::Html(_))
}
pub fn is_text_html(&self) -> bool {
matches!(self.body, PartType::Html(_))
}
pub fn is_binary(&self) -> bool {
matches!(self.body, PartType::Binary(_) | PartType::InlineBinary(_))
}
pub fn is_multipart(&self) -> bool {
matches!(self.body, PartType::Multipart(_))
}
pub fn is_message(&self) -> bool {
matches!(self.body, PartType::Message(_))
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn headers(&self) -> &[Header] {
&self.headers
}
pub fn raw_len(&self) -> usize {
self.offset_end.saturating_sub(self.offset_header)
}
pub fn raw_header_offset(&self) -> usize {
self.offset_header
}
pub fn raw_body_offset(&self) -> usize {
self.offset_body
}
pub fn offset_end_offset(&self) -> usize {
self.offset_header
}
}
impl<'x> fmt::Display for MessagePart<'x> {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.write_str(self.get_text_contents().unwrap_or("[no contents]"))
}
}
impl<'x> MimeHeaders<'x> for MessagePart<'x> {
fn get_content_description(&self) -> Option<&str> {
self.headers
.get_rfc(&RfcHeader::ContentDescription)
.and_then(|header| header.as_text_ref())
}
fn get_content_disposition(&self) -> Option<&ContentType> {
self.headers
.get_rfc(&RfcHeader::ContentDisposition)
.and_then(|header| header.as_content_type_ref())
}
fn get_content_id(&self) -> Option<&str> {
self.headers
.get_rfc(&RfcHeader::ContentId)
.and_then(|header| header.as_text_ref())
}
fn get_content_transfer_encoding(&self) -> Option<&str> {
self.headers
.get_rfc(&RfcHeader::ContentTransferEncoding)
.and_then(|header| header.as_text_ref())
}
fn get_content_type(&self) -> Option<&ContentType> {
self.headers
.get_rfc(&RfcHeader::ContentType)
.and_then(|header| header.as_content_type_ref())
}
fn get_content_language(&self) -> &HeaderValue {
self.headers
.get_rfc(&RfcHeader::ContentLanguage)
.unwrap_or(&HeaderValue::Empty)
}
fn get_content_location(&self) -> Option<&str> {
self.headers
.get_rfc(&RfcHeader::ContentLocation)
.and_then(|header| header.as_text_ref())
}
}
pub trait GetHeader {
fn get_rfc(&self, name: &RfcHeader) -> Option<&HeaderValue>;
fn get_header(&self, name: &str) -> Option<&Header>;
}
impl<'x> GetHeader for Vec<Header<'x>> {
fn get_rfc(&self, name: &RfcHeader) -> Option<&HeaderValue<'x>> {
self.iter()
.rev()
.find(|header| matches!(&header.name, HeaderName::Rfc(rfc_name) if rfc_name == name))
.map(|header| &header.value)
}
fn get_header(&self, name: &str) -> Option<&Header> {
self.iter()
.rev()
.find(|header| header.name.as_str().eq_ignore_ascii_case(name))
}
}
#[doc(hidden)]
pub struct BodyPartIterator<'x> {
message: &'x Message<'x>,
list: &'x [MessagePartId],
pos: isize,
}
#[doc(hidden)]
pub struct AttachmentIterator<'x> {
message: &'x Message<'x>,
pos: isize,
}
impl<'x> BodyPartIterator<'x> {
fn new(message: &'x Message<'x>, list: &'x [MessagePartId]) -> BodyPartIterator<'x> {
BodyPartIterator {
message,
list,
pos: -1,
}
}
}
impl<'x> Iterator for BodyPartIterator<'x> {
type Item = &'x MessagePart<'x>;
fn next(&mut self) -> Option<Self::Item> {
self.pos += 1;
self.message.parts.get(*self.list.get(self.pos as usize)?)
}
}
impl<'x> AttachmentIterator<'x> {
fn new(message: &'x Message<'x>) -> AttachmentIterator<'x> {
AttachmentIterator { message, pos: -1 }
}
}
impl<'x> Iterator for AttachmentIterator<'x> {
type Item = &'x MessagePart<'x>;
fn next(&mut self) -> Option<Self::Item> {
self.pos += 1;
self.message.get_attachment(self.pos as usize)
}
}
impl<'x> ContentType<'x> {
pub fn get_type(&self) -> &str {
&self.c_type
}
pub fn get_subtype(&self) -> Option<&str> {
self.c_subtype.as_ref()?.as_ref().into()
}
pub fn get_attribute(&self, name: &str) -> Option<&str> {
self.attributes
.as_ref()?
.iter()
.find(|(key, _)| key == name)?
.1
.as_ref()
.into()
}
pub fn remove_attribute(&mut self, name: &str) -> Option<Cow<str>> {
let attributes = self.attributes.as_mut()?;
attributes
.iter()
.position(|(key, _)| key == name)
.map(|pos| attributes.swap_remove(pos).1)
}
pub fn get_attributes(&self) -> Option<&[(Cow<str>, Cow<str>)]> {
self.attributes.as_deref()
}
pub fn has_attribute(&self, name: &str) -> bool {
self.attributes
.as_ref()
.map_or_else(|| false, |attr| attr.iter().any(|(key, _)| key == name))
}
pub fn is_attachment(&self) -> bool {
self.c_type.eq_ignore_ascii_case("attachment")
}
pub fn is_inline(&self) -> bool {
self.c_type.eq_ignore_ascii_case("inline")
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum MessageAttachment<'x> {
Parsed(Box<Message<'x>>),
Raw(Cow<'x, [u8]>),
}
impl<'x> Default for MessageAttachment<'x> {
fn default() -> Self {
MessageAttachment::Raw((&[] as &[u8]).into())
}
}
impl<'x> MessageAttachment<'x> {
pub fn get_message(&'x self) -> Option<Cow<'x, Message<'x>>> {
match self {
MessageAttachment::Parsed(message) => Some(Cow::Borrowed(message.as_ref())),
MessageAttachment::Raw(raw_message) => Some(Cow::Owned(Message::parse(raw_message)?)),
}
}
}
#[cfg(feature = "serde_support")]
impl<'x> Serialize for MessageAttachment<'x> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
MessageAttachment::Parsed(message) => message.serialize(serializer),
MessageAttachment::Raw(raw) => Message::parse(raw.as_ref())
.ok_or_else(|| serde::ser::Error::custom("Failed to parse message attachment."))?
.serialize(serializer),
}
}
}
#[cfg(feature = "serde_support")]
impl<'x, 'de> Deserialize<'de> for MessageAttachment<'x> {
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
panic!("Deserializing message attachments is not supported at this time.")
}
}
impl From<DateTime> for i64 {
fn from(value: DateTime) -> Self {
value.to_timestamp()
}
}
impl From<RfcHeader> for String {
fn from(header: RfcHeader) -> Self {
header.to_string()
}
}
impl From<RfcHeader> for Cow<'_, str> {
fn from(header: RfcHeader) -> Self {
Cow::Borrowed(header.as_str())
}
}