use std::{io::Read, path::Path, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::ole;
use super::{
error::Error,
storage::{Properties, Storages},
};
const B64_CHARS: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
fn base64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(B64_CHARS[((triple >> 18) & 0x3F) as usize] as char);
out.push(B64_CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(B64_CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(B64_CHARS[(triple & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
type Name = String;
type Email = String;
static RE_CONTENT_TYPE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?im)^Content-Type: (.*(\n\s.*)*)\r\n").unwrap());
static RE_DATE: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)Date: (.*(\n\s.*)*)\r\n").unwrap());
static RE_MESSAGE_ID: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?im)^Message-ID: (.*(\n\s.*)*)\r\n").unwrap());
static RE_REPLY_TO: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?im)^Reply-To: (.*(\n\s.*)*)\r\n").unwrap());
#[non_exhaustive]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct TransportHeaders {
pub raw: String,
pub content_type: String,
pub date: String,
pub message_id: String,
pub reply_to: String,
}
impl TransportHeaders {
fn extract_field(text: &str, re: &Regex) -> String {
if text.is_empty() {
return String::new();
}
re.captures(text)
.and_then(|cap| cap.get(1).map(|x| String::from(x.as_str())))
.unwrap_or_default()
}
pub fn create_from_headers_text(text: &str) -> Self {
Self {
raw: text.to_string(),
content_type: Self::extract_field(text, &RE_CONTENT_TYPE),
date: Self::extract_field(text, &RE_DATE),
message_id: Self::extract_field(text, &RE_MESSAGE_ID),
reply_to: Self::extract_field(text, &RE_REPLY_TO),
}
}
}
#[non_exhaustive]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Person {
pub name: Name,
pub email: Email,
}
impl Person {
#[cfg(test)]
fn new(name: Name, email: Email) -> Self {
Self { name, email }
}
fn create_from_props(props: &Properties, name_key: &str, email_keys: &[&str]) -> Self {
let name: String = props.get(name_key).map_or(String::new(), |x| x.into());
let email = email_keys
.iter()
.map(|&key| props.get(key).map_or(String::new(), |x| x.into()))
.find(|x| !x.is_empty())
.unwrap_or_default();
Self { name, email }
}
fn is_x500_dn(email: &str) -> bool {
let upper = email.to_ascii_uppercase();
upper.starts_with("/O=") || upper.starts_with("/CN=")
}
fn resolve_email(&mut self, raw_headers: &str) {
if self.email.is_empty() || !Self::is_x500_dn(&self.email) {
return;
}
if let Some(smtp) = Self::find_smtp_in_headers(raw_headers, &self.name) {
self.email = smtp;
}
}
fn find_smtp_in_headers(headers: &str, display_name: &str) -> Option<String> {
if display_name.is_empty() || headers.is_empty() {
return None;
}
let name_lower = display_name.to_lowercase();
for line in headers.lines() {
let line_lower = line.to_lowercase();
if !line_lower.contains(&name_lower) {
continue;
}
if let Some(start) = line.rfind('<')
&& let Some(end) = line[start..].find('>')
{
let candidate = &line[start + 1..start + end];
if candidate.contains('@') {
return Some(candidate.to_string());
}
}
}
None
}
}
#[non_exhaustive]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Attachment {
pub display_name: String,
pub payload: String,
#[serde(with = "hex")]
pub payload_bytes: Vec<u8>,
pub extension: String,
pub mime_tag: String,
pub file_name: String,
pub long_file_name: String,
pub attach_method: u32,
pub content_id: String,
}
impl Attachment {
fn create(storages: &Storages, idx: usize) -> Self {
let payload_bytes = storages.get_bytes_from_attachment(idx, "AttachDataObject");
let payload = hex::encode(&payload_bytes);
Self {
display_name: storages.get_val_from_attachment_or_default(idx, "DisplayName"),
payload,
payload_bytes,
extension: storages.get_val_from_attachment_or_default(idx, "AttachExtension"),
mime_tag: storages.get_val_from_attachment_or_default(idx, "AttachMimeTag"),
file_name: storages.get_val_from_attachment_or_default(idx, "AttachFilename"),
long_file_name: storages.get_val_from_attachment_or_default(idx, "AttachLongFilename"),
attach_method: storages
.get_attachment_int_prop(idx, "AttachMethod")
.unwrap_or(0),
content_id: storages.get_val_from_attachment_or_default(idx, "AttachContentId"),
}
}
pub fn is_embedded_message(&self) -> bool {
self.attach_method == 5
}
pub fn as_message(&self) -> Option<Result<Outlook, Error>> {
if !self.is_embedded_message() || self.payload_bytes.is_empty() {
return None;
}
Some(Outlook::from_slice(&self.payload_bytes))
}
}
#[non_exhaustive]
#[derive(Serialize, Deserialize, Debug)]
pub struct Outlook {
pub headers: TransportHeaders,
pub sender: Person,
pub to: Vec<Person>,
pub cc: Vec<Person>,
pub bcc: Vec<Person>,
pub subject: String,
pub body: String,
pub html: String,
pub rtf_compressed: String,
pub message_class: String,
pub importance: u32,
pub sensitivity: u32,
pub client_submit_time: String,
pub message_delivery_time: String,
pub creation_time: String,
pub last_modification_time: String,
pub attachments: Vec<Attachment>,
pub named_properties: std::collections::HashMap<String, String>,
}
impl Outlook {
fn populate(storages: &Storages) -> Self {
let headers_text = storages.get_val_from_root_or_default("TransportMessageHeaders");
let headers = TransportHeaders::create_from_headers_text(&headers_text);
let mut to = Vec::new();
let mut cc = Vec::new();
let mut bcc = Vec::new();
for (i, recip_map) in storages.recipients.iter().enumerate() {
let mut person = Person::create_from_props(
recip_map,
"DisplayName",
&["SmtpAddress", "EmailAddress"],
);
person.resolve_email(&headers_text);
match storages.get_recipient_int_prop(i, "RecipientType") {
Some(2) => cc.push(person),
Some(3) => bcc.push(person),
_ => to.push(person), }
}
let mut sender = Person::create_from_props(
&storages.root,
"SenderName",
&["SenderSmtpAddress", "SenderEmailAddress"],
);
sender.resolve_email(&headers_text);
Self {
headers,
sender,
to,
cc,
bcc,
subject: storages.get_val_from_root_or_default("Subject"),
body: storages.get_val_from_root_or_default("Body"),
html: storages.get_val_from_root_or_default("Html"),
rtf_compressed: storages.get_val_from_root_or_default("RtfCompressed"),
message_class: storages.get_val_from_root_or_default("MessageClass"),
importance: storages.get_root_int_prop("Importance").unwrap_or(1),
sensitivity: storages.get_root_int_prop("Sensitivity").unwrap_or(0),
client_submit_time: storages.get_val_from_root_or_default("ClientSubmitTime"),
message_delivery_time: storages.get_val_from_root_or_default("MessageDeliveryTime"),
creation_time: storages.get_val_from_root_or_default("CreationTime"),
last_modification_time: storages.get_val_from_root_or_default("LastModificationTime"),
attachments: storages
.attachments
.iter()
.enumerate()
.map(|(i, _)| Attachment::create(storages, i))
.collect(),
named_properties: {
let named_names = storages.named_property_names();
storages
.root
.iter()
.filter(|(k, _)| named_names.contains(k.as_str()))
.map(|(k, v)| (k.clone(), String::from(v)))
.collect()
},
}
}
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let data = std::fs::read(path)?;
let parser = ole::Reader::from_bytes(data)?;
let mut storages = Storages::new(&parser);
storages.process_streams(&parser);
let outlook = Self::populate(&storages);
Ok(outlook)
}
pub fn from_reader<R: std::io::Read>(reader: R) -> Result<Self, Error> {
const MAX_SIZE: u64 = 256 * 1024 * 1024;
let mut limited = reader.take(MAX_SIZE + 1);
let mut buf = Vec::new();
limited.read_to_end(&mut buf)?;
if buf.len() as u64 > MAX_SIZE {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Input exceeds maximum allowed size (256 MB)",
)
.into());
}
let parser = ole::Reader::from_bytes(buf)?;
let mut storages = Storages::new(&parser);
storages.process_streams(&parser);
Ok(Self::populate(&storages))
}
pub fn from_slice(slice: impl AsRef<[u8]>) -> Result<Self, Error> {
let parser = ole::Reader::from_bytes(slice.as_ref())?;
let mut storages = Storages::new(&parser);
storages.process_streams(&parser);
let outlook = Self::populate(&storages);
Ok(outlook)
}
pub fn to_json(&self) -> Result<String, Error> {
Ok(serde_json::to_string(self)?)
}
pub fn rtf_decompressed(&self) -> Option<Vec<u8>> {
if self.rtf_compressed.is_empty() {
return None;
}
let raw = hex::decode(&self.rtf_compressed).ok()?;
super::rtf::decompress_rtf(&raw)
}
pub fn html_from_rtf(&self) -> Option<String> {
let rtf = self.rtf_decompressed()?;
super::rtf::extract_html_from_rtf(&rtf)
}
pub fn attachment_by_content_id(&self, cid: &str) -> Option<&Attachment> {
if cid.is_empty() {
return None;
}
self.attachments
.iter()
.find(|a| !a.content_id.is_empty() && a.content_id == cid)
}
pub fn resolve_cid_references(&self, html: &str) -> String {
let mut result = html.to_string();
for attach in &self.attachments {
if attach.content_id.is_empty() {
continue;
}
let cid_ref = format!("cid:{}", attach.content_id);
if !result.contains(&cid_ref) {
continue;
}
let mime = if attach.mime_tag.is_empty() {
"application/octet-stream"
} else {
&attach.mime_tag
};
let b64 = base64_encode(&attach.payload_bytes);
let data_uri = format!("data:{};base64,{}", mime, b64);
result = result.replace(&cid_ref, &data_uri);
}
result
}
}
impl std::fmt::Display for Person {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.name.is_empty() {
write!(f, "{}", self.email)
} else if self.email.is_empty() || self.name == self.email {
write!(f, "{}", self.name)
} else {
write!(f, "{} <{}>", self.name, self.email)
}
}
}
impl std::fmt::Display for Attachment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = if self.long_file_name.is_empty() {
if self.file_name.is_empty() {
&self.display_name
} else {
&self.file_name
}
} else {
&self.long_file_name
};
let method = match self.attach_method {
1 => "file",
5 => "embedded .msg",
6 => "OLE object",
_ => "unknown",
};
write!(
f,
"{} ({}, {} bytes)",
name,
method,
self.payload_bytes.len()
)
}
}
impl std::fmt::Display for Outlook {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "From: {}", self.sender)?;
writeln!(f, "Subject: {}", self.subject)?;
if !self.to.is_empty() {
let to: Vec<String> = self.to.iter().map(|p| p.to_string()).collect();
writeln!(f, "To: {}", to.join(", "))?;
}
if !self.cc.is_empty() {
let cc: Vec<String> = self.cc.iter().map(|p| p.to_string()).collect();
writeln!(f, "CC: {}", cc.join(", "))?;
}
if !self.bcc.is_empty() {
let bcc: Vec<String> = self.bcc.iter().map(|p| p.to_string()).collect();
writeln!(f, "BCC: {}", bcc.join(", "))?;
}
if !self.message_delivery_time.is_empty() {
writeln!(f, "Date: {}", self.message_delivery_time)?;
}
if !self.attachments.is_empty() {
writeln!(f, "Attachments ({}):", self.attachments.len())?;
for a in &self.attachments {
writeln!(f, " - {}", a)?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{Outlook, Person, TransportHeaders};
#[test]
fn test_invalid_file() {
let path = "data/bad_outlook.msg";
let err = Outlook::from_path(path).unwrap_err();
assert_eq!(
err.to_string(),
"Error parsing file with ole: Invalid OLE File".to_string()
);
}
#[test]
fn test_transport_header_test_email_1() {
use super::super::storage::Storages;
use crate::ole::Reader;
let parser = Reader::from_path("data/test_email.msg").unwrap();
let mut storages = Storages::new(&parser);
storages.process_streams(&parser);
let transport_text = storages.get_val_from_root_or_default("TransportMessageHeaders");
let header = TransportHeaders::create_from_headers_text(&transport_text);
assert!(header.raw.is_empty());
assert!(header.content_type.is_empty());
assert!(header.date.is_empty());
assert!(header.message_id.is_empty());
assert!(header.reply_to.is_empty());
}
#[test]
fn test_test_email() {
let path = "data/test_email.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(
outlook.sender,
Person {
name: "".to_string(),
email: "".to_string()
}
);
assert_eq!(
outlook.to,
vec![Person {
name: "marirs@outlook.com".to_string(),
email: "marirs@outlook.com".to_string()
}]
);
assert_eq!(
outlook.cc,
vec![
Person {
name: "Sriram Govindan".to_string(),
email: "marirs@aol.in".to_string()
},
Person {
name: "marirs@outlook.in".to_string(),
email: "marirs@outlook.in".to_string()
},
]
);
assert_eq!(
outlook.bcc,
vec![
Person {
name: "Sriram Govindan".to_string(),
email: "marirs@aol.in".to_string()
},
Person {
name: "Sriram Govindan".to_string(),
email: "marirs@outlook.com".to_string()
},
Person {
name: "marirs@outlook.in".to_string(),
email: "marirs@outlook.in".to_string()
},
]
);
assert_eq!(outlook.subject, String::from("Test Email"));
assert!(outlook.headers.raw.is_empty());
assert!(outlook.headers.content_type.is_empty());
assert!(outlook.body.starts_with("Test Email\r\n"));
assert!(outlook.rtf_compressed.starts_with("51210000c8a200004c5a4"));
}
#[test]
fn test_test_email_2() {
let path = "data/test_email.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(
outlook.sender,
Person {
name: "".to_string(),
email: "".to_string()
}
);
assert_eq!(outlook.to.len(), 1);
assert_eq!(outlook.cc.len(), 2);
assert_eq!(outlook.bcc.len(), 3);
assert_eq!(outlook.subject, String::from("Test Email"));
assert!(outlook.body.starts_with("Test Email"));
assert_eq!(outlook.attachments.len(), 3);
let displays: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.display_name.clone())
.collect();
assert_eq!(
displays,
vec![
"1 Days Left—35% off cloud space, upgrade now!".to_string(),
"milky-way-2695569_960_720.jpg".to_string(),
"Test Email.msg".to_string(),
]
);
let exts: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.extension.clone())
.collect();
assert_eq!(
exts,
vec!["".to_string(), ".jpg".to_string(), ".msg".to_string()]
);
let mimes: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.mime_tag.clone())
.collect();
assert_eq!(mimes, vec!["".to_string(), "".to_string(), "".to_string()]);
let filenames: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.file_name.clone())
.collect();
assert_eq!(
filenames,
vec![
"".to_string(),
"milky-~1.jpg".to_string(),
"TestEm~1.msg".to_string()
]
);
}
#[test]
fn test_attachment_msg() {
let path = "data/attachment.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(outlook.attachments.len(), 3);
let displays: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.display_name.clone())
.collect();
assert_eq!(
displays,
vec![
"loan_proposal.doc".to_string(),
"image001.png".to_string(),
"image002.jpg".to_string()
]
);
let exts: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.extension.clone())
.collect();
assert_eq!(
exts,
vec![".doc".to_string(), ".png".to_string(), ".jpg".to_string()]
);
let mimes: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.mime_tag.clone())
.collect();
assert_eq!(
mimes,
vec![
"application/msword".to_string(),
"image/png".to_string(),
"image/jpeg".to_string()
]
);
let filenames: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.file_name.clone())
.collect();
assert_eq!(
filenames,
vec![
"loan_p~1.doc".to_string(),
"image001.png".to_string(),
"image002.jpg".to_string()
]
);
let long_names: Vec<String> = outlook
.attachments
.iter()
.map(|x| x.long_file_name.clone())
.collect();
assert_eq!(
long_names,
vec![
"loan_proposal.doc".to_string(),
"image001.png".to_string(),
"image002.jpg".to_string()
]
);
}
#[test]
fn test_payload_bytes() {
let outlook = Outlook::from_path("data/attachment.msg").unwrap();
for attach in &outlook.attachments {
assert_eq!(attach.payload, hex::encode(&attach.payload_bytes));
}
assert_eq!(
&outlook.attachments[0].payload_bytes[..4],
b"\xd0\xcf\x11\xe0"
);
assert_eq!(&outlook.attachments[1].payload_bytes[..4], b"\x89PNG");
assert_eq!(&outlook.attachments[2].payload_bytes[..2], b"\xff\xd8");
assert!(!outlook.attachments[0].payload_bytes.is_empty());
assert!(!outlook.attachments[1].payload_bytes.is_empty());
assert!(!outlook.attachments[2].payload_bytes.is_empty());
}
#[test]
fn test_attach_method() {
let outlook = Outlook::from_path("data/test_email.msg").unwrap();
assert_eq!(outlook.attachments[0].attach_method, 5); assert_eq!(outlook.attachments[1].attach_method, 1); assert_eq!(outlook.attachments[2].attach_method, 1);
let outlook = Outlook::from_path("data/attachment.msg").unwrap();
for attach in &outlook.attachments {
assert_eq!(attach.attach_method, 1);
}
}
#[test]
fn test_unicode_msg() {
let path = "data/unicode.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(
outlook.sender,
Person {
name: "Brian Zhou".to_string(),
email: "brizhou@gmail.com".to_string()
}
);
assert_eq!(
outlook.to,
vec![Person {
name: "brianzhou@me.com".to_string(),
email: "brianzhou@me.com".to_string()
}]
);
assert_eq!(
outlook.cc,
vec![Person::new(
"Brian Zhou".to_string(),
"brizhou@gmail.com".to_string()
)]
);
assert!(outlook.bcc.is_empty());
assert_eq!(outlook.subject, String::from("Test for TIF files"));
assert!(!outlook.headers.raw.is_empty());
assert_eq!(
outlook.headers.content_type,
"multipart/mixed; boundary=001a113392ecbd7a5404eb6f4d6a"
);
assert_eq!(outlook.headers.date, "Mon, 18 Nov 2013 10:26:24 +0200");
assert_eq!(
outlook.headers.message_id,
"<CADtJ4eNjQSkGcBtVteCiTF+YFG89+AcHxK3QZ=-Mt48xygkvdQ@mail.gmail.com>"
);
assert!(outlook.headers.reply_to.is_empty());
assert!(outlook.rtf_compressed.starts_with("bc020000b908"));
}
#[test]
fn test_ascii() {
let path = "data/ascii.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(
outlook.sender,
Person {
name: "from@domain.com".to_string(),
email: "from@domain.com".to_string()
}
);
assert_eq!(
outlook.to,
vec![Person {
name: "to@domain.com".to_string(),
email: "to@domain.com".to_string()
},]
);
assert_eq!(
outlook.subject,
String::from("creating an outlook message file")
);
}
#[test]
fn test_recipient_types() {
let path = "data/test_email.msg";
let outlook = Outlook::from_path(path).unwrap();
assert_eq!(outlook.to.len(), 1);
assert_eq!(outlook.cc.len(), 2);
assert_eq!(outlook.bcc.len(), 3);
let outlook = Outlook::from_path("data/ascii.msg").unwrap();
assert_eq!(outlook.to.len(), 1);
assert!(outlook.cc.is_empty());
assert!(outlook.bcc.is_empty());
}
#[test]
fn test_date_fields() {
let outlook = Outlook::from_path("data/unicode.msg").unwrap();
assert_eq!(outlook.client_submit_time, "2013-11-18T08:26:24Z");
assert_eq!(outlook.message_delivery_time, "2013-11-18T08:26:29Z");
assert!(outlook.creation_time.starts_with("2013-11-18T08:32:28"));
assert!(
outlook
.last_modification_time
.starts_with("2013-11-18T08:32:28")
);
let outlook = Outlook::from_path("data/test_email.msg").unwrap();
assert!(outlook.client_submit_time.is_empty());
assert!(
outlook
.message_delivery_time
.starts_with("2021-01-05T03:00:32")
);
assert!(outlook.creation_time.starts_with("2021-01-05T03:13:18"));
let outlook = Outlook::from_path("data/ascii.msg").unwrap();
assert!(outlook.client_submit_time.is_empty());
assert!(outlook.message_delivery_time.is_empty());
assert!(outlook.creation_time.starts_with("2017-06-01T15:24:31"));
}
#[test]
fn test_message_class_importance_sensitivity() {
let outlook = Outlook::from_path("data/test_email.msg").unwrap();
assert_eq!(outlook.message_class, "IPM.Note");
assert_eq!(outlook.importance, 1); assert_eq!(outlook.sensitivity, 0);
let outlook = Outlook::from_path("data/unicode.msg").unwrap();
assert_eq!(outlook.message_class, "IPM.Note");
assert_eq!(outlook.importance, 1);
assert_eq!(outlook.sensitivity, 0);
let outlook = Outlook::from_path("data/ascii.msg").unwrap();
assert_eq!(outlook.message_class, "IPM.Note");
}
#[test]
fn test_from_reader() {
let file = std::fs::File::open("data/unicode.msg").unwrap();
let reader_outlook = Outlook::from_reader(file).unwrap();
let path_outlook = Outlook::from_path("data/unicode.msg").unwrap();
assert_eq!(reader_outlook.subject, path_outlook.subject);
assert_eq!(reader_outlook.sender, path_outlook.sender);
assert_eq!(reader_outlook.to, path_outlook.to);
assert_eq!(reader_outlook.cc, path_outlook.cc);
}
#[test]
fn test_to_json() {
let path = "data/test_email.msg";
let outlook = Outlook::from_path(path).unwrap();
let json = outlook.to_json().unwrap();
assert!(!json.is_empty());
}
#[test]
fn test_html_field_present() {
let outlook = Outlook::from_path("data/test_email.msg").unwrap();
assert!(outlook.html.is_empty());
let outlook = Outlook::from_path("data/unicode.msg").unwrap();
assert!(outlook.html.is_empty());
}
#[test]
fn test_html_in_json_output() {
let outlook = Outlook::from_path("data/test_email.msg").unwrap();
let json = outlook.to_json().unwrap();
assert!(json.contains("\"html\""));
}
}