use std::io::{self, Write};
use crate::commit_encoding::{decode_bytes, reencode_utf8_to_label};
use crate::config::ConfigSet;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum QuotedCrAction {
#[default]
Warn,
NoWarn,
Strip,
}
impl QuotedCrAction {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s {
"warn" => Some(Self::Warn),
"nowarn" => Some(Self::NoWarn),
"strip" => Some(Self::Strip),
_ => None,
}
}
}
#[derive(Debug, Clone)]
pub struct MailinfoOptions {
pub keep_subject: bool,
pub keep_non_patch_brackets_in_subject: bool,
pub add_message_id: bool,
pub metainfo_charset: Option<String>,
pub use_scissors: bool,
pub use_inbody_headers: bool,
pub quoted_cr: QuotedCrAction,
}
impl Default for MailinfoOptions {
fn default() -> Self {
Self {
keep_subject: false,
keep_non_patch_brackets_in_subject: false,
add_message_id: false,
metainfo_charset: Some("utf-8".to_string()),
use_scissors: false,
use_inbody_headers: true,
quoted_cr: QuotedCrAction::Warn,
}
}
}
pub fn apply_mailinfo_config(cfg: &ConfigSet, opts: &mut MailinfoOptions) {
if let Some(v) = cfg.get("mailinfo.scissors") {
if let Ok(b) = parse_bool_loose(v.as_str()) {
opts.use_scissors = b;
}
}
if let Some(v) = cfg
.get("mailinfo.quotedcr")
.or_else(|| cfg.get("mailinfo.quotedCr"))
{
if let Some(a) = QuotedCrAction::parse(v.trim()) {
opts.quoted_cr = a;
}
}
}
fn parse_bool_loose(s: &str) -> Result<bool, ()> {
match s.trim().to_ascii_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Ok(true),
"false" | "no" | "off" | "0" => Ok(false),
_ => Err(()),
}
}
struct Input<'a> {
bytes: &'a [u8],
pos: usize,
}
impl<'a> Input<'a> {
fn new(bytes: &'a [u8]) -> Self {
Self { bytes, pos: 0 }
}
fn eof(&self) -> bool {
self.pos >= self.bytes.len()
}
fn skip_leading_ws(&mut self) {
while self
.bytes
.get(self.pos)
.is_some_and(|b| b.is_ascii_whitespace())
{
self.pos += 1;
}
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn read_line(&mut self, out: &mut Vec<u8>) -> io::Result<bool> {
out.clear();
if self.pos >= self.bytes.len() {
return Ok(false);
}
let start = self.pos;
while self.pos < self.bytes.len() {
let b = self.bytes[self.pos];
self.pos += 1;
out.push(b);
if b == b'\n' {
break;
}
}
trim_end_crlf(out);
Ok(self.pos > start)
}
fn read_line_keep_lf(&mut self, out: &mut Vec<u8>) -> io::Result<bool> {
out.clear();
if self.pos >= self.bytes.len() {
return Ok(false);
}
let start = self.pos;
while self.pos < self.bytes.len() {
let b = self.bytes[self.pos];
self.pos += 1;
out.push(b);
if b == b'\n' {
break;
}
}
if out.len() >= 2 && out[out.len() - 2] == b'\r' && out[out.len() - 1] == b'\n' {
out.remove(out.len() - 2);
}
Ok(self.pos > start)
}
}
fn trim_end_crlf(line: &mut Vec<u8>) {
while line.last() == Some(&b'\r') || line.last() == Some(&b'\n') {
line.pop();
}
}
fn is_rfc2822_header(line: &[u8]) -> bool {
if line.starts_with(b"From ") || line.starts_with(b">From ") {
return true;
}
let mut i = 0;
while i < line.len() {
let ch = line[i];
if ch == b':' {
return true;
}
if (33..=57).contains(&ch) || (59..=126).contains(&ch) {
i += 1;
continue;
}
break;
}
false
}
fn read_header_line(inp: &mut Input<'_>, line: &mut Vec<u8>) -> io::Result<bool> {
line.clear();
if !inp.read_line(line)? {
return Ok(false);
}
if line.is_empty() || !is_rfc2822_header(line) {
line.push(b'\n');
return Ok(false);
}
loop {
match inp.peek() {
Some(b' ') | Some(b'\t') => {
inp.pos += 1;
let mut cont = Vec::new();
if !inp.read_line(&mut cont)? {
break;
}
line.push(b' ');
line.extend_from_slice(&cont);
}
_ => break,
}
}
Ok(true)
}
#[derive(Default, Clone)]
struct OptHdr(Option<String>);
impl OptHdr {
fn set_if_empty(&mut self, v: String) {
if self.0.is_none() {
self.0 = Some(v);
}
}
fn clear(&mut self) {
self.0 = None;
}
fn as_opt(&self) -> Option<&str> {
self.0.as_deref()
}
}
#[derive(Clone, Copy, PartialEq, Eq, Default)]
enum TE {
#[default]
DontCare,
Qp,
Base64,
}
struct Mime {
boundaries: Vec<Vec<u8>>,
charset: String,
te: TE,
format_flowed: bool,
delsp: bool,
}
impl Default for Mime {
fn default() -> Self {
Self {
boundaries: Vec::new(),
charset: String::new(),
te: TE::DontCare,
format_flowed: false,
delsp: false,
}
}
}
pub fn mailinfo(
input: &[u8],
opts: &MailinfoOptions,
mut msg_out: impl Write,
mut patch_out: impl Write,
mut info_out: impl Write,
mut stderr: impl Write,
) -> io::Result<()> {
let mut inp = Input::new(input);
inp.skip_leading_ws();
if inp.eof() {
return Err(io::Error::new(io::ErrorKind::InvalidData, "empty patch"));
}
let mut p_from = OptHdr::default();
let mut p_subj = OptHdr::default();
let mut p_date = OptHdr::default();
let mut message_id: Option<String> = None;
let mut mime = Mime::default();
let mut header_err = false;
let mut hl = Vec::new();
while read_header_line(&mut inp, &mut hl)? {
let ls = String::from_utf8_lossy(&hl).into_owned();
if !parse_rfc_header(
&ls,
opts,
&mut p_from,
&mut p_subj,
&mut p_date,
&mut mime,
&mut message_id,
) {
header_err = true;
}
}
if header_err {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"mailinfo: bad header",
));
}
let mut s_from = OptHdr::default();
let mut s_subj = OptHdr::default();
let mut s_date = OptHdr::default();
let mut log = Vec::new();
let mut patch_lines: u64 = 0;
let mut have_quoted_cr = false;
let mut body_err = false;
let mut st = BodyState {
filter_stage: 0,
header_stage: true,
inbody_accum: String::new(),
qp_carry: Vec::new(),
flowed_prev: Vec::new(),
body_done: false,
};
let mut line = Vec::new();
let mut need_first_boundary = !mime.boundaries.is_empty();
loop {
if need_first_boundary {
need_first_boundary = false;
if !find_boundary_line(&mut inp, &mime, &mut line)? {
flush_inbody_accum(
&mut st.inbody_accum,
opts,
&mut s_from,
&mut s_subj,
&mut s_date,
);
break;
}
} else if !inp.read_line_keep_lf(&mut line)? {
break;
}
process_body_raw_line(
&mut inp,
&mut line,
opts,
&mut mime,
&mut st,
&mut s_from,
&mut s_subj,
&mut s_date,
&mut message_id,
&mut log,
&mut patch_out,
&mut patch_lines,
&mut have_quoted_cr,
&mut body_err,
)?;
if body_err {
break;
}
if st.body_done {
break;
}
}
flush_qp_tail(
opts,
&mime,
&mut st,
&mut s_from,
&mut s_subj,
&mut s_date,
&mut message_id,
&mut log,
&mut patch_out,
&mut patch_lines,
&mut have_quoted_cr,
)?;
if !st.flowed_prev.is_empty() {
let p = std::mem::take(&mut st.flowed_prev);
handle_filter(
opts,
&p,
&mut s_from,
&mut s_subj,
&mut s_date,
&mut message_id,
&mut log,
&mut patch_out,
&mut patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
&mime,
)?;
}
flush_inbody_accum(
&mut st.inbody_accum,
opts,
&mut s_from,
&mut s_subj,
&mut s_date,
);
if have_quoted_cr && opts.quoted_cr == QuotedCrAction::Warn {
writeln!(stderr, "warning: quoted CRLF detected")?;
}
if body_err {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"mailinfo: bad body",
));
}
msg_out.write_all(&log)?;
write_info(
opts,
patch_lines,
&p_from,
&p_subj,
&p_date,
&s_from,
&s_subj,
&s_date,
&mut info_out,
)?;
Ok(())
}
struct BodyState {
filter_stage: u8,
header_stage: bool,
inbody_accum: String,
qp_carry: Vec<u8>,
flowed_prev: Vec<u8>,
body_done: bool,
}
fn parse_rfc_header(
line: &str,
opts: &MailinfoOptions,
p_from: &mut OptHdr,
p_subj: &mut OptHdr,
p_date: &mut OptHdr,
mime: &mut Mime,
message_id: &mut Option<String>,
) -> bool {
let Some(colon) = line.find(':') else {
return true;
};
let name = line[..colon].trim();
let val = line[colon + 1..].trim_start();
if name.eq_ignore_ascii_case("From") {
let mut v = val.to_string();
if decode_rfc2047(opts, &mut v).is_err() {
return false;
}
p_from.set_if_empty(v);
return true;
}
if name.eq_ignore_ascii_case("Subject") {
let mut v = val.to_string();
if decode_rfc2047(opts, &mut v).is_err() {
return false;
}
p_subj.set_if_empty(v);
return true;
}
if name.eq_ignore_ascii_case("Date") {
let mut v = val.to_string();
if decode_rfc2047(opts, &mut v).is_err() {
return false;
}
p_date.set_if_empty(v);
return true;
}
if name.eq_ignore_ascii_case("Content-Type") {
mime.format_flowed = has_attr_ci(line, "format=", "flowed");
mime.delsp = has_attr_ci(line, "delsp=", "yes");
if let Some(b) = slurp_attr_ci(line, "boundary=") {
let mut full = vec![b'-', b'-'];
full.extend_from_slice(b.as_bytes());
if mime.boundaries.len() < 5 {
mime.boundaries.push(full);
}
}
if let Some(cs) = slurp_attr_ci(line, "charset=") {
mime.charset = cs;
}
return true;
}
if name.eq_ignore_ascii_case("Content-Transfer-Encoding") {
let lower = val.to_ascii_lowercase();
mime.te = if lower.contains("base64") {
TE::Base64
} else if lower.contains("quoted-printable") {
TE::Qp
} else {
TE::DontCare
};
return true;
}
if opts.add_message_id
&& (name.eq_ignore_ascii_case("Message-ID") || name.eq_ignore_ascii_case("Message-Id"))
{
let mut v = val.to_string();
if decode_rfc2047(opts, &mut v).is_err() {
return false;
}
*message_id = Some(v);
}
true
}
fn slurp_attr_ci(line: &str, name: &str) -> Option<String> {
let lower = line.to_ascii_lowercase();
let needle = name.to_ascii_lowercase();
let pos = lower.find(&needle)?;
let mut ap = &line[pos + needle.len()..];
if ap.starts_with('"') {
ap = &ap[1..];
let end = ap.find('"')?;
Some(ap[..end].to_string())
} else {
let end = ap.find([';', ' ', '\t']).unwrap_or(ap.len());
let s = ap[..end].trim();
(!s.is_empty()).then(|| s.to_string())
}
}
fn has_attr_ci(line: &str, name: &str, value: &str) -> bool {
slurp_attr_ci(line, name).is_some_and(|s| s.eq_ignore_ascii_case(value))
}
fn decode_rfc2047(opts: &MailinfoOptions, s: &mut String) -> Result<(), ()> {
let Some(out) = decode_rfc2047_inner(s, opts.metainfo_charset.as_deref()) else {
return Err(());
};
*s = out;
Ok(())
}
fn decode_rfc2047_inner(input: &str, metainfo: Option<&str>) -> Option<String> {
let mut out = String::new();
let mut pos = 0usize;
while let Some(rel) = input[pos..].find("=?") {
let ep = pos + rel;
let before = &input[pos..ep];
if !before.is_empty() {
let only_ws = before.chars().all(|c| c.is_whitespace());
if !only_ws || pos == 0 {
out.push_str(before);
}
}
let rest = &input[ep + 2..];
let d1 = rest.find('?')?;
let charset = &rest[..d1];
let after = &rest[d1 + 1..];
let d2 = after.find('?')?;
let enc = after[..d2].to_ascii_lowercase();
let after_enc = &after[d2 + 1..];
let end = after_enc.find("?=")?;
let payload = &after_enc[..end];
pos = ep + 2 + d1 + 1 + d2 + 1 + end + 2;
let bytes = match enc.as_str() {
"q" => decode_qp(payload, true),
"b" => base64_decode(payload),
_ => return None,
};
let mut piece = decode_bytes(Some(charset), &bytes);
if let Some(target) = metainfo {
if let Some(raw) = reencode_utf8_to_label(target, &piece) {
piece = decode_bytes(Some(target), &raw);
}
}
out.push_str(&piece);
}
out.push_str(&input[pos..]);
Some(out)
}
fn decode_qp(input: &str, rfc2047: bool) -> Vec<u8> {
let mut out = Vec::new();
let bytes = input.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if rfc2047 && b == b'_' {
out.push(b' ');
i += 1;
continue;
}
if b == b'=' {
let d1 = bytes.get(i + 1).copied();
if d1 == Some(b'\n') {
i += 2;
continue;
}
if d1 == Some(b'\r') && bytes.get(i + 2) == Some(&b'\n') {
i += 3;
continue;
}
if d1.is_none() || d1 == Some(b'\n') {
break;
}
let h1 = d1;
let h2 = bytes.get(i + 2).copied();
if let (Some(a), Some(c)) = (h1, h2) {
if let (Some(hi), Some(lo)) = (hex_nibble(a), hex_nibble(c)) {
out.push((hi << 4) | lo);
i += 3;
continue;
}
}
out.push(b'=');
i += 1;
continue;
}
out.push(b);
i += 1;
}
out
}
fn hex_nibble(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
fn base64_decode(input: &str) -> Vec<u8> {
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut o = Vec::new();
let mut buf: u32 = 0;
let mut bits: u32 = 0;
for &byte in input.as_bytes() {
if byte == b'=' {
break;
}
if byte.is_ascii_whitespace() {
continue;
}
let Some(v) = T.iter().position(|&c| c == byte) else {
continue;
};
buf = (buf << 6) | v as u32;
bits += 6;
if bits >= 8 {
bits -= 8;
o.push((buf >> bits) as u8);
buf &= (1 << bits) - 1;
}
}
o
}
#[allow(clippy::too_many_arguments)]
fn process_body_raw_line(
inp: &mut Input<'_>,
raw: &mut Vec<u8>,
opts: &MailinfoOptions,
mime: &mut Mime,
st: &mut BodyState,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
have_quoted_cr: &mut bool,
body_err: &mut bool,
) -> io::Result<()> {
if let Some(boundary) = mime.boundaries.last().cloned() {
if raw.len() >= boundary.len() && raw[..boundary.len()] == boundary[..] {
let after = &raw[boundary.len()..];
let is_closing = after.starts_with(b"--");
if is_closing {
mime.boundaries.pop();
if mime.boundaries.is_empty() {
handle_filter(
opts,
b"\n",
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
mime,
)?;
}
loop {
if !find_boundary_line(inp, mime, raw)? {
flush_inbody_accum(&mut st.inbody_accum, opts, s_from, s_subj, s_date);
st.body_done = true;
return Ok(());
}
let Some(b) = mime.boundaries.last() else {
break;
};
if raw.len() >= b.len() + 2
&& raw[..b.len()] == b[..]
&& &raw[b.len()..b.len() + 2] == b"--"
{
mime.boundaries.pop();
if mime.boundaries.is_empty() {
handle_filter(
opts,
b"\n",
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
mime,
)?;
}
continue;
}
break;
}
} else {
*have_quoted_cr = false;
}
mime.te = TE::DontCare;
mime.charset.clear();
mime.format_flowed = false;
mime.delsp = false;
read_part_headers(inp, mime, raw, opts, body_err)?;
if *body_err {
return Ok(());
}
if !inp.read_line_keep_lf(raw)? {
flush_inbody_accum(&mut st.inbody_accum, opts, s_from, s_subj, s_date);
st.body_done = true;
return Ok(());
}
}
}
if st.body_done {
return Ok(());
}
let mut decoded = raw.clone();
match mime.te {
TE::Qp => {
let s = String::from_utf8_lossy(&decoded).into_owned();
decoded = decode_qp(&s, false);
}
TE::Base64 => {
let s: String = String::from_utf8_lossy(&decoded)
.chars()
.filter(|c| !c.is_ascii_whitespace())
.collect();
decoded = base64_decode(&s);
}
TE::DontCare => {}
}
match mime.te {
TE::Qp | TE::Base64 => {
st.qp_carry.extend_from_slice(&decoded);
split_qp_carry(
opts,
mime,
st,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)?;
}
TE::DontCare => {
if !decoded.ends_with(b"\n") && !decoded.is_empty() {
decoded.push(b'\n');
}
process_decoded_physical_line(
opts,
mime,
st,
&decoded,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)?;
}
}
Ok(())
}
fn find_boundary_line(inp: &mut Input<'_>, mime: &Mime, line: &mut Vec<u8>) -> io::Result<bool> {
let Some(b) = mime.boundaries.last() else {
return Ok(false);
};
loop {
line.clear();
if !inp.read_line_keep_lf(line)? {
return Ok(false);
}
if line.len() >= b.len() && line[..b.len()] == b[..] {
return Ok(true);
}
}
}
fn read_part_headers(
inp: &mut Input<'_>,
mime: &mut Mime,
line: &mut Vec<u8>,
opts: &MailinfoOptions,
body_err: &mut bool,
) -> io::Result<()> {
while read_header_line(inp, line)? {
let ls = String::from_utf8_lossy(line).into_owned();
let mut d1 = OptHdr::default();
let mut d2 = OptHdr::default();
let mut d3 = OptHdr::default();
if !parse_rfc_header(&ls, opts, &mut d1, &mut d2, &mut d3, mime, &mut None) {
*body_err = true;
return Ok(());
}
}
Ok(())
}
fn split_qp_carry(
opts: &MailinfoOptions,
mime: &Mime,
st: &mut BodyState,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
have_quoted_cr: &mut bool,
) -> io::Result<()> {
let mut start = 0;
let mut i = 0;
while i < st.qp_carry.len() {
if st.qp_carry[i] == b'\n' {
let chunk = st.qp_carry[start..=i].to_vec();
start = i + 1;
process_decoded_physical_line(
opts,
mime,
st,
&chunk,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)?;
}
i += 1;
}
st.qp_carry.drain(..start);
Ok(())
}
fn flush_qp_tail(
opts: &MailinfoOptions,
mime: &Mime,
st: &mut BodyState,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
have_quoted_cr: &mut bool,
) -> io::Result<()> {
if st.qp_carry.is_empty() {
return Ok(());
}
let mut chunk = std::mem::take(&mut st.qp_carry);
if !chunk.ends_with(b"\n") {
chunk.push(b'\n');
}
process_decoded_physical_line(
opts,
mime,
st,
&chunk,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)
}
fn process_decoded_physical_line(
opts: &MailinfoOptions,
mime: &Mime,
st: &mut BodyState,
line: &[u8],
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
have_quoted_cr: &mut bool,
) -> io::Result<()> {
if mime.format_flowed {
handle_filter_flowed(
opts,
mime,
line,
st,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)
} else {
let mut l = line.to_vec();
if l.len() >= 2 && l[l.len() - 2] == b'\r' && l[l.len() - 1] == b'\n' {
*have_quoted_cr = true;
if opts.quoted_cr == QuotedCrAction::Strip {
l.truncate(l.len() - 2);
l.push(b'\n');
}
}
handle_filter(
opts,
&l,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
mime,
)
}
}
fn bytes_to_log_text(line: &[u8], mime: &Mime) -> String {
let cs = (!mime.charset.trim().is_empty()).then_some(mime.charset.as_str());
decode_bytes(cs, line)
}
fn handle_filter_flowed(
opts: &MailinfoOptions,
mime: &Mime,
line: &[u8],
st: &mut BodyState,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
have_quoted_cr: &mut bool,
) -> io::Result<()> {
let mut len = line.len();
if len > 0 && line[len - 1] == b'\n' {
len -= 1;
if len > 0 && line[len - 1] == b'\r' {
len -= 1;
}
}
if line.len() >= 3 && &line[..3] == b"-- " && len == 3 {
if !st.flowed_prev.is_empty() {
let p = std::mem::take(&mut st.flowed_prev);
handle_filter(
opts,
&p,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
mime,
)?;
}
return handle_filter(
opts,
line,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
&mut st.filter_stage,
&mut st.header_stage,
&mut st.inbody_accum,
mime,
);
}
if len > 0 && line[0] == b' ' {
let mut l = line[1..].to_vec();
if !l.ends_with(b"\n") {
l.push(b'\n');
}
return process_decoded_physical_line(
opts,
&Mime {
format_flowed: false,
charset: mime.charset.clone(),
..Default::default()
},
st,
&l,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
);
}
if len > 0 && line[len - 1] == b' ' {
let take = len - usize::from(mime.delsp);
st.flowed_prev.extend_from_slice(&line[..take]);
return Ok(());
}
let mut combined = Vec::new();
combined.extend_from_slice(&st.flowed_prev);
combined.extend_from_slice(&line[..len]);
st.flowed_prev.clear();
if !combined.ends_with(b"\n") {
combined.push(b'\n');
}
process_decoded_physical_line(
opts,
&Mime {
format_flowed: false,
charset: mime.charset.clone(),
..Default::default()
},
st,
&combined,
s_from,
s_subj,
s_date,
message_id,
log,
patch_out,
patch_lines,
have_quoted_cr,
)
}
fn handle_filter(
opts: &MailinfoOptions,
line: &[u8],
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
patch_out: &mut impl Write,
patch_lines: &mut u64,
filter_stage: &mut u8,
header_stage: &mut bool,
inbody_accum: &mut String,
mime: &Mime,
) -> io::Result<()> {
match *filter_stage {
0 => {
if !handle_commit_msg(
opts,
line,
mime,
s_from,
s_subj,
s_date,
message_id,
log,
header_stage,
inbody_accum,
)? {
return Ok(());
}
*filter_stage = 1;
patch_out.write_all(line)?;
*patch_lines += 1;
Ok(())
}
1 => {
patch_out.write_all(line)?;
*patch_lines += 1;
Ok(())
}
_ => Ok(()),
}
}
fn handle_commit_msg(
opts: &MailinfoOptions,
line: &[u8],
mime: &Mime,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
message_id: &mut Option<String>,
log: &mut Vec<u8>,
header_stage: &mut bool,
inbody_accum: &mut String,
) -> io::Result<bool> {
let text = bytes_to_log_text(line, mime);
if *header_stage {
let only_ws = text.chars().all(|c| c.is_whitespace());
if only_ws {
if !inbody_accum.is_empty() {
flush_inbody_accum(inbody_accum, opts, s_from, s_subj, s_date);
*header_stage = false;
}
return Ok(false);
}
}
if opts.use_inbody_headers && *header_stage {
*header_stage = check_inbody(opts, &text, inbody_accum, s_from, s_subj, s_date);
if *header_stage {
return Ok(false);
}
} else {
*header_stage = false;
}
if opts.use_scissors && is_scissors_line(&text) {
log.clear();
*header_stage = true;
s_from.clear();
s_subj.clear();
s_date.clear();
return Ok(false);
}
if patchbreak(line) {
if let Some(mid) = message_id.clone() {
log.extend_from_slice(format!("Message-ID: {mid}\n").as_bytes());
}
return Ok(true);
}
log.extend_from_slice(text.as_bytes());
Ok(false)
}
fn flush_inbody_accum(
acc: &mut String,
opts: &MailinfoOptions,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
) {
if acc.is_empty() {
return;
}
let line = std::mem::take(acc);
apply_inbody_line(&line, opts, s_from, s_subj, s_date);
}
fn apply_inbody_line(
line: &str,
opts: &MailinfoOptions,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
) {
let Some(colon) = line.find(':') else {
return;
};
let name = line[..colon].trim();
let mut val = line[colon + 1..].trim_start().to_string();
let _ = decode_rfc2047(opts, &mut val);
if name.eq_ignore_ascii_case("From") && s_from.as_opt().is_none() {
s_from.set_if_empty(val);
} else if name.eq_ignore_ascii_case("Subject") && s_subj.as_opt().is_none() {
s_subj.set_if_empty(val);
} else if name.eq_ignore_ascii_case("Date") && s_date.as_opt().is_none() {
s_date.set_if_empty(val);
}
}
fn inbody_header_candidate(line: &str, s_from: &OptHdr, s_subj: &OptHdr, s_date: &OptHdr) -> bool {
let Some(colon) = line.find(':') else {
return false;
};
let name = line[..colon].trim();
(name.eq_ignore_ascii_case("From") && s_from.as_opt().is_none())
|| (name.eq_ignore_ascii_case("Subject") && s_subj.as_opt().is_none())
|| (name.eq_ignore_ascii_case("Date") && s_date.as_opt().is_none())
}
fn check_inbody(
opts: &MailinfoOptions,
line: &str,
accum: &mut String,
s_from: &mut OptHdr,
s_subj: &mut OptHdr,
s_date: &mut OptHdr,
) -> bool {
if !accum.is_empty() && (line.starts_with(' ') || line.starts_with('\t')) {
if opts.use_scissors && is_scissors_line(line) {
flush_inbody_accum(accum, opts, s_from, s_subj, s_date);
return false;
}
while accum.ends_with('\n') {
accum.pop();
}
accum.push_str(line);
return true;
}
flush_inbody_accum(accum, opts, s_from, s_subj, s_date);
if line.starts_with(">From ") {
let rest = &line[1..];
if is_format_patch_sep(rest) {
return true;
}
}
if line.starts_with("[PATCH] ") {
s_subj.set_if_empty(line.to_string());
return true;
}
if inbody_header_candidate(line, s_from, s_subj, s_date) {
accum.push_str(line);
return true;
}
false
}
fn is_format_patch_sep(line: &str) -> bool {
const TAIL: &str = " Mon Sep 17 00:00:00 2001\n";
if !line.starts_with("From ") || line.len() != 5 + 40 + TAIL.len() {
return false;
}
let hex = &line[5..45];
hex.chars().all(|c| c.is_ascii_hexdigit()) && &line[45..] == TAIL
}
fn patchbreak(line: &[u8]) -> bool {
if line.starts_with(b"diff -") || line.starts_with(b"Index: ") {
return true;
}
if line.len() < 4 || !line.starts_with(b"---") {
return false;
}
if line.len() > 3 && line[3] == b' ' {
return line.len() > 4 && !line[4].is_ascii_whitespace();
}
for i in 3..line.len() {
match line[i] {
b'\n' => return true,
b if !b.is_ascii_whitespace() => break,
_ => {}
}
}
false
}
fn is_scissors_line(line: &str) -> bool {
let mut scissors = 0;
let mut gap = 0;
let mut first_nb = None;
let mut last_nb = None;
let mut perforation: usize = 0;
let mut in_perf = false;
let c: Vec<char> = line.chars().collect();
let mut i = 0;
while i < c.len() {
let ch = c[i];
if ch.is_whitespace() {
if in_perf {
perforation += 1;
gap += 1;
}
i += 1;
continue;
}
last_nb = Some(i);
if first_nb.is_none() {
first_nb = Some(i);
}
if ch == '-' {
in_perf = true;
perforation += 1;
i += 1;
continue;
}
let rest: String = c[i..].iter().collect();
if rest.starts_with(">8")
|| rest.starts_with("8<")
|| rest.starts_with(">%")
|| rest.starts_with("%<")
{
in_perf = true;
perforation += 2;
scissors += 2;
i += 2;
continue;
}
in_perf = false;
i += 1;
}
let visible = match (first_nb, last_nb) {
(Some(a), Some(b)) => b - a + 1,
_ => 0,
};
scissors > 0 && visible >= 8 && visible < perforation.saturating_mul(3) && gap * 2 < perforation
}
fn write_info(
opts: &MailinfoOptions,
patch_lines: u64,
p_from: &OptHdr,
p_subj: &OptHdr,
p_date: &OptHdr,
s_from: &OptHdr,
s_subj: &OptHdr,
s_date: &OptHdr,
out: &mut impl Write,
) -> io::Result<()> {
for (hdr, val) in [
(
"From",
if patch_lines > 0 && s_from.as_opt().is_some() {
s_from.as_opt()
} else {
p_from.as_opt()
},
),
(
"Subject",
if patch_lines > 0 && s_subj.as_opt().is_some() {
s_subj.as_opt()
} else {
p_subj.as_opt()
},
),
(
"Date",
if patch_lines > 0 && s_date.as_opt().is_some() {
s_date.as_opt()
} else {
p_date.as_opt()
},
),
] {
let Some(val) = val else { continue };
if val.as_bytes().contains(&0) {
return Err(io::Error::new(io::ErrorKind::InvalidData, "NUL in header"));
}
match hdr {
"Subject" => {
let mut subj = val.to_string();
if !opts.keep_subject {
cleanup_subject(opts, &mut subj);
cleanup_space(&mut subj);
}
for part in subj.split('\n') {
writeln!(out, "Subject: {part}")?;
}
}
"From" => {
let mut f = val.to_string();
cleanup_space(&mut f);
let (name, email) = handle_from(&f);
writeln!(out, "Author: {name}")?;
writeln!(out, "Email: {email}")?;
}
_ => {
let mut d = val.to_string();
cleanup_space(&mut d);
writeln!(out, "{hdr}: {d}")?;
}
}
}
writeln!(out)?;
Ok(())
}
fn cleanup_space(s: &mut String) {
let chs: Vec<char> = s.chars().collect();
let mut out = String::new();
let mut i = 0;
while i < chs.len() {
if chs[i].is_whitespace() {
out.push(' ');
i += 1;
while i < chs.len() && chs[i].is_whitespace() {
i += 1;
}
} else {
out.push(chs[i]);
i += 1;
}
}
*s = out;
}
fn cleanup_subject(opts: &MailinfoOptions, subject: &mut String) {
let mut at = 0;
while at < subject.len() {
let rest = &subject[at..];
let Some(ch0) = rest.chars().next() else {
break;
};
match ch0 {
'r' | 'R' => {
let b = rest.as_bytes();
if rest.len() >= 3 && (b[1] == b'e' || b[1] == b'E') && b[2] == b':' {
subject.drain(at..at + 3);
continue;
}
at += ch0.len_utf8();
}
' ' | '\t' | ':' => {
subject.remove(at);
continue;
}
'[' => {
let Some(end_rel) = rest.find(']') else {
break;
};
let remove = end_rel + 1;
let bracket = &rest[..remove];
let strip = !opts.keep_non_patch_brackets_in_subject
|| (remove >= 7 && bracket.to_ascii_uppercase().contains("PATCH"));
if strip {
subject.drain(at..at + remove);
} else {
at += remove;
if at < subject.len() && subject[at..].starts_with(|c: char| c.is_whitespace())
{
at += 1;
}
}
continue;
}
_ => break,
}
}
*subject = subject.trim().to_string();
}
fn handle_from(from: &str) -> (String, String) {
let mut f = unquote_quoted_pair(from);
let Some(mut at) = f.find('@') else {
return parse_bogus_from(from);
};
if f[at + 1..].contains('@') {
return (String::new(), String::new());
}
let mut bytes = std::mem::take(&mut f).into_bytes();
while at > 0 {
let prev = bytes[at - 1];
if prev.is_ascii_whitespace() {
break;
}
if prev == b'<' {
bytes[at - 1] = b' ';
break;
}
at -= 1;
}
let el = bytes[at..]
.iter()
.take_while(|&&b| !b.is_ascii_whitespace() && b != b'>')
.count();
let email = String::from_utf8_lossy(&bytes[at..at + el]).into_owned();
let skip = bytes
.get(at + el)
.filter(|&&b| b == b'>')
.map(|_| 1)
.unwrap_or(0);
let remove = el + skip;
let mut name = String::new();
name.push_str(&String::from_utf8_lossy(&bytes[..at]));
name.push_str(&String::from_utf8_lossy(&bytes[at + remove..]));
cleanup_space(&mut name);
let mut name = name.trim().to_string();
if name.starts_with('(') && name.len() > 1 && name.ends_with(')') {
name = name[1..name.len() - 1].to_string();
}
let mut disp = name.clone();
if disp.is_empty()
|| disp.len() > 60
|| disp.contains('@')
|| disp.contains('<')
|| disp.contains('>')
{
disp.clone_from(&email);
}
(disp, email)
}
fn parse_bogus_from(line: &str) -> (String, String) {
let Some(bra) = line.find('<') else {
return (String::new(), String::new());
};
let Some(k) = line[bra + 1..].find('>') else {
return (String::new(), String::new());
};
let ket = bra + 1 + k;
let email = line[bra + 1..ket].to_string();
let mut name = line[..bra].trim().trim_matches('"').to_string();
if name.is_empty()
|| name.len() > 60
|| name.contains('@')
|| name.contains('<')
|| name.contains('>')
{
name.clone_from(&email);
}
(name, email)
}
fn unquote_quoted_pair(input: &str) -> String {
let mut out = String::new();
let mut it = input.chars().peekable();
while let Some(c) = it.next() {
match c {
'"' => {
while let Some(d) = it.next() {
if d == '\\' {
if let Some(e) = it.next() {
out.push(e);
}
} else if d == '"' {
break;
} else {
out.push(d);
}
}
}
'(' => {
out.push('(');
let mut depth = 1;
let mut lit = false;
for d in it.by_ref() {
if lit {
out.push(d);
lit = false;
continue;
}
if d == '\\' {
lit = true;
continue;
}
match d {
'(' => {
out.push('(');
depth += 1;
}
')' => {
out.push(')');
depth -= 1;
if depth == 0 {
break;
}
}
_ => out.push(d),
}
}
}
_ => out.push(c),
}
}
out
}