Skip to content

Commit d88bc20

Browse files
committed
Improved API.
1 parent 3f99761 commit d88bc20

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+3744
-4042
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
mail-parser 0.4.0
2+
================================
3+
- Lazy conversion to/from HTML an plain text parts.
4+
- Improved API.
5+
- Parts are now generics.
6+
17
mail-parser 0.3.1
28
================================
39
- Support for non-standard headers.

Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "mail-parser"
33
description = "Fast and robust e-mail parsing library for Rust"
4-
version = "0.3.1"
4+
version = "0.4.0"
55
edition = "2018"
66
authors = [ "Stalwart Labs <hello@stalw.art>"]
77
license = "Apache-2.0 OR MIT"

README.md

+51-49
Original file line numberDiff line numberDiff line change
@@ -35,42 +35,46 @@ Performance and memory safety were two important factors while designing _mail-p
3535
## Usage Example
3636

3737
```rust
38-
let input = concat!(
39-
"From: Art Vandelay <art@vandelay.com> (Vandelay Industries)\n",
40-
"To: \"Colleagues\": \"James Smythe\" <james@vandelay.com>; Friends:\n",
41-
" jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;\n",
42-
"Date: Sat, 20 Nov 2021 14:22:01 -0800\n",
43-
"Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?=\n",
44-
"Content-Type: multipart/mixed; boundary=\"festivus\";\n\n",
45-
"--festivus\n",
46-
"Content-Type: text/html; charset=\"us-ascii\"\n",
47-
"Content-Transfer-Encoding: base64\n\n",
48-
"PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle\n",
49-
"HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm\n",
50-
"cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8\n",
51-
"gJiN4MjYzQTs8L3A+PC9odG1sPg==\n",
52-
"--festivus\n",
53-
"Content-Type: message/rfc822\n\n",
54-
"From: \"Cosmo Kramer\" <kramer@kramerica.com>\n",
55-
"Subject: Exporting my book about coffee tables\n",
56-
"Content-Type: multipart/mixed; boundary=\"giddyup\";\n\n",
57-
"--giddyup\n",
58-
"Content-Type: text/plain; charset=\"utf-16\"\n",
59-
"Content-Transfer-Encoding: quoted-printable\n\n",
60-
"=FF=FE=0C!5=D8\"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8\"=DD =005=D8\"=\n",
61-
"=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD =\n",
62-
"=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8\"=\n",
63-
"=DD5=D8=1E=DD5=D80=DD5=D8\"=DD!=00\n",
64-
"--giddyup\n",
65-
"Content-Type: image/gif; name*1=\"about \"; name*0=\"Book \";\n",
66-
" name*2*=utf-8''%e2%98%95 tables.gif\n",
67-
"Content-Transfer-Encoding: Base64\n",
68-
"Content-Disposition: attachment\n\n",
69-
"R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\n",
70-
"--giddyup--\n",
71-
"--festivus--\n",
72-
)
73-
.as_bytes();
38+
let input = br#"From: Art Vandelay <art@vandelay.com> (Vandelay Industries)
39+
To: "Colleagues": "James Smythe" <james@vandelay.com>; Friends:
40+
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;
41+
Date: Sat, 20 Nov 2021 14:22:01 -0800
42+
Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?=
43+
Content-Type: multipart/mixed; boundary="festivus";
44+
45+
--festivus
46+
Content-Type: text/html; charset="us-ascii"
47+
Content-Transfer-Encoding: base64
48+
49+
PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle
50+
HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm
51+
cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8
52+
gJiN4MjYzQTs8L3A+PC9odG1sPg==
53+
--festivus
54+
Content-Type: message/rfc822
55+
56+
From: "Cosmo Kramer" <kramer@kramerica.com>
57+
Subject: Exporting my book about coffee tables
58+
Content-Type: multipart/mixed; boundary="giddyup";
59+
60+
--giddyup
61+
Content-Type: text/plain; charset="utf-16"
62+
Content-Transfer-Encoding: quoted-printable
63+
64+
=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"=
65+
=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD =
66+
=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"=
67+
=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00
68+
--giddyup
69+
Content-Type: image/gif; name*1="about "; name*0="Book ";
70+
name*2*=utf-8''%e2%98%95 tables.gif
71+
Content-Transfer-Encoding: Base64
72+
Content-Disposition: attachment
73+
74+
R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
75+
--giddyup--
76+
--festivus--
77+
"#;
7478

7579
let message = Message::parse(input).unwrap();
7680

@@ -119,9 +123,9 @@ Performance and memory safety were two important factors while designing _mail-p
119123
"Why not both importing AND exporting? ☺"
120124
);
121125

122-
// HTML and text body parts are returned conforming to RFC8621, Section 4.1.4
126+
// HTML and text body parts are returned conforming to RFC8621, Section 4.1.4
123127
assert_eq!(
124-
message.get_html_body(0).unwrap().to_string(),
128+
message.get_html_body(0).unwrap(),
125129
concat!(
126130
"<html><p>I was thinking about quitting the &ldquo;exporting&rdquo; to ",
127131
"focus just on the &ldquo;importing&rdquo;,</p><p>but then I thought,",
@@ -131,18 +135,19 @@ Performance and memory safety were two important factors while designing _mail-p
131135

132136
// HTML parts are converted to plain text (and viceversa) when missing
133137
assert_eq!(
134-
message.get_text_body(0).unwrap().to_string(),
138+
message.get_text_body(0).unwrap(),
135139
concat!(
136140
"I was thinking about quitting the “exporting” to focus just on the",
137141
" “importing”,\nbut then I thought, why not do both? ☺\n"
138142
)
139143
);
140144

141145
// Supports nested messages as well as multipart/digest
142-
let nested_message = match message.get_attachment(0).unwrap() {
143-
MessagePart::Message(v) => v,
144-
_ => unreachable!(),
145-
};
146+
let nested_message = message
147+
.get_attachment(0)
148+
.unwrap()
149+
.unwrap_message()
150+
.get_body();
146151

147152
assert_eq!(
148153
nested_message.get_subject().unwrap(),
@@ -151,18 +156,15 @@ Performance and memory safety were two important factors while designing _mail-p
151156

152157
// Handles UTF-* as well as many legacy encodings
153158
assert_eq!(
154-
nested_message.get_text_body(0).unwrap().to_string(),
159+
nested_message.get_text_body(0).unwrap(),
155160
"ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨 𝔭𝔩𝔢𝔞𝔰𝔢!"
156161
);
157162
assert_eq!(
158-
nested_message.get_html_body(0).unwrap().to_string(),
163+
nested_message.get_html_body(0).unwrap(),
159164
"<html><body>ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨 𝔭𝔩𝔢𝔞𝔰𝔢!</body></html>"
160165
);
161166

162-
let nested_attachment = match nested_message.get_attachment(0).unwrap() {
163-
MessagePart::Binary(v) => v,
164-
_ => unreachable!(),
165-
};
167+
let nested_attachment = nested_message.get_attachment(0).unwrap().unwrap_binary();
166168

167169
assert_eq!(nested_attachment.len(), 42);
168170

examples/email_to_json_and_yaml.rs examples/export_to_json_and_yaml.rs

+10-12
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
102102

103103
// HTML and text body parts are returned conforming to RFC8621, Section 4.1.4
104104
assert_eq!(
105-
message.get_html_body(0).unwrap().to_string(),
105+
message.get_html_body(0).unwrap(),
106106
concat!(
107107
"<html><p>I was thinking about quitting the &ldquo;exporting&rdquo; to ",
108108
"focus just on the &ldquo;importing&rdquo;,</p><p>but then I thought,",
@@ -112,18 +112,19 @@ R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
112112

113113
// HTML parts are converted to plain text (and viceversa) when missing
114114
assert_eq!(
115-
message.get_text_body(0).unwrap().to_string(),
115+
message.get_text_body(0).unwrap(),
116116
concat!(
117117
"I was thinking about quitting the “exporting” to focus just on the",
118118
" “importing”,\nbut then I thought, why not do both? ☺\n"
119119
)
120120
);
121121

122122
// Supports nested messages as well as multipart/digest
123-
let nested_message = match message.get_attachment(0).unwrap() {
124-
MessagePart::Message(v) => v,
125-
_ => unreachable!(),
126-
};
123+
let nested_message = message
124+
.get_attachment(0)
125+
.unwrap()
126+
.unwrap_message()
127+
.get_body();
127128

128129
assert_eq!(
129130
nested_message.get_subject().unwrap(),
@@ -132,18 +133,15 @@ R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
132133

133134
// Handles UTF-* as well as many legacy encodings
134135
assert_eq!(
135-
nested_message.get_text_body(0).unwrap().to_string(),
136+
nested_message.get_text_body(0).unwrap(),
136137
"ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨 𝔭𝔩𝔢𝔞𝔰𝔢!"
137138
);
138139
assert_eq!(
139-
nested_message.get_html_body(0).unwrap().to_string(),
140+
nested_message.get_html_body(0).unwrap(),
140141
"<html><body>ℌ𝔢𝔩𝔭 𝔪𝔢 𝔢𝔵𝔭𝔬𝔯𝔱 𝔪𝔶 𝔟𝔬𝔬𝔨 𝔭𝔩𝔢𝔞𝔰𝔢!</body></html>"
141142
);
142143

143-
let nested_attachment = match nested_message.get_attachment(0).unwrap() {
144-
MessagePart::Binary(v) => v,
145-
_ => unreachable!(),
146-
};
144+
let nested_attachment = nested_message.get_attachment(0).unwrap().unwrap_binary();
147145

148146
assert_eq!(nested_attachment.len(), 42);
149147

examples/write_attachments.rs

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright Stalwart Labs, Minter Ltd. See the COPYING
3+
* file at the top-level directory of this distribution.
4+
*
5+
* Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6+
* https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7+
* <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
8+
* option. This file may not be copied, modified, or distributed
9+
* except according to those terms.
10+
*/
11+
12+
use mail_parser::*;
13+
14+
fn main() {
15+
let input = br#"From: Art Vandelay <art@vandelay.com> (Vandelay Industries)
16+
To: "Colleagues": "James Smythe" <james@vandelay.com>; Friends:
17+
jane@example.com, =?UTF-8?Q?John_Sm=C3=AEth?= <john@example.com>;
18+
Date: Sat, 20 Nov 2021 14:22:01 -0800
19+
Subject: Why not both importing AND exporting? =?utf-8?b?4pi6?=
20+
Content-Type: multipart/mixed; boundary="festivus";
21+
22+
--festivus
23+
Content-Type: text/html; charset="us-ascii"
24+
Content-Transfer-Encoding: base64
25+
26+
PGh0bWw+PHA+SSB3YXMgdGhpbmtpbmcgYWJvdXQgcXVpdHRpbmcgdGhlICZsZHF1bztle
27+
HBvcnRpbmcmcmRxdW87IHRvIGZvY3VzIGp1c3Qgb24gdGhlICZsZHF1bztpbXBvcnRpbm
28+
cmcmRxdW87LDwvcD48cD5idXQgdGhlbiBJIHRob3VnaHQsIHdoeSBub3QgZG8gYm90aD8
29+
gJiN4MjYzQTs8L3A+PC9odG1sPg==
30+
--festivus
31+
Content-Type: message/rfc822; name="Exporting my book about coffee tables.eml"
32+
Content-Disposition: inline; filename="Exporting my book about coffee tables.eml"
33+
Content-Transfer-Encoding: 7bit
34+
35+
From: "Cosmo Kramer" <kramer@kramerica.com>
36+
Subject: Exporting my book about coffee tables
37+
Content-Type: multipart/mixed; boundary="giddyup";
38+
39+
--giddyup
40+
Content-Type: text/plain; charset="utf-16"
41+
Content-Transfer-Encoding: quoted-printable
42+
43+
=FF=FE=0C!5=D8"=DD5=D8)=DD5=D8-=DD =005=D8*=DD5=D8"=DD =005=D8"=
44+
=DD5=D85=DD5=D8-=DD5=D8,=DD5=D8/=DD5=D81=DD =005=D8*=DD5=D86=DD =
45+
=005=D8=1F=DD5=D8,=DD5=D8,=DD5=D8(=DD =005=D8-=DD5=D8)=DD5=D8"=
46+
=DD5=D8=1E=DD5=D80=DD5=D8"=DD!=00
47+
--giddyup
48+
Content-Type: image/gif; name*1="about "; name*0="Book ";
49+
name*2*=utf-8''%e2%98%95 tables.gif
50+
Content-Transfer-Encoding: Base64
51+
Content-Disposition: attachment
52+
53+
R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
54+
--giddyup--
55+
--festivus--
56+
"#;
57+
58+
write_attachments(&Message::parse(input).unwrap());
59+
}
60+
61+
fn write_attachments(message: &Message) {
62+
for attachment in message.get_attachments() {
63+
match attachment {
64+
MessagePart::Text(text) | MessagePart::Html(text) => write_part(text),
65+
MessagePart::Binary(blob) | MessagePart::InlineBinary(blob) => write_part(blob),
66+
MessagePart::Message(message) => {
67+
write_part(message);
68+
write_attachments(message.get_body());
69+
}
70+
_ => (),
71+
}
72+
}
73+
}
74+
75+
fn write_part<'x>(part: &'x impl BodyPart<'x>) {
76+
std::fs::write(
77+
part.get_content_disposition()
78+
.and_then(|cd| cd.get_attribute("filename"))
79+
.unwrap_or_else(|| {
80+
part.get_content_type()
81+
.and_then(|ct| ct.get_attribute("name"))
82+
.unwrap_or("Untitled")
83+
}),
84+
part.get_contents(),
85+
)
86+
.unwrap();
87+
}

0 commit comments

Comments
 (0)