Fix proper viewing for multipart alternatives, html view and quoted printable soft breaks

embed
Manos Pitsidianakis 2018-08-08 19:06:00 +03:00
parent 93b36a9941
commit 14d65838b7
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
7 changed files with 240 additions and 125 deletions

View File

@ -83,15 +83,39 @@ impl Display for ContentType {
}
}
}
impl ContentType {
pub fn is_text(&self) -> bool {
if let ContentType::Text { .. } = self {
true
} else {
false
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ContentSubType {
Plain,
Html,
Other { tag: Vec<u8> },
}
impl ContentSubType {
pub fn is_html(&self) -> bool {
if let ContentSubType::Html = self {
true
} else {
false
}
}
}
impl Display for ContentSubType {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match *self {
ContentSubType::Plain => write!(f, "plain"),
ContentSubType::Html => write!(f, "html"),
ContentSubType::Other { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)),
}
}

View File

@ -18,14 +18,14 @@
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use std::fmt::{Display, Formatter, Result as FmtResult};
use std::fmt;
use std::str;
use data_encoding::BASE64_MIME;
use mailbox::email::parser;
pub use mailbox::email::attachment_types::*;
#[derive(Clone, Debug)]
#[derive(Clone)]
pub enum AttachmentType {
Data {
tag: Vec<u8>,
@ -39,6 +39,16 @@ pub enum AttachmentType {
},
}
impl fmt::Debug for AttachmentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AttachmentType::Data { .. } => write!(f, "AttachmentType::Data {{ .. }}"),
AttachmentType::Text { .. } => write!(f, "AttachmentType::Text {{ .. }}"),
AttachmentType::Multipart { of_type, subattachments } => write!(f, "AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}", of_type, subattachments),
}
}
}
/*
*
* Data
@ -54,17 +64,27 @@ pub struct AttachmentBuilder {
raw: Vec<u8>,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct Attachment {
content_type: (ContentType, ContentSubType),
content_transfer_encoding: ContentTransferEncoding,
raw: Vec<u8>,
attachment_type: AttachmentType,
}
impl Display for AttachmentType {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
impl fmt::Debug for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Attachment {{\n content_type: {:?},\n content_transfer_encoding: {:?},\n raw: Vec of {} bytes\n attachment_type: {:?}\n }}",
self.content_type,
self.content_transfer_encoding,
self.raw.len(),
self.attachment_type)
}
}
impl fmt::Display for AttachmentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AttachmentType::Data { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)),
AttachmentType::Text { content: ref c } => write!(f, "{}", String::from_utf8_lossy(c)),
@ -108,7 +128,9 @@ impl AttachmentBuilder {
break;
}
}
if !cst.eq_ignore_ascii_case(b"plain") {
if cst.eq_ignore_ascii_case(b"html") {
self.content_type.1 = ContentSubType::Html;
} else if !cst.eq_ignore_ascii_case(b"plain") {
self.content_type.1 = ContentSubType::Other {
tag: cst.to_ascii_lowercase(),
};
@ -144,25 +166,27 @@ impl AttachmentBuilder {
self
}
fn decode(&self) -> Vec<u8> {
// TODO merge this and standalone decode() function
let charset = match self.content_type.0 {
ContentType::Text{ charset: c } => c,
_ => Default::default(),
};
let decoded_result = parser::decode_charset(&self.raw, charset);
let b: &[u8] = decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw);
match self.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(b) {
let bytes = match self.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(&self.raw) {
Ok(v) => v,
_ => b.to_vec(),
_ => self.raw.to_vec(),
},
ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_text(b)
ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(&self.raw)
.to_full_result()
.unwrap(),
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => b.to_vec(),
}
| ContentTransferEncoding::Other { .. } => self.raw.to_vec(),
};
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw).to_vec()
}
pub fn build(self) -> Attachment {
let attachment_type = match self.content_type.0 {
@ -172,9 +196,9 @@ impl AttachmentBuilder {
ContentType::Multipart { boundary: ref b } => {
let multipart_type = match self.content_type.1 {
ContentSubType::Other { ref tag } => match &tag[..] {
b"mixed" => MultipartType::Mixed,
b"alternative" => MultipartType::Alternative,
b"digest" => MultipartType::Digest,
b"mixed" | b"Mixed" | b"MIXED" => MultipartType::Mixed,
b"alternative" | b"Alternative" | b"ALTERNATIVE" => MultipartType::Alternative,
b"digest" | b"Digest" | b"DIGEST" => MultipartType::Digest,
_ => MultipartType::Unsupported { tag: tag.clone() },
},
_ => panic!(),
@ -238,8 +262,8 @@ impl AttachmentBuilder {
}
impl Display for Attachment {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
impl fmt::Display for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.attachment_type {
AttachmentType::Data { .. } => {
write!(f, "Data attachment of type {}", self.mime_type())
@ -265,13 +289,13 @@ impl Attachment {
pub fn bytes(&self) -> &[u8] {
&self.raw
}
fn get_text_recursive(&self, text: &mut String) {
fn get_text_recursive(&self, text: &mut Vec<u8>) {
match self.attachment_type {
AttachmentType::Data { .. } => {
//text.push_str(&format!("Data attachment of type {}", self.mime_type()));
}
AttachmentType::Text { content: ref t } => {
text.push_str(&String::from_utf8_lossy(t));
AttachmentType::Text { .. } => {
text.extend(decode(self, None));
}
AttachmentType::Multipart {
of_type: ref multipart_type,
@ -286,15 +310,15 @@ impl Attachment {
} else {
for a in sub_att_vec {
a.get_text_recursive(text);
text.push_str("\n\n");
text.extend_from_slice(b"\n\n");
}
},
}
}
pub fn text(&self) -> String {
let mut text = String::with_capacity(self.raw.len());
let mut text = Vec::with_capacity(self.raw.len());
self.get_text_recursive(&mut text);
text
String::from_utf8_lossy(&text).into()
}
pub fn description(&self) -> Vec<String> {
self.attachments().iter().map(|a| a.text()).collect()
@ -341,24 +365,66 @@ pub fn interpret_format_flowed(_t: &str) -> String {
unimplemented!()
}
pub fn decode(a: &Attachment) -> Vec<u8> {
fn decode_rec_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
if let Some(filter) = filter {
return filter(a);
}
match a.attachment_type {
AttachmentType::Data { .. } => { Vec::new()},
AttachmentType::Text { .. } => decode_helper(a, filter),
AttachmentType::Multipart {
of_type: ref multipart_type,
subattachments: ref sub_att_vec,
} => if *multipart_type == MultipartType::Alternative {
for a in sub_att_vec {
if a.content_type.1 == ContentSubType::Plain {
return decode_helper(a, filter);
}
}
decode_helper(a, filter)
} else {
let mut vec = Vec::new();
for a in sub_att_vec {
vec.extend(decode_rec_helper(a, filter));
vec.extend_from_slice(b"\n\n");
}
vec
},
}
}
pub fn decode_rec(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
decode_rec_helper(a, &filter)
}
fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
if let Some(filter) = filter {
return filter(a);
}
let charset = match a.content_type.0 {
ContentType::Text{ charset: c } => c,
_ => Default::default(),
};
let decoded_result = parser::decode_charset(a.bytes(), charset);
let b: &[u8] = decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes());
match a.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(b) {
let bytes = match a.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(a.bytes()) {
Ok(v) => v,
_ => b.to_vec(),
_ => a.bytes().to_vec(),
},
ContentTransferEncoding::QuotedPrintable => parser::quoted_printed_bytes(b)
ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(a.bytes())
.to_full_result()
.unwrap(),
ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => b.to_vec(),
| ContentTransferEncoding::Other { .. } => a.bytes().to_vec(),
};
if a.content_type().0.is_text() {
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes()).to_vec()
} else {
bytes.to_vec()
}
}
pub fn decode(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
decode_helper(a, &filter)
}

View File

@ -206,7 +206,7 @@ fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
Ok(v) => v,
Err(_) => encoded.to_vec(),
},
b'q' | b'Q' => match quoted_printed_bytes(encoded) {
b'q' | b'Q' => match quoted_printable_bytes_header(encoded) {
IResult::Done(b"", s) => s,
_ => return IResult::Error(error_code!(ErrorKind::Custom(43))),
},
@ -296,16 +296,38 @@ pub fn decode_charset(s: &[u8], charset: Charset) -> Result<String> {
}
}
fn quoted_printable_soft_break(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.len() < 2 {
IResult::Incomplete(Needed::Size(1))
} else if input[0] == b'=' && input[1] == b'\n' {
IResult::Done(&input[2..], &input[0..2]) // `=\n` is an escaped space character.
} else {
IResult::Error(error_code!(ErrorKind::Custom(43)))
}
}
named!(qp_underscore_header<u8>, do_parse!(tag!("_") >> ({ b' ' })));
/// For atoms in Header values.
// With MIME, headers in quoted printable format can contain underscores that represent spaces.
// In non-header context, an underscore is just a plain underscore.
named!(
pub quoted_printed_bytes<Vec<u8>>,
pub quoted_printable_bytes_header<Vec<u8>>,
many0!(alt_complete!(
quoted_printable_byte | qp_underscore_header | le_u8
))
);
/// For atoms in Header values.
named!(
pub quoted_printable_bytes<Vec<u8>>,
many0!(alt_complete!(
preceded!(quoted_printable_soft_break, quoted_printable_byte) |
preceded!(quoted_printable_soft_break, le_u8)
| quoted_printable_byte | le_u8
))
);
named!(
encoded_word_list<Vec<u8>>,
ws!(do_parse!(
@ -527,11 +549,7 @@ fn test_address() {
println!("{:?}", rfc2822address_list(s).unwrap());
}
named!(pub rfc2822address_list<Vec<Address>>, ws!(
separated_list!(is_a!(","), address)
));
named!(pub rfc2822address_list<Vec<Address>>, ws!( separated_list!(is_a!(","), address)));
named!(pub address_list<String>, ws!(do_parse!(
list: alt_complete!( encoded_word_list | ascii_token) >>
@ -766,15 +784,15 @@ named!(pub content_type< (&[u8], &[u8], Vec<(&[u8], &[u8])>) >,
} )
));
named!(pub quoted_printable_text<Vec<u8>>,
do_parse!(
bytes: many0!(alt_complete!(
preceded!(tag!("=\n"), quoted_printable_byte) |
preceded!(tag!("=\n"), le_u8) |
quoted_printable_byte |
le_u8)) >>
( {
bytes
} )
)
);
//named!(pub quoted_printable_text<Vec<u8>>,
// do_parse!(
// bytes: many0!(alt_complete!(
// preceded!(tag!("=\n"), quoted_printable_byte) |
// preceded!(tag!("=\n"), le_u8) |
// quoted_printable_byte |
// le_u8)) >>
// ( {
// bytes
// } )
// )
//);

View File

@ -70,6 +70,78 @@ impl MailView {
cmd_buf: String::with_capacity(4),
}
}
/// Returns the string to be displayed in the Viewer
fn attachment_to_text(&self, envelope: &Envelope) -> String {
let finder = LinkFinder::new();
let body = envelope.body();
let body_text = if body.content_type().0.is_text() && body.content_type().1.is_html() {
String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| {
use std::io::Write;
use std::process::{Command, Stdio};
let raw = decode(a, None);
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
html_filter.stdin.as_mut().unwrap().write_all(&raw).expect("Failed to write to w3m stdin");
html_filter.wait_with_output().unwrap().stdout
})))).into_owned()
} else {
String::from_utf8_lossy(&decode_rec(&body, None)).into()
};
match self.mode {
ViewMode::Normal => {
let mut t = body_text.to_string();
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(),
ViewMode::Url => {
let mut t = body_text.to_string();
for (lidx, l) in finder.links(&body.text()).enumerate() {
let offset = if lidx < 10 {
lidx * 3
} else if lidx < 100 {
26 + (lidx - 9) * 4
} else if lidx < 1000 {
385 + (lidx - 99) * 5
} else {
panic!("BUG: Message body with more than 100 urls");
};
t.insert_str(l.start() + offset, &format!("[{}]", lidx));
}
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Attachment(aidx) => {
let attachments = body.attachments();
let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
ret.push_str(&attachments[aidx].text());
ret
}
}
}
}
impl Component for MailView {
@ -177,61 +249,15 @@ impl Component for MailView {
.as_ref()
.unwrap();
let envelope: &Envelope = &mailbox.collection[envelope_idx];
let text = self.attachment_to_text(envelope);
let finder = LinkFinder::new();
let mut text = match self.mode {
ViewMode::Normal => {
let mut t = envelope.body().text().to_string();
if envelope.body().count_attachments() > 1 {
t = envelope.body().attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(),
ViewMode::Url => {
let mut t = envelope.body().text().to_string();
for (lidx, l) in finder.links(&envelope.body().text()).enumerate() {
let offset = if lidx < 10 {
lidx * 3
} else if lidx < 100 {
26 + (lidx - 9) * 4
} else if lidx < 1000 {
385 + (lidx - 99) * 5
} else {
panic!("BUG: Message body with more than 100 urls");
};
t.insert_str(l.start() + offset, &format!("[{}]", lidx));
}
if envelope.body().count_attachments() > 1 {
t = envelope.body().attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Attachment(aidx) => {
let attachments = envelope.body().attachments();
let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
ret.push_str(&attachments[aidx].text());
ret
}
};
let mut buf = CellBuffer::from(&text);
if self.mode == ViewMode::Url {
// URL indexes must be colored (ugh..)
let lines: Vec<&str> = text.split('\n').map(|l| l.trim_right()).collect();
let mut shift = 0;
let mut lidx_total = 0;
let finder = LinkFinder::new();
for r in &lines {
for l in finder.links(&r) {
let offset = if lidx_total < 10 {
@ -331,7 +357,7 @@ impl Component for MailView {
let attachment_type = u.mime_type();
let binary = query_default_app(&attachment_type);
if let Ok(binary) = binary {
let mut p = create_temp_file(&decode(u), None);
let mut p = create_temp_file(&decode(u, None), None);
Command::new(&binary)
.arg(p.path())
.stdin(Stdio::piped())

View File

@ -45,18 +45,6 @@ pub enum SortField {
impl FromStr for SortField {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
eprintln!("sortfield from_str {}", s);
match s.trim() {
"subject" | "s" | "sub" | "sbj" | "subj" => {
eprintln!("parsed: subject");
}
"date" | "d" => {
eprintln!("parsed date");
}
_ => {
eprintln!("error in parse");
}
}
match s.trim() {
"subject" | "s" | "sub" | "sbj" | "subj" => Ok(SortField::Subject),
"date" | "d" => Ok(SortField::Date),
@ -68,7 +56,6 @@ impl FromStr for SortField {
impl FromStr for SortOrder {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
eprintln!("sortoder from_str {}", s);
match s.trim() {
"asc" => Ok(SortOrder::Asc),
"desc" => Ok(SortOrder::Desc),

View File

@ -183,10 +183,6 @@ impl State<std::io::Stdout> {
account.watch(RefreshEventConsumer::new(Box::new(move |r| {
sender.send(ThreadEvent::from(r));
})));
}
for (k, v) in &s.context.mailbox_hashes {
eprintln!("{:x} -> {:?}", k, v);
}
s
}
@ -348,9 +344,7 @@ impl<W: Write> State<W> {
}
/// Convert user commands to actions/method calls.
fn parse_command(&mut self, cmd: &str) {
eprintln!("cmd is {}", cmd);
let result = parse_command(&cmd.as_bytes()).to_full_result();
eprintln!("rseult is {:?}", result);
if let Ok(v) = result {
self.rcv_event(UIEvent {

View File

@ -40,8 +40,8 @@ impl File {
pub fn file(&mut self) -> std::fs::File {
std::fs::File::create(&self.path).unwrap()
}
pub fn path(&mut self) -> &mut PathBuf {
&mut self.path
pub fn path(&self) -> &PathBuf {
&self.path
}
}