mirror of
https://gitlab.com/mailcat-devs/mailcat.git
synced 2025-12-20 13:23:26 +01:00
213 lines
6 KiB
Rust
213 lines
6 KiB
Rust
use mailparse::{DispositionType, MailHeaderMap, ParsedMail};
|
|
|
|
#[derive(PartialEq, Eq, Debug, Default)]
|
|
pub struct Body {
|
|
pub text: Option<String>,
|
|
}
|
|
|
|
pub trait ParsedMailExt {
|
|
fn is_attachment(&self) -> bool;
|
|
fn subject(&self) -> Option<String>;
|
|
fn date(&self) -> Result<Option<chrono::DateTime<chrono::FixedOffset>>, anyhow::Error>;
|
|
fn body(&self) -> Result<Option<Body>, anyhow::Error>;
|
|
}
|
|
|
|
impl<'a> ParsedMailExt for ParsedMail<'a> {
|
|
fn is_attachment(&self) -> bool {
|
|
self.get_content_disposition().disposition == DispositionType::Attachment
|
|
}
|
|
|
|
fn subject(&self) -> Option<String> {
|
|
self.headers.get_first_value("Subject")
|
|
}
|
|
fn date(&self) -> Result<Option<chrono::DateTime<chrono::FixedOffset>>, anyhow::Error> {
|
|
let date = self
|
|
.headers
|
|
.get_first_value("Date")
|
|
.map(|s| chrono::DateTime::parse_from_rfc2822(&s))
|
|
.transpose()?;
|
|
Ok(date)
|
|
}
|
|
|
|
fn body(&self) -> Result<Option<Body>, anyhow::Error> {
|
|
let mimetype: mime::Mime = self.ctype.mimetype.parse()?;
|
|
if self.is_attachment() {
|
|
return Ok(None);
|
|
}
|
|
if mimetype == mime::TEXT_PLAIN {
|
|
return Ok(Some(Body {
|
|
text: Some(self.get_body()?),
|
|
}));
|
|
}
|
|
for subpart in &self.subparts {
|
|
if let Some(body) = subpart.body()? {
|
|
return Ok(Some(body));
|
|
}
|
|
}
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use chrono::TimeZone;
|
|
|
|
use super::*;
|
|
|
|
const TEXT_PLAIN: &str = r#"From: Andrey Golovizin <ag@sologoc.com>
|
|
To: ag@sologoc.com
|
|
Subject: Plain text body
|
|
Date: Sat, 17 Jul 2021 18:04:10 +0200
|
|
Message-ID: <2148453.iZASKD2KPV@sakuragaoka>
|
|
MIME-Version: 1.0
|
|
Content-Transfer-Encoding: quoted-printable
|
|
Content-Type: text/plain; charset="iso-8859-1"
|
|
|
|
Prost=FD text.
|
|
"#;
|
|
|
|
const MULTIPART_ALTERNATIVE: &str = r#"From: Andrey Golovizin <ag@sologoc.com>
|
|
Subject: Plain text with HTML
|
|
Date: Sat, 17 Jul 2021 18:13:26 +0200
|
|
Message-ID: <3609140.kQq0lBPeGt@sakuragaoka>
|
|
MIME-Version: 1.0
|
|
Content-Type: multipart/alternative; boundary="nextPart3377186.iIbC2pHGDl"
|
|
Content-Transfer-Encoding: 7Bit
|
|
|
|
This is a multi-part message in MIME format.
|
|
|
|
--nextPart3377186.iIbC2pHGDl
|
|
Content-Transfer-Encoding: quoted-printable
|
|
Content-Type: text/plain; charset="iso-8859-1"
|
|
|
|
Prost=FD text.
|
|
--nextPart3377186.iIbC2pHGDl
|
|
Content-Transfer-Encoding: quoted-printable
|
|
Content-Type: text/html; charset="UTF-8"
|
|
|
|
<html>
|
|
<head>
|
|
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF-8">
|
|
</head>
|
|
<body><p style=3D"margin-top:0;margin-bottom:0;margin-left:0;margin-right:0=
|
|
;"><strong>N=C4=9Bjaky HTML text.</strong></p>
|
|
</body>
|
|
</html>
|
|
--nextPart3377186.iIbC2pHGDl--
|
|
|
|
"#;
|
|
|
|
const MULTIPART_MIXED_ALTERNATIVE: &str = r#"From: Andrey Golovizin <ag@sologoc.com>
|
|
Subject: Plain text with HTML and attachment
|
|
Date: Sat, 17 Jul 2021 21:39:08 +0200
|
|
Message-ID: <3054314.5fSG56mABF@sakuragaoka>
|
|
MIME-Version: 1.0
|
|
Content-Type: multipart/mixed; boundary="nextPart1698715.VLH7GnMWUR"
|
|
Content-Transfer-Encoding: 7Bit
|
|
|
|
This is a multi-part message in MIME format.
|
|
|
|
--nextPart1698715.VLH7GnMWUR
|
|
Content-Type: multipart/alternative; boundary="nextPart5630828.MhkbZ0Pkbq"
|
|
Content-Transfer-Encoding: 7Bit
|
|
|
|
This is a multi-part message in MIME format.
|
|
|
|
--nextPart5630828.MhkbZ0Pkbq
|
|
Content-Transfer-Encoding: base64
|
|
Content-Type: text/plain; charset="UTF-8"
|
|
|
|
UHJvc3TDvSB0ZXh0Lg==
|
|
|
|
|
|
--nextPart5630828.MhkbZ0Pkbq
|
|
Content-Transfer-Encoding: quoted-printable
|
|
Content-Type: text/html; charset="UTF-8"
|
|
|
|
<html>
|
|
<head>
|
|
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF-8">
|
|
</head>
|
|
<body><p style=3D"margin-top:0;margin-bottom:0;margin-left:0;margin-right:0=
|
|
;"><strong>Tu=C4=8Dn=C3=BD HTML text</strong></p>
|
|
</body>
|
|
</html>
|
|
--nextPart5630828.MhkbZ0Pkbq--
|
|
|
|
--nextPart1698715.VLH7GnMWUR
|
|
Content-Disposition: attachment; filename="test.txt"
|
|
Content-Transfer-Encoding: base64
|
|
Content-Type: text/plain; charset="UTF-8"; name="test.txt"
|
|
|
|
TsSbamFrw6EgcMWZw61sb2hhLgo=
|
|
|
|
|
|
--nextPart1698715.VLH7GnMWUR--
|
|
"#;
|
|
|
|
#[test]
|
|
fn test_plain_text_body() -> Result<(), anyhow::Error> {
|
|
let message = mailparse::parse_mail(TEXT_PLAIN.as_bytes())?;
|
|
assert_eq!(message.subject(), Some("Plain text body".to_string()));
|
|
assert_eq!(
|
|
message.date()?,
|
|
Some(
|
|
chrono::FixedOffset::east(3600 * 2)
|
|
.ymd(2021, 7, 17)
|
|
.and_hms(18, 4, 10),
|
|
)
|
|
);
|
|
assert_eq!(
|
|
message.body()?,
|
|
Some(Body {
|
|
text: Some("Prostý text.".to_string())
|
|
}),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multipart_alternative_text_body() -> Result<(), anyhow::Error> {
|
|
let message = mailparse::parse_mail(MULTIPART_ALTERNATIVE.as_bytes())?;
|
|
assert_eq!(message.subject(), Some("Plain text with HTML".to_string()));
|
|
assert_eq!(
|
|
message.date()?,
|
|
Some(
|
|
chrono::FixedOffset::east(3600 * 2)
|
|
.ymd(2021, 7, 17)
|
|
.and_hms(18, 13, 26),
|
|
)
|
|
);
|
|
assert_eq!(
|
|
message.body()?,
|
|
Some(Body {
|
|
text: Some("Prostý text.".to_string())
|
|
}),
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_multipart_mixed_alternative_text_body() -> Result<(), anyhow::Error> {
|
|
let message = mailparse::parse_mail(MULTIPART_MIXED_ALTERNATIVE.as_bytes())?;
|
|
assert_eq!(
|
|
message.subject(),
|
|
Some("Plain text with HTML and attachment".to_string())
|
|
);
|
|
assert_eq!(
|
|
message.date()?,
|
|
Some(
|
|
chrono::FixedOffset::east(3600 * 2)
|
|
.ymd(2021, 7, 17)
|
|
.and_hms(21, 39, 8),
|
|
)
|
|
);
|
|
assert_eq!(
|
|
message.body()?,
|
|
Some(Body {
|
|
text: Some("Prostý text.".to_string())
|
|
}),
|
|
);
|
|
Ok(())
|
|
}
|
|
}
|