use std::borrow::Cow;
use std::io::{self, Read, Write};
use chrono::prelude::*;
use super::literal_source::LiteralSource;
use super::mailbox_name::MailboxName;
use crate::account::model::Flag;
use crate::mime::utf7;
#[derive(Clone, Copy, Debug)]
pub struct LexWriter<W> {
writer: W,
unicode_aware: bool,
literal_plus: bool,
}
impl<W: Write> LexWriter<W> {
pub fn new(writer: W, unicode_aware: bool, literal_plus: bool) -> Self {
LexWriter {
writer,
unicode_aware,
literal_plus,
}
}
#[cfg(test)]
pub fn into_inner(self) -> W {
self.writer
}
pub fn verbatim(&mut self, s: &str) -> io::Result<()> {
self.writer.write_all(s.as_bytes())?;
Ok(())
}
pub fn verbatim_bytes(&mut self, s: &[u8]) -> io::Result<()> {
self.writer.write_all(s)?;
Ok(())
}
pub fn nil(&mut self) -> io::Result<()> {
self.verbatim("NIL")
}
pub fn censored_astring(&mut self, s: &str) -> io::Result<()> {
self.astring(&self.censor(s))
}
pub fn unicode_astring(&mut self, s: &str) -> io::Result<()> {
self.astring(s)
}
pub fn censored_nstring(
&mut self,
s: &Option<impl AsRef<str>>,
) -> io::Result<()> {
match s.as_ref() {
None => self.nil(),
Some(s) => self.string(&self.censor(s.as_ref())),
}
}
pub fn encoded_nstring(
&mut self,
s: &Option<impl AsRef<str>>,
) -> io::Result<()> {
match s.as_ref() {
None => self.nil(),
Some(s) => self.string(&self.encode(s.as_ref())),
}
}
pub fn censored_string(&mut self, s: &str) -> io::Result<()> {
self.string(&self.censor(s))
}
pub fn mailbox(&mut self, mn: &MailboxName<'_>) -> io::Result<()> {
if self.is_conservative_atom(&mn.raw) {
write!(self.writer, "{}", mn.raw)?;
} else if self.unicode_aware || !mn.utf8 {
self.string(&mn.raw)?;
} else {
self.string(&utf7::IMAP.encode(&mn.raw))?;
}
Ok(())
}
pub fn literal(
&mut self,
use_binary_syntax: bool,
mut data: impl Read,
len: u64,
) -> io::Result<()> {
write!(
self.writer,
"{}{{{}{}}}\r\n",
if use_binary_syntax { "~" } else { "" },
len,
if !use_binary_syntax && self.literal_plus {
"+"
} else {
""
}
)?;
io::copy(&mut data, &mut self.writer)?;
Ok(())
}
pub fn literal_source(&mut self, ls: &mut LiteralSource) -> io::Result<()> {
self.literal(ls.binary, &mut ls.data, ls.len)
}
pub fn flag(&mut self, flag: &Flag) -> io::Result<()> {
write!(self.writer, "{}", flag)
}
pub fn date(&mut self, date: &NaiveDate) -> io::Result<()> {
write!(self.writer, "\"{}\"", date.format("%-d-%b-%Y"))
}
pub fn datetime(
&mut self,
datetime: &DateTime<FixedOffset>,
) -> io::Result<()> {
write!(
self.writer,
"\"{}\"",
datetime.format("%_d-%b-%Y %H:%M:%S %z")
)
}
pub fn num_u32(&mut self, value: &u32) -> io::Result<()> {
write!(self.writer, "{}", *value)
}
pub fn num_u64(&mut self, value: &u64) -> io::Result<()> {
write!(self.writer, "{}", *value)
}
fn astring(&mut self, s: &str) -> io::Result<()> {
if self.is_conservative_atom(s) {
write!(self.writer, "{}", s)?;
} else {
self.string(s)?;
}
Ok(())
}
fn string(&mut self, s: &str) -> io::Result<()> {
if self.is_quotable(s) {
write!(self.writer, "\"{}\"", s)?;
} else {
self.literal(false, s.as_bytes(), s.len() as u64)?;
}
Ok(())
}
fn censor<'a>(&self, s: &'a str) -> Cow<'a, str> {
if self.unicode_aware || s.is_ascii() {
Cow::Borrowed(s)
} else {
Cow::Owned(s.replace(|ch| ch > '\u{7f}', "X"))
}
}
fn encode<'a>(&self, s: &'a str) -> Cow<'a, str> {
if self.unicode_aware || s.is_ascii() {
Cow::Borrowed(s)
} else {
let mut total_accum = String::new();
let mut part_accum = String::new();
let mut first = true;
for c in s.chars() {
part_accum.push(c);
if part_accum.len() > 40 {
encode_part(&mut total_accum, &part_accum, first);
part_accum.clear();
first = false;
}
}
encode_part(&mut total_accum, &part_accum, first);
Cow::Owned(total_accum)
}
}
fn is_conservative_atom(&self, s: &str) -> bool {
!"nil".eq_ignore_ascii_case(s)
&& !s.is_empty()
&& s.as_bytes().iter().copied().all(|b| match b {
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'='
| b'?'
| b'/'
| b'+'
| b'_'
| b'.'
| b'-' => true,
_ => false,
})
}
fn is_quotable(&self, s: &str) -> bool {
s.len() < 100
&& s.as_bytes().iter().copied().all(|b| match b {
0..=31 | 127 | b'\\' | b'"' => false,
128..=255 => self.unicode_aware,
_ => true,
})
}
}
fn encode_part(dst: &mut String, src: &str, first: bool) {
if src.is_empty() {
return;
}
if !first {
dst.push(' ');
}
dst.push_str("=?utf-8?b?");
dst.push_str(&base64::encode_config(src, base64::STANDARD_NO_PAD));
dst.push_str("?=");
}
#[cfg(test)]
mod test {
use super::*;
use crate::mime::encoded_word;
fn to_str(l: LexWriter<Vec<u8>>) -> String {
String::from_utf8(l.into_inner()).unwrap()
}
#[test]
fn nil() {
let mut l = LexWriter::new(Vec::<u8>::new(), true, false);
l.nil().unwrap();
assert_eq!("NIL", to_str(l));
}
#[test]
fn censored_astring_non_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), false, false);
l.censored_astring("foo").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("nil").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("NIL").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("foo bar").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("foo\\ bar").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("föö").unwrap();
assert_eq!(
"foo \"nil\" \"NIL\" \"foo bar\" {8}\r\nfoo\\ bar fXX",
to_str(l),
);
}
#[test]
fn censored_astring_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), true, false);
l.censored_astring("foo").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("nil").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("NIL").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("foo bar").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("foo\\ bar").unwrap();
l.verbatim(" ").unwrap();
l.censored_astring("föö").unwrap();
assert_eq!(
"foo \"nil\" \"NIL\" \"foo bar\" {8}\r\nfoo\\ bar \"föö\"",
to_str(l),
);
}
#[test]
fn mailbox_non_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), false, false);
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed("INBOX")))
.unwrap();
l.verbatim(" ").unwrap();
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed("Lost & Found")))
.unwrap();
l.verbatim(" ").unwrap();
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed(
"~peter/mail/台北/日本語",
)))
.unwrap();
assert_eq!(
"INBOX \"Lost &- Found\" \"~peter/mail/&U,BTFw-/&ZeVnLIqe-\"",
to_str(l)
);
}
#[test]
fn mailbox_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), true, false);
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed("INBOX")))
.unwrap();
l.verbatim(" ").unwrap();
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed("Lost & Found")))
.unwrap();
l.verbatim(" ").unwrap();
l.mailbox(&MailboxName::of_utf8(Cow::Borrowed(
"~peter/mail/台北/日本語",
)))
.unwrap();
assert_eq!(
"INBOX \"Lost & Found\" \"~peter/mail/台北/日本語\"",
to_str(l)
);
}
#[test]
fn flags_non_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), false, false);
l.flag(&Flag::Flagged).unwrap();
l.verbatim(" ").unwrap();
l.flag(&Flag::Keyword("foo".to_owned())).unwrap();
assert_eq!("\\Flagged foo", to_str(l));
}
#[test]
fn flags_unicode() {
let mut l = LexWriter::new(Vec::<u8>::new(), true, false);
l.flag(&Flag::Flagged).unwrap();
l.verbatim(" ").unwrap();
l.flag(&Flag::Keyword("foo".to_owned())).unwrap();
assert_eq!("\\Flagged foo", to_str(l));
}
#[test]
fn encoded_words_are_decodable() {
let mut l = LexWriter::new(Vec::<u8>::new(), false, false);
l.encoded_nstring(&Some("föö")).unwrap();
assert_eq!(
Some("föö".to_owned()),
encoded_word::ew_decode(to_str(l).trim_matches('"'))
);
}
}