1use std::io::{self, Write};
7
8use crate::commit_encoding::{decode_bytes, reencode_utf8_to_label};
9use crate::config::ConfigSet;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
13pub enum QuotedCrAction {
14 #[default]
15 Warn,
16 NoWarn,
17 Strip,
18}
19
20impl QuotedCrAction {
21 #[must_use]
22 pub fn parse(s: &str) -> Option<Self> {
23 match s {
24 "warn" => Some(Self::Warn),
25 "nowarn" => Some(Self::NoWarn),
26 "strip" => Some(Self::Strip),
27 _ => None,
28 }
29 }
30}
31
32#[derive(Debug, Clone)]
35pub struct MailinfoOptions {
36 pub keep_subject: bool,
37 pub keep_non_patch_brackets_in_subject: bool,
38 pub add_message_id: bool,
39 pub metainfo_charset: Option<String>,
40 pub use_scissors: bool,
41 pub use_inbody_headers: bool,
42 pub quoted_cr: QuotedCrAction,
43}
44
45impl Default for MailinfoOptions {
46 fn default() -> Self {
47 Self {
48 keep_subject: false,
49 keep_non_patch_brackets_in_subject: false,
50 add_message_id: false,
51 metainfo_charset: Some("utf-8".to_string()),
52 use_scissors: false,
53 use_inbody_headers: true,
54 quoted_cr: QuotedCrAction::Warn,
55 }
56 }
57}
58
59pub fn apply_mailinfo_config(cfg: &ConfigSet, opts: &mut MailinfoOptions) {
61 if let Some(v) = cfg.get("mailinfo.scissors") {
62 if let Ok(b) = parse_bool_loose(v.as_str()) {
63 opts.use_scissors = b;
64 }
65 }
66 if let Some(v) = cfg
67 .get("mailinfo.quotedcr")
68 .or_else(|| cfg.get("mailinfo.quotedCr"))
69 {
70 if let Some(a) = QuotedCrAction::parse(v.trim()) {
71 opts.quoted_cr = a;
72 }
73 }
74}
75
76fn parse_bool_loose(s: &str) -> Result<bool, ()> {
77 match s.trim().to_ascii_lowercase().as_str() {
78 "true" | "yes" | "on" | "1" => Ok(true),
79 "false" | "no" | "off" | "0" => Ok(false),
80 _ => Err(()),
81 }
82}
83
84struct Input<'a> {
85 bytes: &'a [u8],
86 pos: usize,
87}
88
89impl<'a> Input<'a> {
90 fn new(bytes: &'a [u8]) -> Self {
91 Self { bytes, pos: 0 }
92 }
93
94 fn eof(&self) -> bool {
95 self.pos >= self.bytes.len()
96 }
97
98 fn skip_leading_ws(&mut self) {
99 while self
100 .bytes
101 .get(self.pos)
102 .is_some_and(|b| b.is_ascii_whitespace())
103 {
104 self.pos += 1;
105 }
106 }
107
108 fn peek(&self) -> Option<u8> {
109 self.bytes.get(self.pos).copied()
110 }
111
112 fn read_line(&mut self, out: &mut Vec<u8>) -> io::Result<bool> {
114 out.clear();
115 if self.pos >= self.bytes.len() {
116 return Ok(false);
117 }
118 let start = self.pos;
119 while self.pos < self.bytes.len() {
120 let b = self.bytes[self.pos];
121 self.pos += 1;
122 out.push(b);
123 if b == b'\n' {
124 break;
125 }
126 }
127 trim_end_crlf(out);
128 Ok(self.pos > start)
129 }
130
131 fn read_line_keep_lf(&mut self, out: &mut Vec<u8>) -> io::Result<bool> {
133 out.clear();
134 if self.pos >= self.bytes.len() {
135 return Ok(false);
136 }
137 let start = self.pos;
138 while self.pos < self.bytes.len() {
139 let b = self.bytes[self.pos];
140 self.pos += 1;
141 out.push(b);
142 if b == b'\n' {
143 break;
144 }
145 }
146 if out.len() >= 2 && out[out.len() - 2] == b'\r' && out[out.len() - 1] == b'\n' {
147 out.remove(out.len() - 2);
148 }
149 Ok(self.pos > start)
150 }
151}
152
153fn trim_end_crlf(line: &mut Vec<u8>) {
154 while line.last() == Some(&b'\r') || line.last() == Some(&b'\n') {
155 line.pop();
156 }
157}
158
159fn is_rfc2822_header(line: &[u8]) -> bool {
160 if line.starts_with(b"From ") || line.starts_with(b">From ") {
161 return true;
162 }
163 let mut i = 0;
164 while i < line.len() {
165 let ch = line[i];
166 if ch == b':' {
167 return true;
168 }
169 if (33..=57).contains(&ch) || (59..=126).contains(&ch) {
170 i += 1;
171 continue;
172 }
173 break;
174 }
175 false
176}
177
178fn read_header_line(inp: &mut Input<'_>, line: &mut Vec<u8>) -> io::Result<bool> {
179 line.clear();
180 if !inp.read_line(line)? {
181 return Ok(false);
182 }
183 if line.is_empty() || !is_rfc2822_header(line) {
184 line.push(b'\n');
185 return Ok(false);
186 }
187 loop {
188 match inp.peek() {
189 Some(b' ') | Some(b'\t') => {
190 inp.pos += 1;
191 let mut cont = Vec::new();
192 if !inp.read_line(&mut cont)? {
193 break;
194 }
195 line.push(b' ');
196 line.extend_from_slice(&cont);
197 }
198 _ => break,
199 }
200 }
201 Ok(true)
202}
203
204#[derive(Default, Clone)]
205struct OptHdr(Option<String>);
206
207impl OptHdr {
208 fn set_if_empty(&mut self, v: String) {
209 if self.0.is_none() {
210 self.0 = Some(v);
211 }
212 }
213 fn clear(&mut self) {
214 self.0 = None;
215 }
216 fn as_opt(&self) -> Option<&str> {
217 self.0.as_deref()
218 }
219}
220
221#[derive(Clone, Copy, PartialEq, Eq, Default)]
222enum TE {
223 #[default]
224 DontCare,
225 Qp,
226 Base64,
227}
228
229struct Mime {
230 boundaries: Vec<Vec<u8>>,
231 charset: String,
232 te: TE,
233 format_flowed: bool,
234 delsp: bool,
235}
236
237impl Default for Mime {
238 fn default() -> Self {
239 Self {
240 boundaries: Vec::new(),
241 charset: String::new(),
242 te: TE::DontCare,
243 format_flowed: false,
244 delsp: false,
245 }
246 }
247}
248
249pub fn mailinfo(
265 input: &[u8],
266 opts: &MailinfoOptions,
267 mut msg_out: impl Write,
268 mut patch_out: impl Write,
269 mut info_out: impl Write,
270 mut stderr: impl Write,
271) -> io::Result<()> {
272 let mut inp = Input::new(input);
273 inp.skip_leading_ws();
274 if inp.eof() {
275 return Err(io::Error::new(io::ErrorKind::InvalidData, "empty patch"));
276 }
277
278 let mut p_from = OptHdr::default();
279 let mut p_subj = OptHdr::default();
280 let mut p_date = OptHdr::default();
281 let mut message_id: Option<String> = None;
282 let mut mime = Mime::default();
283 let mut header_err = false;
284
285 let mut hl = Vec::new();
286 while read_header_line(&mut inp, &mut hl)? {
287 let ls = String::from_utf8_lossy(&hl).into_owned();
288 if !parse_rfc_header(
289 &ls,
290 opts,
291 &mut p_from,
292 &mut p_subj,
293 &mut p_date,
294 &mut mime,
295 &mut message_id,
296 ) {
297 header_err = true;
298 }
299 }
300 if header_err {
301 return Err(io::Error::new(
302 io::ErrorKind::InvalidData,
303 "mailinfo: bad header",
304 ));
305 }
306
307 let mut s_from = OptHdr::default();
308 let mut s_subj = OptHdr::default();
309 let mut s_date = OptHdr::default();
310 let mut log = Vec::new();
311 let mut patch_lines: u64 = 0;
312 let mut have_quoted_cr = false;
313 let mut body_err = false;
314
315 let mut st = BodyState {
316 filter_stage: 0,
317 header_stage: true,
318 inbody_accum: String::new(),
319 qp_carry: Vec::new(),
320 flowed_prev: Vec::new(),
321 body_done: false,
322 };
323
324 let mut line = Vec::new();
325 let mut need_first_boundary = !mime.boundaries.is_empty();
326 loop {
327 if need_first_boundary {
328 need_first_boundary = false;
329 if !find_boundary_line(&mut inp, &mime, &mut line)? {
330 flush_inbody_accum(
331 &mut st.inbody_accum,
332 opts,
333 &mut s_from,
334 &mut s_subj,
335 &mut s_date,
336 );
337 break;
338 }
339 } else if !inp.read_line_keep_lf(&mut line)? {
340 break;
341 }
342 process_body_raw_line(
343 &mut inp,
344 &mut line,
345 opts,
346 &mut mime,
347 &mut st,
348 &mut s_from,
349 &mut s_subj,
350 &mut s_date,
351 &mut message_id,
352 &mut log,
353 &mut patch_out,
354 &mut patch_lines,
355 &mut have_quoted_cr,
356 &mut body_err,
357 )?;
358 if body_err {
359 break;
360 }
361 if st.body_done {
362 break;
363 }
364 }
365
366 flush_qp_tail(
367 opts,
368 &mime,
369 &mut st,
370 &mut s_from,
371 &mut s_subj,
372 &mut s_date,
373 &mut message_id,
374 &mut log,
375 &mut patch_out,
376 &mut patch_lines,
377 &mut have_quoted_cr,
378 )?;
379 if !st.flowed_prev.is_empty() {
380 let p = std::mem::take(&mut st.flowed_prev);
381 handle_filter(
382 opts,
383 &p,
384 &mut s_from,
385 &mut s_subj,
386 &mut s_date,
387 &mut message_id,
388 &mut log,
389 &mut patch_out,
390 &mut patch_lines,
391 &mut st.filter_stage,
392 &mut st.header_stage,
393 &mut st.inbody_accum,
394 &mime,
395 )?;
396 }
397 flush_inbody_accum(
398 &mut st.inbody_accum,
399 opts,
400 &mut s_from,
401 &mut s_subj,
402 &mut s_date,
403 );
404
405 if have_quoted_cr && opts.quoted_cr == QuotedCrAction::Warn {
406 writeln!(stderr, "warning: quoted CRLF detected")?;
407 }
408 if body_err {
409 return Err(io::Error::new(
410 io::ErrorKind::InvalidData,
411 "mailinfo: bad body",
412 ));
413 }
414
415 msg_out.write_all(&log)?;
416 write_info(
417 opts,
418 patch_lines,
419 &p_from,
420 &p_subj,
421 &p_date,
422 &s_from,
423 &s_subj,
424 &s_date,
425 &mut info_out,
426 )?;
427 Ok(())
428}
429
430struct BodyState {
431 filter_stage: u8,
432 header_stage: bool,
433 inbody_accum: String,
434 qp_carry: Vec<u8>,
435 flowed_prev: Vec<u8>,
436 body_done: bool,
438}
439
440fn parse_rfc_header(
441 line: &str,
442 opts: &MailinfoOptions,
443 p_from: &mut OptHdr,
444 p_subj: &mut OptHdr,
445 p_date: &mut OptHdr,
446 mime: &mut Mime,
447 message_id: &mut Option<String>,
448) -> bool {
449 let Some(colon) = line.find(':') else {
450 return true;
451 };
452 let name = line[..colon].trim();
453 let val = line[colon + 1..].trim_start();
454
455 if name.eq_ignore_ascii_case("From") {
456 let mut v = val.to_string();
457 if decode_rfc2047(opts, &mut v).is_err() {
458 return false;
459 }
460 p_from.set_if_empty(v);
461 return true;
462 }
463 if name.eq_ignore_ascii_case("Subject") {
464 let mut v = val.to_string();
465 if decode_rfc2047(opts, &mut v).is_err() {
466 return false;
467 }
468 p_subj.set_if_empty(v);
469 return true;
470 }
471 if name.eq_ignore_ascii_case("Date") {
472 let mut v = val.to_string();
473 if decode_rfc2047(opts, &mut v).is_err() {
474 return false;
475 }
476 p_date.set_if_empty(v);
477 return true;
478 }
479 if name.eq_ignore_ascii_case("Content-Type") {
480 mime.format_flowed = has_attr_ci(line, "format=", "flowed");
481 mime.delsp = has_attr_ci(line, "delsp=", "yes");
482 if let Some(b) = slurp_attr_ci(line, "boundary=") {
483 let mut full = vec![b'-', b'-'];
484 full.extend_from_slice(b.as_bytes());
485 if mime.boundaries.len() < 5 {
486 mime.boundaries.push(full);
487 }
488 }
489 if let Some(cs) = slurp_attr_ci(line, "charset=") {
490 mime.charset = cs;
491 }
492 return true;
493 }
494 if name.eq_ignore_ascii_case("Content-Transfer-Encoding") {
495 let lower = val.to_ascii_lowercase();
496 mime.te = if lower.contains("base64") {
497 TE::Base64
498 } else if lower.contains("quoted-printable") {
499 TE::Qp
500 } else {
501 TE::DontCare
502 };
503 return true;
504 }
505 if opts.add_message_id
506 && (name.eq_ignore_ascii_case("Message-ID") || name.eq_ignore_ascii_case("Message-Id"))
507 {
508 let mut v = val.to_string();
509 if decode_rfc2047(opts, &mut v).is_err() {
510 return false;
511 }
512 *message_id = Some(v);
513 }
514 true
515}
516
517fn slurp_attr_ci(line: &str, name: &str) -> Option<String> {
518 let lower = line.to_ascii_lowercase();
519 let needle = name.to_ascii_lowercase();
520 let pos = lower.find(&needle)?;
521 let mut ap = &line[pos + needle.len()..];
522 if ap.starts_with('"') {
523 ap = &ap[1..];
524 let end = ap.find('"')?;
525 Some(ap[..end].to_string())
526 } else {
527 let end = ap.find([';', ' ', '\t']).unwrap_or(ap.len());
528 let s = ap[..end].trim();
529 (!s.is_empty()).then(|| s.to_string())
530 }
531}
532
533fn has_attr_ci(line: &str, name: &str, value: &str) -> bool {
534 slurp_attr_ci(line, name).is_some_and(|s| s.eq_ignore_ascii_case(value))
535}
536
537fn decode_rfc2047(opts: &MailinfoOptions, s: &mut String) -> Result<(), ()> {
538 let Some(out) = decode_rfc2047_inner(s, opts.metainfo_charset.as_deref()) else {
539 return Err(());
540 };
541 *s = out;
542 Ok(())
543}
544
545fn decode_rfc2047_inner(input: &str, metainfo: Option<&str>) -> Option<String> {
546 let mut out = String::new();
547 let mut pos = 0usize;
548 while let Some(rel) = input[pos..].find("=?") {
549 let ep = pos + rel;
550 let before = &input[pos..ep];
551 if !before.is_empty() {
552 let only_ws = before.chars().all(|c| c.is_whitespace());
553 if !only_ws || pos == 0 {
554 out.push_str(before);
555 }
556 }
557 let rest = &input[ep + 2..];
558 let d1 = rest.find('?')?;
559 let charset = &rest[..d1];
560 let after = &rest[d1 + 1..];
561 let d2 = after.find('?')?;
562 let enc = after[..d2].to_ascii_lowercase();
563 let after_enc = &after[d2 + 1..];
564 let end = after_enc.find("?=")?;
565 let payload = &after_enc[..end];
566 pos = ep + 2 + d1 + 1 + d2 + 1 + end + 2;
567 let bytes = match enc.as_str() {
568 "q" => decode_qp(payload, true),
569 "b" => base64_decode(payload),
570 _ => return None,
571 };
572 let mut piece = decode_bytes(Some(charset), &bytes);
573 if let Some(target) = metainfo {
574 if let Some(raw) = reencode_utf8_to_label(target, &piece) {
575 piece = decode_bytes(Some(target), &raw);
576 }
577 }
578 out.push_str(&piece);
579 }
580 out.push_str(&input[pos..]);
581 Some(out)
582}
583
584fn decode_qp(input: &str, rfc2047: bool) -> Vec<u8> {
585 let mut out = Vec::new();
586 let bytes = input.as_bytes();
587 let mut i = 0usize;
588 while i < bytes.len() {
589 let b = bytes[i];
590 if rfc2047 && b == b'_' {
591 out.push(b' ');
592 i += 1;
593 continue;
594 }
595 if b == b'=' {
596 let d1 = bytes.get(i + 1).copied();
597 if d1 == Some(b'\n') {
598 i += 2;
599 continue;
600 }
601 if d1 == Some(b'\r') && bytes.get(i + 2) == Some(&b'\n') {
602 i += 3;
603 continue;
604 }
605 if d1.is_none() || d1 == Some(b'\n') {
606 break;
607 }
608 let h1 = d1;
609 let h2 = bytes.get(i + 2).copied();
610 if let (Some(a), Some(c)) = (h1, h2) {
611 if let (Some(hi), Some(lo)) = (hex_nibble(a), hex_nibble(c)) {
612 out.push((hi << 4) | lo);
613 i += 3;
614 continue;
615 }
616 }
617 out.push(b'=');
618 i += 1;
619 continue;
620 }
621 out.push(b);
622 i += 1;
623 }
624 out
625}
626
627fn hex_nibble(b: u8) -> Option<u8> {
628 match b {
629 b'0'..=b'9' => Some(b - b'0'),
630 b'a'..=b'f' => Some(b - b'a' + 10),
631 b'A'..=b'F' => Some(b - b'A' + 10),
632 _ => None,
633 }
634}
635
636fn base64_decode(input: &str) -> Vec<u8> {
637 const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
638 let mut o = Vec::new();
639 let mut buf: u32 = 0;
640 let mut bits: u32 = 0;
641 for &byte in input.as_bytes() {
642 if byte == b'=' {
643 break;
644 }
645 if byte.is_ascii_whitespace() {
646 continue;
647 }
648 let Some(v) = T.iter().position(|&c| c == byte) else {
649 continue;
650 };
651 buf = (buf << 6) | v as u32;
652 bits += 6;
653 if bits >= 8 {
654 bits -= 8;
655 o.push((buf >> bits) as u8);
656 buf &= (1 << bits) - 1;
657 }
658 }
659 o
660}
661
662#[allow(clippy::too_many_arguments)]
663fn process_body_raw_line(
664 inp: &mut Input<'_>,
665 raw: &mut Vec<u8>,
666 opts: &MailinfoOptions,
667 mime: &mut Mime,
668 st: &mut BodyState,
669 s_from: &mut OptHdr,
670 s_subj: &mut OptHdr,
671 s_date: &mut OptHdr,
672 message_id: &mut Option<String>,
673 log: &mut Vec<u8>,
674 patch_out: &mut impl Write,
675 patch_lines: &mut u64,
676 have_quoted_cr: &mut bool,
677 body_err: &mut bool,
678) -> io::Result<()> {
679 if let Some(boundary) = mime.boundaries.last().cloned() {
680 if raw.len() >= boundary.len() && raw[..boundary.len()] == boundary[..] {
681 let after = &raw[boundary.len()..];
682 let is_closing = after.starts_with(b"--");
683 if is_closing {
684 mime.boundaries.pop();
685 if mime.boundaries.is_empty() {
686 handle_filter(
687 opts,
688 b"\n",
689 s_from,
690 s_subj,
691 s_date,
692 message_id,
693 log,
694 patch_out,
695 patch_lines,
696 &mut st.filter_stage,
697 &mut st.header_stage,
698 &mut st.inbody_accum,
699 mime,
700 )?;
701 }
702 loop {
703 if !find_boundary_line(inp, mime, raw)? {
704 flush_inbody_accum(&mut st.inbody_accum, opts, s_from, s_subj, s_date);
705 st.body_done = true;
706 return Ok(());
707 }
708 let Some(b) = mime.boundaries.last() else {
709 break;
710 };
711 if raw.len() >= b.len() + 2
712 && raw[..b.len()] == b[..]
713 && &raw[b.len()..b.len() + 2] == b"--"
714 {
715 mime.boundaries.pop();
716 if mime.boundaries.is_empty() {
717 handle_filter(
718 opts,
719 b"\n",
720 s_from,
721 s_subj,
722 s_date,
723 message_id,
724 log,
725 patch_out,
726 patch_lines,
727 &mut st.filter_stage,
728 &mut st.header_stage,
729 &mut st.inbody_accum,
730 mime,
731 )?;
732 }
733 continue;
734 }
735 break;
736 }
737 } else {
738 *have_quoted_cr = false;
739 }
740 mime.te = TE::DontCare;
741 mime.charset.clear();
742 mime.format_flowed = false;
743 mime.delsp = false;
744 read_part_headers(inp, mime, raw, opts, body_err)?;
745 if *body_err {
746 return Ok(());
747 }
748 if !inp.read_line_keep_lf(raw)? {
749 flush_inbody_accum(&mut st.inbody_accum, opts, s_from, s_subj, s_date);
750 st.body_done = true;
751 return Ok(());
752 }
753 }
754 }
755
756 if st.body_done {
757 return Ok(());
758 }
759
760 let mut decoded = raw.clone();
761 match mime.te {
762 TE::Qp => {
763 let s = String::from_utf8_lossy(&decoded).into_owned();
764 decoded = decode_qp(&s, false);
765 }
766 TE::Base64 => {
767 let s: String = String::from_utf8_lossy(&decoded)
768 .chars()
769 .filter(|c| !c.is_ascii_whitespace())
770 .collect();
771 decoded = base64_decode(&s);
772 }
773 TE::DontCare => {}
774 }
775
776 match mime.te {
777 TE::Qp | TE::Base64 => {
778 st.qp_carry.extend_from_slice(&decoded);
779 split_qp_carry(
780 opts,
781 mime,
782 st,
783 s_from,
784 s_subj,
785 s_date,
786 message_id,
787 log,
788 patch_out,
789 patch_lines,
790 have_quoted_cr,
791 )?;
792 }
793 TE::DontCare => {
794 if !decoded.ends_with(b"\n") && !decoded.is_empty() {
795 decoded.push(b'\n');
796 }
797 process_decoded_physical_line(
798 opts,
799 mime,
800 st,
801 &decoded,
802 s_from,
803 s_subj,
804 s_date,
805 message_id,
806 log,
807 patch_out,
808 patch_lines,
809 have_quoted_cr,
810 )?;
811 }
812 }
813 Ok(())
814}
815
816fn find_boundary_line(inp: &mut Input<'_>, mime: &Mime, line: &mut Vec<u8>) -> io::Result<bool> {
817 let Some(b) = mime.boundaries.last() else {
818 return Ok(false);
819 };
820 loop {
821 line.clear();
822 if !inp.read_line_keep_lf(line)? {
823 return Ok(false);
824 }
825 if line.len() >= b.len() && line[..b.len()] == b[..] {
826 return Ok(true);
827 }
828 }
829}
830
831fn read_part_headers(
832 inp: &mut Input<'_>,
833 mime: &mut Mime,
834 line: &mut Vec<u8>,
835 opts: &MailinfoOptions,
836 body_err: &mut bool,
837) -> io::Result<()> {
838 while read_header_line(inp, line)? {
839 let ls = String::from_utf8_lossy(line).into_owned();
840 let mut d1 = OptHdr::default();
841 let mut d2 = OptHdr::default();
842 let mut d3 = OptHdr::default();
843 if !parse_rfc_header(&ls, opts, &mut d1, &mut d2, &mut d3, mime, &mut None) {
844 *body_err = true;
845 return Ok(());
846 }
847 }
848 Ok(())
849}
850
851fn split_qp_carry(
852 opts: &MailinfoOptions,
853 mime: &Mime,
854 st: &mut BodyState,
855 s_from: &mut OptHdr,
856 s_subj: &mut OptHdr,
857 s_date: &mut OptHdr,
858 message_id: &mut Option<String>,
859 log: &mut Vec<u8>,
860 patch_out: &mut impl Write,
861 patch_lines: &mut u64,
862 have_quoted_cr: &mut bool,
863) -> io::Result<()> {
864 let mut start = 0;
865 let mut i = 0;
866 while i < st.qp_carry.len() {
867 if st.qp_carry[i] == b'\n' {
868 let chunk = st.qp_carry[start..=i].to_vec();
869 start = i + 1;
870 process_decoded_physical_line(
871 opts,
872 mime,
873 st,
874 &chunk,
875 s_from,
876 s_subj,
877 s_date,
878 message_id,
879 log,
880 patch_out,
881 patch_lines,
882 have_quoted_cr,
883 )?;
884 }
885 i += 1;
886 }
887 st.qp_carry.drain(..start);
888 Ok(())
889}
890
891fn flush_qp_tail(
892 opts: &MailinfoOptions,
893 mime: &Mime,
894 st: &mut BodyState,
895 s_from: &mut OptHdr,
896 s_subj: &mut OptHdr,
897 s_date: &mut OptHdr,
898 message_id: &mut Option<String>,
899 log: &mut Vec<u8>,
900 patch_out: &mut impl Write,
901 patch_lines: &mut u64,
902 have_quoted_cr: &mut bool,
903) -> io::Result<()> {
904 if st.qp_carry.is_empty() {
905 return Ok(());
906 }
907 let mut chunk = std::mem::take(&mut st.qp_carry);
908 if !chunk.ends_with(b"\n") {
909 chunk.push(b'\n');
910 }
911 process_decoded_physical_line(
912 opts,
913 mime,
914 st,
915 &chunk,
916 s_from,
917 s_subj,
918 s_date,
919 message_id,
920 log,
921 patch_out,
922 patch_lines,
923 have_quoted_cr,
924 )
925}
926
927fn process_decoded_physical_line(
928 opts: &MailinfoOptions,
929 mime: &Mime,
930 st: &mut BodyState,
931 line: &[u8],
932 s_from: &mut OptHdr,
933 s_subj: &mut OptHdr,
934 s_date: &mut OptHdr,
935 message_id: &mut Option<String>,
936 log: &mut Vec<u8>,
937 patch_out: &mut impl Write,
938 patch_lines: &mut u64,
939 have_quoted_cr: &mut bool,
940) -> io::Result<()> {
941 if mime.format_flowed {
942 handle_filter_flowed(
943 opts,
944 mime,
945 line,
946 st,
947 s_from,
948 s_subj,
949 s_date,
950 message_id,
951 log,
952 patch_out,
953 patch_lines,
954 have_quoted_cr,
955 )
956 } else {
957 let mut l = line.to_vec();
958 if l.len() >= 2 && l[l.len() - 2] == b'\r' && l[l.len() - 1] == b'\n' {
959 *have_quoted_cr = true;
960 if opts.quoted_cr == QuotedCrAction::Strip {
961 l.truncate(l.len() - 2);
962 l.push(b'\n');
963 }
964 }
965 handle_filter(
966 opts,
967 &l,
968 s_from,
969 s_subj,
970 s_date,
971 message_id,
972 log,
973 patch_out,
974 patch_lines,
975 &mut st.filter_stage,
976 &mut st.header_stage,
977 &mut st.inbody_accum,
978 mime,
979 )
980 }
981}
982
983fn bytes_to_log_text(line: &[u8], mime: &Mime) -> String {
984 let cs = (!mime.charset.trim().is_empty()).then_some(mime.charset.as_str());
985 decode_bytes(cs, line)
986}
987
988fn handle_filter_flowed(
989 opts: &MailinfoOptions,
990 mime: &Mime,
991 line: &[u8],
992 st: &mut BodyState,
993 s_from: &mut OptHdr,
994 s_subj: &mut OptHdr,
995 s_date: &mut OptHdr,
996 message_id: &mut Option<String>,
997 log: &mut Vec<u8>,
998 patch_out: &mut impl Write,
999 patch_lines: &mut u64,
1000 have_quoted_cr: &mut bool,
1001) -> io::Result<()> {
1002 let mut len = line.len();
1003 if len > 0 && line[len - 1] == b'\n' {
1004 len -= 1;
1005 if len > 0 && line[len - 1] == b'\r' {
1006 len -= 1;
1007 }
1008 }
1009
1010 if line.len() >= 3 && &line[..3] == b"-- " && len == 3 {
1011 if !st.flowed_prev.is_empty() {
1012 let p = std::mem::take(&mut st.flowed_prev);
1013 handle_filter(
1014 opts,
1015 &p,
1016 s_from,
1017 s_subj,
1018 s_date,
1019 message_id,
1020 log,
1021 patch_out,
1022 patch_lines,
1023 &mut st.filter_stage,
1024 &mut st.header_stage,
1025 &mut st.inbody_accum,
1026 mime,
1027 )?;
1028 }
1029 return handle_filter(
1030 opts,
1031 line,
1032 s_from,
1033 s_subj,
1034 s_date,
1035 message_id,
1036 log,
1037 patch_out,
1038 patch_lines,
1039 &mut st.filter_stage,
1040 &mut st.header_stage,
1041 &mut st.inbody_accum,
1042 mime,
1043 );
1044 }
1045
1046 if len > 0 && line[0] == b' ' {
1047 let mut l = line[1..].to_vec();
1048 if !l.ends_with(b"\n") {
1049 l.push(b'\n');
1050 }
1051 return process_decoded_physical_line(
1052 opts,
1053 &Mime {
1054 format_flowed: false,
1055 charset: mime.charset.clone(),
1056 ..Default::default()
1057 },
1058 st,
1059 &l,
1060 s_from,
1061 s_subj,
1062 s_date,
1063 message_id,
1064 log,
1065 patch_out,
1066 patch_lines,
1067 have_quoted_cr,
1068 );
1069 }
1070
1071 if len > 0 && line[len - 1] == b' ' {
1072 let take = len - usize::from(mime.delsp);
1073 st.flowed_prev.extend_from_slice(&line[..take]);
1074 return Ok(());
1075 }
1076
1077 let mut combined = Vec::new();
1078 combined.extend_from_slice(&st.flowed_prev);
1079 combined.extend_from_slice(&line[..len]);
1080 st.flowed_prev.clear();
1081 if !combined.ends_with(b"\n") {
1082 combined.push(b'\n');
1083 }
1084 process_decoded_physical_line(
1085 opts,
1086 &Mime {
1087 format_flowed: false,
1088 charset: mime.charset.clone(),
1089 ..Default::default()
1090 },
1091 st,
1092 &combined,
1093 s_from,
1094 s_subj,
1095 s_date,
1096 message_id,
1097 log,
1098 patch_out,
1099 patch_lines,
1100 have_quoted_cr,
1101 )
1102}
1103
1104fn handle_filter(
1105 opts: &MailinfoOptions,
1106 line: &[u8],
1107 s_from: &mut OptHdr,
1108 s_subj: &mut OptHdr,
1109 s_date: &mut OptHdr,
1110 message_id: &mut Option<String>,
1111 log: &mut Vec<u8>,
1112 patch_out: &mut impl Write,
1113 patch_lines: &mut u64,
1114 filter_stage: &mut u8,
1115 header_stage: &mut bool,
1116 inbody_accum: &mut String,
1117 mime: &Mime,
1118) -> io::Result<()> {
1119 match *filter_stage {
1120 0 => {
1121 if !handle_commit_msg(
1122 opts,
1123 line,
1124 mime,
1125 s_from,
1126 s_subj,
1127 s_date,
1128 message_id,
1129 log,
1130 header_stage,
1131 inbody_accum,
1132 )? {
1133 return Ok(());
1134 }
1135 *filter_stage = 1;
1136 patch_out.write_all(line)?;
1137 *patch_lines += 1;
1138 Ok(())
1139 }
1140 1 => {
1141 patch_out.write_all(line)?;
1142 *patch_lines += 1;
1143 Ok(())
1144 }
1145 _ => Ok(()),
1146 }
1147}
1148
1149fn handle_commit_msg(
1150 opts: &MailinfoOptions,
1151 line: &[u8],
1152 mime: &Mime,
1153 s_from: &mut OptHdr,
1154 s_subj: &mut OptHdr,
1155 s_date: &mut OptHdr,
1156 message_id: &mut Option<String>,
1157 log: &mut Vec<u8>,
1158 header_stage: &mut bool,
1159 inbody_accum: &mut String,
1160) -> io::Result<bool> {
1161 let text = bytes_to_log_text(line, mime);
1162
1163 if *header_stage {
1164 let only_ws = text.chars().all(|c| c.is_whitespace());
1165 if only_ws {
1166 if !inbody_accum.is_empty() {
1167 flush_inbody_accum(inbody_accum, opts, s_from, s_subj, s_date);
1168 *header_stage = false;
1169 }
1170 return Ok(false);
1171 }
1172 }
1173
1174 if opts.use_inbody_headers && *header_stage {
1175 *header_stage = check_inbody(opts, &text, inbody_accum, s_from, s_subj, s_date);
1176 if *header_stage {
1177 return Ok(false);
1178 }
1179 } else {
1180 *header_stage = false;
1181 }
1182
1183 if opts.use_scissors && is_scissors_line(&text) {
1184 log.clear();
1185 *header_stage = true;
1186 s_from.clear();
1187 s_subj.clear();
1188 s_date.clear();
1189 return Ok(false);
1190 }
1191
1192 if patchbreak(line) {
1193 if let Some(mid) = message_id.clone() {
1194 log.extend_from_slice(format!("Message-ID: {mid}\n").as_bytes());
1195 }
1196 return Ok(true);
1197 }
1198
1199 log.extend_from_slice(text.as_bytes());
1200 Ok(false)
1201}
1202
1203fn flush_inbody_accum(
1204 acc: &mut String,
1205 opts: &MailinfoOptions,
1206 s_from: &mut OptHdr,
1207 s_subj: &mut OptHdr,
1208 s_date: &mut OptHdr,
1209) {
1210 if acc.is_empty() {
1211 return;
1212 }
1213 let line = std::mem::take(acc);
1214 apply_inbody_line(&line, opts, s_from, s_subj, s_date);
1215}
1216
1217fn apply_inbody_line(
1218 line: &str,
1219 opts: &MailinfoOptions,
1220 s_from: &mut OptHdr,
1221 s_subj: &mut OptHdr,
1222 s_date: &mut OptHdr,
1223) {
1224 let Some(colon) = line.find(':') else {
1225 return;
1226 };
1227 let name = line[..colon].trim();
1228 let mut val = line[colon + 1..].trim_start().to_string();
1229 let _ = decode_rfc2047(opts, &mut val);
1230 if name.eq_ignore_ascii_case("From") && s_from.as_opt().is_none() {
1231 s_from.set_if_empty(val);
1232 } else if name.eq_ignore_ascii_case("Subject") && s_subj.as_opt().is_none() {
1233 s_subj.set_if_empty(val);
1234 } else if name.eq_ignore_ascii_case("Date") && s_date.as_opt().is_none() {
1235 s_date.set_if_empty(val);
1236 }
1237}
1238
1239fn inbody_header_candidate(line: &str, s_from: &OptHdr, s_subj: &OptHdr, s_date: &OptHdr) -> bool {
1240 let Some(colon) = line.find(':') else {
1241 return false;
1242 };
1243 let name = line[..colon].trim();
1244 (name.eq_ignore_ascii_case("From") && s_from.as_opt().is_none())
1245 || (name.eq_ignore_ascii_case("Subject") && s_subj.as_opt().is_none())
1246 || (name.eq_ignore_ascii_case("Date") && s_date.as_opt().is_none())
1247}
1248
1249fn check_inbody(
1250 opts: &MailinfoOptions,
1251 line: &str,
1252 accum: &mut String,
1253 s_from: &mut OptHdr,
1254 s_subj: &mut OptHdr,
1255 s_date: &mut OptHdr,
1256) -> bool {
1257 if !accum.is_empty() && (line.starts_with(' ') || line.starts_with('\t')) {
1258 if opts.use_scissors && is_scissors_line(line) {
1259 flush_inbody_accum(accum, opts, s_from, s_subj, s_date);
1260 return false;
1261 }
1262 while accum.ends_with('\n') {
1263 accum.pop();
1264 }
1265 accum.push_str(line);
1266 return true;
1267 }
1268 flush_inbody_accum(accum, opts, s_from, s_subj, s_date);
1269
1270 if line.starts_with(">From ") {
1271 let rest = &line[1..];
1272 if is_format_patch_sep(rest) {
1273 return true;
1274 }
1275 }
1276 if line.starts_with("[PATCH] ") {
1277 s_subj.set_if_empty(line.to_string());
1278 return true;
1279 }
1280 if inbody_header_candidate(line, s_from, s_subj, s_date) {
1281 accum.push_str(line);
1282 return true;
1283 }
1284 false
1285}
1286
1287fn is_format_patch_sep(line: &str) -> bool {
1288 const TAIL: &str = " Mon Sep 17 00:00:00 2001\n";
1289 if !line.starts_with("From ") || line.len() != 5 + 40 + TAIL.len() {
1290 return false;
1291 }
1292 let hex = &line[5..45];
1293 hex.chars().all(|c| c.is_ascii_hexdigit()) && &line[45..] == TAIL
1294}
1295
1296fn patchbreak(line: &[u8]) -> bool {
1297 if line.starts_with(b"diff -") || line.starts_with(b"Index: ") {
1298 return true;
1299 }
1300 if line.len() < 4 || !line.starts_with(b"---") {
1301 return false;
1302 }
1303 if line.len() > 3 && line[3] == b' ' {
1304 return line.len() > 4 && !line[4].is_ascii_whitespace();
1305 }
1306 for i in 3..line.len() {
1307 match line[i] {
1308 b'\n' => return true,
1309 b if !b.is_ascii_whitespace() => break,
1310 _ => {}
1311 }
1312 }
1313 false
1314}
1315
1316fn is_scissors_line(line: &str) -> bool {
1317 let mut scissors = 0;
1318 let mut gap = 0;
1319 let mut first_nb = None;
1320 let mut last_nb = None;
1321 let mut perforation: usize = 0;
1322 let mut in_perf = false;
1323 let c: Vec<char> = line.chars().collect();
1324 let mut i = 0;
1325 while i < c.len() {
1326 let ch = c[i];
1327 if ch.is_whitespace() {
1328 if in_perf {
1329 perforation += 1;
1330 gap += 1;
1331 }
1332 i += 1;
1333 continue;
1334 }
1335 last_nb = Some(i);
1336 if first_nb.is_none() {
1337 first_nb = Some(i);
1338 }
1339 if ch == '-' {
1340 in_perf = true;
1341 perforation += 1;
1342 i += 1;
1343 continue;
1344 }
1345 let rest: String = c[i..].iter().collect();
1346 if rest.starts_with(">8")
1347 || rest.starts_with("8<")
1348 || rest.starts_with(">%")
1349 || rest.starts_with("%<")
1350 {
1351 in_perf = true;
1352 perforation += 2;
1353 scissors += 2;
1354 i += 2;
1355 continue;
1356 }
1357 in_perf = false;
1358 i += 1;
1359 }
1360 let visible = match (first_nb, last_nb) {
1361 (Some(a), Some(b)) => b - a + 1,
1362 _ => 0,
1363 };
1364 scissors > 0 && visible >= 8 && visible < perforation.saturating_mul(3) && gap * 2 < perforation
1365}
1366
1367fn write_info(
1368 opts: &MailinfoOptions,
1369 patch_lines: u64,
1370 p_from: &OptHdr,
1371 p_subj: &OptHdr,
1372 p_date: &OptHdr,
1373 s_from: &OptHdr,
1374 s_subj: &OptHdr,
1375 s_date: &OptHdr,
1376 out: &mut impl Write,
1377) -> io::Result<()> {
1378 for (hdr, val) in [
1379 (
1380 "From",
1381 if patch_lines > 0 && s_from.as_opt().is_some() {
1382 s_from.as_opt()
1383 } else {
1384 p_from.as_opt()
1385 },
1386 ),
1387 (
1388 "Subject",
1389 if patch_lines > 0 && s_subj.as_opt().is_some() {
1390 s_subj.as_opt()
1391 } else {
1392 p_subj.as_opt()
1393 },
1394 ),
1395 (
1396 "Date",
1397 if patch_lines > 0 && s_date.as_opt().is_some() {
1398 s_date.as_opt()
1399 } else {
1400 p_date.as_opt()
1401 },
1402 ),
1403 ] {
1404 let Some(val) = val else { continue };
1405 if val.as_bytes().contains(&0) {
1406 return Err(io::Error::new(io::ErrorKind::InvalidData, "NUL in header"));
1407 }
1408 match hdr {
1409 "Subject" => {
1410 let mut subj = val.to_string();
1411 if !opts.keep_subject {
1412 cleanup_subject(opts, &mut subj);
1413 cleanup_space(&mut subj);
1414 }
1415 for part in subj.split('\n') {
1416 writeln!(out, "Subject: {part}")?;
1417 }
1418 }
1419 "From" => {
1420 let mut f = val.to_string();
1421 cleanup_space(&mut f);
1422 let (name, email) = handle_from(&f);
1423 writeln!(out, "Author: {name}")?;
1424 writeln!(out, "Email: {email}")?;
1425 }
1426 _ => {
1427 let mut d = val.to_string();
1428 cleanup_space(&mut d);
1429 writeln!(out, "{hdr}: {d}")?;
1430 }
1431 }
1432 }
1433 writeln!(out)?;
1434 Ok(())
1435}
1436
1437fn cleanup_space(s: &mut String) {
1438 let chs: Vec<char> = s.chars().collect();
1439 let mut out = String::new();
1440 let mut i = 0;
1441 while i < chs.len() {
1442 if chs[i].is_whitespace() {
1443 out.push(' ');
1444 i += 1;
1445 while i < chs.len() && chs[i].is_whitespace() {
1446 i += 1;
1447 }
1448 } else {
1449 out.push(chs[i]);
1450 i += 1;
1451 }
1452 }
1453 *s = out;
1454}
1455
1456fn cleanup_subject(opts: &MailinfoOptions, subject: &mut String) {
1457 let mut at = 0;
1458 while at < subject.len() {
1459 let rest = &subject[at..];
1460 let Some(ch0) = rest.chars().next() else {
1461 break;
1462 };
1463 match ch0 {
1464 'r' | 'R' => {
1465 let b = rest.as_bytes();
1466 if rest.len() >= 3 && (b[1] == b'e' || b[1] == b'E') && b[2] == b':' {
1467 subject.drain(at..at + 3);
1468 continue;
1469 }
1470 at += ch0.len_utf8();
1471 }
1472 ' ' | '\t' | ':' => {
1473 subject.remove(at);
1474 continue;
1475 }
1476 '[' => {
1477 let Some(end_rel) = rest.find(']') else {
1478 break;
1479 };
1480 let remove = end_rel + 1;
1481 let bracket = &rest[..remove];
1482 let strip = !opts.keep_non_patch_brackets_in_subject
1483 || (remove >= 7 && bracket.to_ascii_uppercase().contains("PATCH"));
1484 if strip {
1485 subject.drain(at..at + remove);
1486 } else {
1487 at += remove;
1488 if at < subject.len() && subject[at..].starts_with(|c: char| c.is_whitespace())
1489 {
1490 at += 1;
1491 }
1492 }
1493 continue;
1494 }
1495 _ => break,
1496 }
1497 }
1498 *subject = subject.trim().to_string();
1499}
1500
1501fn handle_from(from: &str) -> (String, String) {
1502 let mut f = unquote_quoted_pair(from);
1503 let Some(mut at) = f.find('@') else {
1504 return parse_bogus_from(from);
1505 };
1506 if f[at + 1..].contains('@') {
1507 return (String::new(), String::new());
1508 }
1509 let mut bytes = std::mem::take(&mut f).into_bytes();
1510 while at > 0 {
1511 let prev = bytes[at - 1];
1512 if prev.is_ascii_whitespace() {
1513 break;
1514 }
1515 if prev == b'<' {
1516 bytes[at - 1] = b' ';
1517 break;
1518 }
1519 at -= 1;
1520 }
1521 let el = bytes[at..]
1522 .iter()
1523 .take_while(|&&b| !b.is_ascii_whitespace() && b != b'>')
1524 .count();
1525 let email = String::from_utf8_lossy(&bytes[at..at + el]).into_owned();
1526 let skip = bytes
1527 .get(at + el)
1528 .filter(|&&b| b == b'>')
1529 .map(|_| 1)
1530 .unwrap_or(0);
1531 let remove = el + skip;
1532 let mut name = String::new();
1533 name.push_str(&String::from_utf8_lossy(&bytes[..at]));
1534 name.push_str(&String::from_utf8_lossy(&bytes[at + remove..]));
1535 cleanup_space(&mut name);
1536 let mut name = name.trim().to_string();
1537 if name.starts_with('(') && name.len() > 1 && name.ends_with(')') {
1538 name = name[1..name.len() - 1].to_string();
1539 }
1540 let mut disp = name.clone();
1541 if disp.is_empty()
1542 || disp.len() > 60
1543 || disp.contains('@')
1544 || disp.contains('<')
1545 || disp.contains('>')
1546 {
1547 disp.clone_from(&email);
1548 }
1549 (disp, email)
1550}
1551
1552fn parse_bogus_from(line: &str) -> (String, String) {
1553 let Some(bra) = line.find('<') else {
1554 return (String::new(), String::new());
1555 };
1556 let Some(k) = line[bra + 1..].find('>') else {
1557 return (String::new(), String::new());
1558 };
1559 let ket = bra + 1 + k;
1560 let email = line[bra + 1..ket].to_string();
1561 let mut name = line[..bra].trim().trim_matches('"').to_string();
1562 if name.is_empty()
1563 || name.len() > 60
1564 || name.contains('@')
1565 || name.contains('<')
1566 || name.contains('>')
1567 {
1568 name.clone_from(&email);
1569 }
1570 (name, email)
1571}
1572
1573fn unquote_quoted_pair(input: &str) -> String {
1574 let mut out = String::new();
1575 let mut it = input.chars().peekable();
1576 while let Some(c) = it.next() {
1577 match c {
1578 '"' => {
1579 while let Some(d) = it.next() {
1580 if d == '\\' {
1581 if let Some(e) = it.next() {
1582 out.push(e);
1583 }
1584 } else if d == '"' {
1585 break;
1586 } else {
1587 out.push(d);
1588 }
1589 }
1590 }
1591 '(' => {
1592 out.push('(');
1593 let mut depth = 1;
1594 let mut lit = false;
1595 for d in it.by_ref() {
1596 if lit {
1597 out.push(d);
1598 lit = false;
1599 continue;
1600 }
1601 if d == '\\' {
1602 lit = true;
1603 continue;
1604 }
1605 match d {
1606 '(' => {
1607 out.push('(');
1608 depth += 1;
1609 }
1610 ')' => {
1611 out.push(')');
1612 depth -= 1;
1613 if depth == 0 {
1614 break;
1615 }
1616 }
1617 _ => out.push(d),
1618 }
1619 }
1620 }
1621 _ => out.push(c),
1622 }
1623 }
1624 out
1625}