Skip to content

Commit 407a42e

Browse files
committed
indicate in wrapper text whether there's an inline attachment or not
1 parent 37891cb commit 407a42e

File tree

1 file changed

+86
-81
lines changed

1 file changed

+86
-81
lines changed

src/main.rs

Lines changed: 86 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,84 @@ fn main() {
112112
},
113113
OriginalMessageBody::Error(_) => None,
114114
};
115-
tracing::debug!(could_parse = original_parsed.is_some(), "parsed message");
115+
tracing::debug!(
116+
could_parse = original_parsed.is_some(),
117+
?original_parsed,
118+
"parsed message"
119+
);
120+
121+
// Try to create an inline attachment for the receivers's convenience of not
122+
// having to double-click the attachment.
123+
//
124+
// This is surprisingly tricky, as the message/rfc822 MIME type only allows
125+
// Content-Transfer-Encoding 7bit, 8bit or binary.
126+
// Any other encoding (quoted-printable, base64) will break in
127+
// Gmail and AppleMail, probably elsewhere. The exact kind of breakage depends:
128+
// in AppleMail, only the `From`, `To`, and `Subject`
129+
// headers are shown inline, and the rest of the message is not visible / accessible.
130+
// In Gmail, it always shows as an attachment and one gets an error when clicking on it.
131+
let re_encoded = (|| {
132+
let Some(original_parsed) = &original_parsed else {
133+
debug!("not parseable");
134+
return None;
135+
};
136+
if original_parsed.ctype.mimetype != "text/plain" {
137+
// TODO: implement support.
138+
// Multi-part would be tricky as we'd possible need to use different
139+
// boundaries to avoid collisions with the boundaries that our wrapper
140+
// message will add.
141+
debug!("not text/plain content-type");
142+
return None;
143+
}
144+
let mut builder = SinglePart::builder();
145+
for header in &original_parsed.headers {
146+
#[derive(Clone)]
147+
struct RawHeader(HeaderName, String);
148+
impl lettre::message::header::Header for RawHeader {
149+
fn name() -> HeaderName {
150+
unimplemented!("not needed, we only use display")
151+
}
152+
153+
fn parse(_: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
154+
unimplemented!("not needed, we only use display")
155+
}
156+
157+
fn display(&self) -> lettre::message::header::HeaderValue {
158+
HeaderValue::new(self.0.clone(), self.1.clone())
159+
}
160+
}
161+
impl RawHeader {
162+
fn new(hdr: &mailparse::MailHeader) -> Option<Self> {
163+
let header_name =
164+
HeaderName::new_from_ascii(hdr.get_key()).ok().or_else(|| {
165+
debug!(hdr=?hdr.get_key(), "header is not ascii");
166+
None
167+
})?;
168+
let header_value = hdr.get_value_utf8().ok().or_else(|| {
169+
debug!(hdr=?hdr, "header value is not utf-8");
170+
None
171+
})?;
172+
Some(Self(header_name, header_value))
173+
}
174+
}
175+
builder = builder.header(RawHeader::new(header).or_else(|| {
176+
debug!("can't adapt libraries into each other");
177+
None
178+
})?);
179+
}
180+
Some(
181+
builder.body(
182+
Body::new_with_encoding(
183+
original_parsed.get_body().ok().or_else(|| {
184+
debug!("cannot get body");
185+
None
186+
})?,
187+
lettre::message::header::ContentTransferEncoding::Base64,
188+
)
189+
.unwrap(),
190+
),
191+
)
192+
})();
116193

117194
// Put together the wrapper message
118195
let sender = {
@@ -209,10 +286,14 @@ fn main() {
209286
writeln!(&mut body, "WARNING: could not determine permissions of the config file, they may or may not be too lax: {e}")?;
210287
},
211288
}
212-
writeln!(
213-
&mut body,
214-
"The original message is attached inline to this wrapper message."
215-
)?;
289+
writeln!(&mut body)?;
290+
{
291+
write!(&mut body, "The original message is attached to this wrapper message.")?;
292+
if re_encoded.is_some() {
293+
write!(&mut body, " For convenience, a re-encoded copy is attached inline.")?;
294+
}
295+
writeln!(&mut body)?;
296+
}
216297
writeln!(&mut body)?;
217298
writeln!(&mut body, "Invocation args: {args}")?;
218299
writeln!(&mut body)?;
@@ -261,83 +342,7 @@ fn main() {
261342
.multipart({
262343
let mut mp_builder = MultiPart::mixed().singlepart(SinglePart::plain(body));
263344

264-
// Try to create an inline attachment for the receivers's convenience of not
265-
// having to double-click the attachment.
266-
//
267-
// This is surprisingly tricky, as the message/rfc822 MIME type only allows
268-
// Content-Transfer-Encoding 7bit, 8bit or binary.
269-
// Any other encoding (quoted-printable, base64) will break in
270-
// Gmail and AppleMail, probably elsewhere. The exact kind of breakage depends:
271-
// in AppleMail, only the `From`, `To`, and `Subject`
272-
// headers are shown inline, and the rest of the message is not visible / accessible.
273-
// In Gmail, it always shows as an attachment and one gets an error when clicking on it.
274-
//
275-
// So, try to re-encode the message body. If that doesn't work, the user can fallback
276-
// to the attachment.
277345
mp_builder = {
278-
let re_encoded = (|| {
279-
let Some(original_parsed) = original_parsed else {
280-
debug!("not parseable");
281-
return None;
282-
};
283-
if original_parsed.ctype.mimetype != "text/plain" {
284-
debug!("not text/plain content-type");
285-
return None;
286-
}
287-
let mut builder = SinglePart::builder();
288-
for header in &original_parsed.headers {
289-
#[derive(Clone)]
290-
struct RawHeader(HeaderName, String);
291-
impl lettre::message::header::Header for RawHeader {
292-
fn name() -> HeaderName {
293-
unimplemented!("not needed, we only use display")
294-
}
295-
296-
fn parse(
297-
_: &str,
298-
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>>
299-
{
300-
unimplemented!("not needed, we only use display")
301-
}
302-
303-
fn display(&self) -> lettre::message::header::HeaderValue {
304-
HeaderValue::new(self.0.clone(), self.1.clone())
305-
}
306-
}
307-
impl RawHeader {
308-
fn new(hdr: &mailparse::MailHeader) -> Option<Self> {
309-
let header_name = HeaderName::new_from_ascii(hdr.get_key())
310-
.ok()
311-
.or_else(|| {
312-
debug!(hdr=?hdr.get_key(), "header is not ascii");
313-
None
314-
})?;
315-
let header_value = hdr.get_value_utf8().ok().or_else(|| {
316-
debug!(hdr=?hdr, "header value is not utf-8");
317-
None
318-
})?;
319-
Some(Self(header_name, header_value))
320-
}
321-
}
322-
builder = builder.header(RawHeader::new(header).or_else(|| {
323-
debug!("can't adapt libraries into each other");
324-
None
325-
})?);
326-
}
327-
Some(
328-
builder.body(
329-
Body::new_with_encoding(
330-
original_parsed.get_body().ok().or_else(|| {
331-
debug!("cannot get body");
332-
None
333-
})?,
334-
lettre::message::header::ContentTransferEncoding::Base64,
335-
)
336-
.unwrap(),
337-
),
338-
)
339-
})();
340-
341346
if let Some(re_encoded) = re_encoded {
342347
mp_builder.singlepart(
343348
SinglePart::builder()

0 commit comments

Comments
 (0)