Add message/rfc822, multipart/digest multipart/mixed views

closes #22
embed
Manos Pitsidianakis 2018-08-18 17:50:31 +03:00
parent 41d8793412
commit d146c81d48
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 681 additions and 131 deletions

View File

@ -104,6 +104,7 @@ impl Display for MultipartType {
pub enum ContentType {
Text { kind: Text, charset: Charset },
Multipart { boundary: SliceBuild, kind: MultipartType, subattachments: Vec<Attachment>},
MessageRfc822,
Unsupported { tag: Vec<u8> },
}
@ -122,6 +123,7 @@ impl Display for ContentType {
ContentType::Text { kind: t, .. } => t.fmt(f),
ContentType::Multipart { kind: k, .. } => k.fmt(f),
ContentType::Unsupported { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)),
ContentType::MessageRfc822 => write!(f, "message/rfc822"),
}
}
}

View File

@ -19,6 +19,7 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use data_encoding::BASE64_MIME;
use mailbox::email::EnvelopeWrapper;
use mailbox::email::parser;
use mailbox::email::parser::BytesExt;
use std::fmt;
@ -128,9 +129,15 @@ impl AttachmentBuilder {
_ => {},
}
}
} else if ct.eq_ignore_ascii_case(b"message") && cst.eq_ignore_ascii_case(b"rfc822") {
self.content_type = ContentType::MessageRfc822;
} else {
let mut tag: Vec<u8> = Vec::with_capacity(ct.len() + cst.len() + 1);
tag.extend(ct);
tag.push(b'/');
tag.extend(cst);
self.content_type = ContentType::Unsupported {
tag: ct.into(),
tag
};
},
Err(v) => {
@ -211,7 +218,7 @@ impl AttachmentBuilder {
let offset = body.as_ptr() as usize - a.as_ptr() as usize;
SliceBuild::new(offset, body.len())
};
builder.raw = body_slice.get(a).into();
builder.raw = body_slice.get(a).ltrim().into();
for (name, value) in headers {
if name.eq_ignore_ascii_case(b"content-type") {
builder.content_type(value);
@ -239,6 +246,11 @@ impl AttachmentBuilder {
impl fmt::Display for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.content_type {
ContentType::MessageRfc822 => {
let wrapper = EnvelopeWrapper::new(self.bytes().to_vec());
write!(f, "message/rfc822: {} - {} - {}", wrapper.date(), wrapper.from_to_string(), wrapper.subject())
}
ContentType::Unsupported { .. } => {
write!(f, "Data attachment of type {}", self.mime_type())
}
@ -315,7 +327,6 @@ impl Attachment {
let mut ret = Vec::new();
fn count_recursive(att: &Attachment, ret: &mut Vec<Attachment>) {
match att.content_type {
ContentType::Unsupported { .. } | ContentType::Text { .. } => ret.push(att.clone()),
ContentType::Multipart {
subattachments: ref sub_att_vec,
..
@ -326,6 +337,7 @@ impl Attachment {
count_recursive(a, ret);
}
}
_ => ret.push(att.clone()),
}
}
@ -354,13 +366,31 @@ pub fn interpret_format_flowed(_t: &str) -> String {
unimplemented!()
}
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.content_type {
fn decode_rfc822(_raw: &[u8]) -> Attachment {
let builder = AttachmentBuilder::new(b"");
return builder.build();
/*
eprintln!("raw is\n{:?}", str::from_utf8(raw).unwrap());
let e = match Envelope::from_bytes(raw) {
Some(e) => e,
None => {
eprintln!("error in parsing mail");
let error_msg = b"Mail cannot be shown because of errors.";
let mut builder = AttachmentBuilder::new(error_msg);
return builder.build();
}
};
e.body(None)
*/
}
fn decode_rec_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment, &mut Vec<u8>) -> ()>>) -> Vec<u8> {
let mut ret = match a.content_type {
ContentType::Unsupported { .. } => Vec::new(),
ContentType::Text { .. } => decode_helper(a, filter),
ContentType::MessageRfc822 => decode_rec(&decode_rfc822(&a.raw), None),
ContentType::Multipart {
kind: ref multipart_type,
subattachments: ref sub_att_vec,
@ -380,16 +410,16 @@ fn decode_rec_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<
}
vec
},
};
if let Some(filter) = filter {
filter(a, &mut ret);
}
ret
}
pub fn decode_rec(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
pub fn decode_rec(a: &Attachment, filter: Option<Box<Fn(&Attachment, &mut 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);
}
fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment, &mut Vec<u8>) -> ()>>) -> Vec<u8> {
let charset = match a.content_type {
ContentType::Text { charset: c, .. } => c,
_ => Default::default(),
@ -408,7 +438,7 @@ fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>
| ContentTransferEncoding::Other { .. } => a.bytes().to_vec(),
};
if a.content_type.is_text() {
let mut ret = if a.content_type.is_text() {
if let Ok(v) = parser::decode_charset(&bytes, charset) {
v.into_bytes()
} else {
@ -416,8 +446,14 @@ fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>
}
} else {
bytes.to_vec()
};
if let Some(filter) = filter {
filter(a, &mut ret);
}
ret
}
pub fn decode(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
pub fn decode(a: &Attachment, filter: Option<Box<Fn(&Attachment, &mut Vec<u8>) -> ()>>) -> Vec<u8> {
decode_helper(a, &filter)
}

View File

@ -207,29 +207,39 @@ bitflags! {
}
#[derive(Debug, Clone, Default)]
pub struct EnvelopeBuilder {
from: Option<Vec<Address>>,
to: Vec<Address>,
body: Option<Attachment>,
in_reply_to: Option<MessageID>,
flags: Flag,
pub struct EnvelopeWrapper {
envelope: Envelope,
buffer: Vec<u8>,
}
impl EnvelopeBuilder {
pub fn new() -> Self {
Default::default()
use std::ops::Deref;
impl Deref for EnvelopeWrapper {
type Target = Envelope;
fn deref(&self) -> &Envelope {
&self.envelope
}
}
impl EnvelopeWrapper {
pub fn new(buffer: Vec<u8>) -> Self {
EnvelopeWrapper {
envelope: Envelope::from_bytes(&buffer).unwrap(),
buffer,
}
}
pub fn build(self) -> Envelope {
unimplemented!();
pub fn update(&mut self, new_buffer: Vec<u8>) {
*self = EnvelopeWrapper::new(new_buffer);
}
/*
* 1. Check for date. Default is now
* 2.
Envelope {
*/
pub fn envelope(&self) -> &Envelope {
&self.envelope
}
pub fn buffer(&self) -> &[u8] {
&self.buffer
}
}
@ -240,7 +250,7 @@ impl EnvelopeBuilder {
/// Access to the underlying email object in the account's backend (for example the file or the
/// entry in an IMAP server) is given through `operation_token`. For more information see
/// `BackendOp`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Envelope {
date: String,
from: Vec<Address>,
@ -279,101 +289,119 @@ impl Envelope {
flags: Flag::default(),
}
}
pub fn from_token(operation: Box<BackendOp>, hash: u64) -> Option<Envelope> {
pub fn from_bytes(bytes: &[u8]) -> Option<Envelope> {
let mut h = DefaultHasher::new();
h.write(bytes);
let mut e = Envelope::new(h.finish());
let res = e.populate_headers(bytes).ok();
if res.is_some() {
return Some(e);
}
None
}
pub fn from_token(mut operation: Box<BackendOp>, hash: u64) -> Option<Envelope> {
let mut e = Envelope::new(hash);
e.flags = operation.fetch_flags();
let res = e.populate_headers(operation).ok();
if res.is_some() {
Some(e)
} else {
None
if let Ok(bytes) = operation.as_bytes() {
let res = e.populate_headers(bytes).ok();
if res.is_some() {
return Some(e);
}
}
None
}
pub fn hash(&self) -> u64 {
self.hash
}
pub fn populate_headers(&mut self, mut operation: Box<BackendOp>) -> Result<()> {
{
let headers = match parser::headers(operation.fetch_headers()?).to_full_result() {
Ok(v) => v,
Err(e) => {
eprintln!("error in parsing mail\n");
return Err(MeliError::from(e));
}
};
pub fn populate_headers(&mut self, bytes: &[u8]) -> Result<()> {
let mut in_reply_to = None;
let mut datetime = None;
let (headers, _) = match parser::mail(bytes).to_full_result() {
Ok(v) => v,
Err(e) => {
eprintln!("error in parsing mail\n{:?}\n", e);
let error_msg = String::from("Mail cannot be shown because of errors.");
return Err(MeliError::new(error_msg));
}
};
let mut in_reply_to = None;
let mut datetime = None;
for (name, value) in headers {
if value.len() == 1 && value.is_empty() {
continue;
}
if name.eq_ignore_ascii_case(b"to") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_to(value);
} else if name.eq_ignore_ascii_case(b"from") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_from(value);
} else if name.eq_ignore_ascii_case(b"subject") {
let parse_result = parser::phrase(value.trim());
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
"".into()
};
self.set_subject(value);
} else if name.eq_ignore_ascii_case(b"message-id") {
self.set_message_id(value);
} else if name.eq_ignore_ascii_case(b"references") {
{
let parse_result = parser::references(value);
if parse_result.is_done() {
for v in parse_result.to_full_result().unwrap() {
self.push_references(v);
}
for (name, value) in headers {
if value.len() == 1 && value.is_empty() {
continue;
}
if name.eq_ignore_ascii_case(b"to") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_to(value);
} else if name.eq_ignore_ascii_case(b"from") {
let parse_result = parser::rfc2822address_list(value);
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
Vec::new()
};
self.set_from(value);
} else if name.eq_ignore_ascii_case(b"subject") {
let parse_result = parser::phrase(value.trim());
let value = if parse_result.is_done() {
parse_result.to_full_result().unwrap()
} else {
"".into()
};
self.set_subject(value);
} else if name.eq_ignore_ascii_case(b"message-id") {
self.set_message_id(value);
} else if name.eq_ignore_ascii_case(b"references") {
{
let parse_result = parser::references(value);
if parse_result.is_done() {
for v in parse_result.to_full_result().unwrap() {
self.push_references(v);
}
}
self.set_references(value);
} else if name.eq_ignore_ascii_case(b"in-reply-to") {
self.set_in_reply_to(value);
in_reply_to = Some(value);
} else if name.eq_ignore_ascii_case(b"date") {
self.set_date(value);
datetime = Some(value);
}
self.set_references(value);
} else if name.eq_ignore_ascii_case(b"in-reply-to") {
self.set_in_reply_to(value);
in_reply_to = Some(value);
} else if name.eq_ignore_ascii_case(b"date") {
self.set_date(value);
datetime = Some(value);
}
/*
* https://tools.ietf.org/html/rfc5322#section-3.6.4
*
* if self.message_id.is_none() ...
*/
if let Some(ref mut x) = in_reply_to {
self.push_references(x);
}
/*
* https://tools.ietf.org/html/rfc5322#section-3.6.4
*
* if self.message_id.is_none() ...
*/
if let Some(ref mut x) = in_reply_to {
self.push_references(x);
}
if let Some(ref mut d) = datetime {
if let Some(d) = parser::date(d) {
self.set_datetime(d);
}
if let Some(ref mut d) = datetime {
if let Some(d) = parser::date(d) {
self.set_datetime(d);
}
}
}
}
if self.message_id.is_none() {
let mut h = DefaultHasher::new();
h.write(&self.bytes(operation));
h.write(bytes);
self.set_message_id(format!("<{:x}>", h.finish()).as_bytes());
}
Ok(())
}
pub fn populate_headers_from_token(&mut self, mut operation: Box<BackendOp>) -> Result<()> {
{
let headers = operation.fetch_headers()?;
return self.populate_headers(headers);
}
}
pub fn date(&self) -> u64 {
self.timestamp
}
@ -410,6 +438,29 @@ impl Envelope {
.map(|v| v.into())
.unwrap_or_else(|_| Vec::new())
}
pub fn body_bytes(&self, bytes: &[u8]) -> Attachment {
let (headers, body) = match parser::mail(bytes).to_full_result() {
Ok(v) => v,
Err(_) => {
eprintln!("error in parsing mail\n");
let error_msg = b"Mail cannot be shown because of errors.";
let mut builder = AttachmentBuilder::new(error_msg);
return builder.build();
}
};
let mut builder = AttachmentBuilder::new(body);
for (name, value) in headers {
if value.len() == 1 && value.is_empty() {
continue;
}
if name.eq_ignore_ascii_case(b"content-transfer-encoding") {
builder.content_transfer_encoding(value);
} else if name.eq_ignore_ascii_case(b"content-type") {
builder.content_type(value);
}
}
builder.build()
}
pub fn body(&self, mut operation: Box<BackendOp>) -> Attachment {
let file = operation.as_bytes();
let (headers, body) = match parser::mail(file.unwrap()).to_full_result() {

View File

@ -0,0 +1,460 @@
/*
* meli - ui crate.
*
* Copyright 2017-2018 Manos Pitsidianakis
*
* This file is part of meli.
*
* meli is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* meli is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
use super::*;
use linkify::{Link, LinkFinder};
use std::process::{Command, Stdio};
use mime_apps::query_default_app;
#[derive(PartialEq, Debug)]
enum ViewMode {
Normal,
Url,
Attachment(usize),
Raw,
Subview,
}
impl ViewMode {
fn is_attachment(&self) -> bool {
match self {
ViewMode::Attachment(_) => true,
_ => false,
}
}
}
/// Contains an Envelope view, with sticky headers, a pager for the body, and subviews for more
/// menus
pub struct EnvelopeView {
pager: Option<Pager>,
subview: Option<Box<Component>>,
dirty: bool,
mode: ViewMode,
wrapper: EnvelopeWrapper,
cmd_buf: String,
}
impl fmt::Display for EnvelopeView {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO display subject/info
write!(f, "view mail")
}
}
impl EnvelopeView {
pub fn new(
wrapper: EnvelopeWrapper,
pager: Option<Pager>,
subview: Option<Box<Component>>,
) -> Self {
EnvelopeView {
pager,
subview,
dirty: true,
mode: ViewMode::Normal,
wrapper,
cmd_buf: String::with_capacity(4),
}
}
/// Returns the string to be displayed in the Viewer
fn attachment_to_text(&self, body: Attachment) -> String {
let finder = LinkFinder::new();
let body_text = String::from_utf8_lossy(&decode_rec(
&body,
Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
if a.content_type().is_text_html() {
use std::io::Write;
use std::process::{Command, Stdio};
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(&v)
.expect("Failed to write to w3m stdin");
*v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n".to_vec();
v.extend(html_filter.wait_with_output().unwrap().stdout);
}
})),
)).into_owned();
match self.mode {
ViewMode::Normal | ViewMode::Subview => {
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(body.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, fix this");
};
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
}
}
}
pub fn plain_text_to_buf(s: &String, highlight_urls: bool) -> CellBuffer {
let mut buf = CellBuffer::from(s);
if highlight_urls {
let lines: Vec<&str> = s.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 {
3
} else if lidx_total < 100 {
4
} else if lidx_total < 1000 {
5
} else {
panic!("BUG: Message body with more than 100 urls");
};
for i in 1..=offset {
buf[(l.start() + shift - i, 0)].set_fg(Color::Byte(226));
//buf[(l.start() + shift - 2, 0)].set_fg(Color::Byte(226));
//buf[(l.start() + shift - 3, 0)].set_fg(Color::Byte(226));
}
lidx_total += 1;
}
// Each Cell represents one char so next line will be:
shift += r.chars().count() + 1;
}
}
buf
}
}
impl Component for EnvelopeView {
fn draw(&mut self, grid: &mut CellBuffer, area: Area, context: &mut Context) {
let upper_left = upper_left!(area);
let bottom_right = bottom_right!(area);
let y :usize = {
let envelope: &Envelope = &self.wrapper;
if self.mode == ViewMode::Raw {
clear_area(grid, area);
context.dirty_areas.push_back(area);
get_y(upper_left) - 1
} else {
let (x, y) = write_string_to_grid(
&format!("Date: {}", envelope.date_as_str()),
grid,
Color::Byte(33),
Color::Default,
area,
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("From: {}", envelope.from_to_string()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("To: {}", envelope.to_to_string()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("Subject: {}", envelope.subject()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
let (x, y) = write_string_to_grid(
&format!("Message-ID: <{}>", envelope.message_id_raw()),
grid,
Color::Byte(33),
Color::Default,
(set_y(upper_left, y + 1), bottom_right),
true,
);
for x in x..=get_x(bottom_right) {
grid[(x, y)].set_ch(' ');
grid[(x, y)].set_bg(Color::Default);
grid[(x, y)].set_fg(Color::Default);
}
clear_area(grid, (set_y(upper_left, y + 1), set_y(bottom_right, y + 2)));
context
.dirty_areas
.push_back((upper_left, set_y(bottom_right, y + 1)));
y + 1
}
};
if self.dirty {
let body = self.wrapper.body_bytes(self.wrapper.buffer());
match self.mode {
ViewMode::Attachment(aidx) if body.attachments()[aidx].is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(
&body.attachments()[aidx],
None,
))));
}
ViewMode::Normal if body.is_html() => {
self.subview = Some(Box::new(HtmlView::new(decode(&body, None))));
self.mode = ViewMode::Subview;
}
_ => {
let buf = {
let text = self.attachment_to_text(body);
// URL indexes must be colored (ugh..)
EnvelopeView::plain_text_to_buf(&text, self.mode == ViewMode::Url)
};
let cursor_pos = if self.mode.is_attachment() {
Some(0)
} else {
self.pager.as_mut().map(|p| p.cursor_pos())
};
self.pager = Some(Pager::from_buf(&buf, cursor_pos));
}
};
self.dirty = false;
}
if let Some(s) = self.subview.as_mut() {
s.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
} else if let Some(p) = self.pager.as_mut() {
p.draw(grid, (set_y(upper_left, y + 1), bottom_right), context);
}
}
fn process_event(&mut self, event: &UIEvent, context: &mut Context) {
match event.event_type {
UIEventType::Input(Key::Esc) => {
self.cmd_buf.clear();
}
UIEventType::Input(Key::Char(c)) if c >= '0' && c <= '9' => {
self.cmd_buf.push(c);
}
UIEventType::Input(Key::Char('r'))
if self.mode == ViewMode::Normal || self.mode == ViewMode::Raw =>
{
self.mode = if self.mode == ViewMode::Raw {
ViewMode::Normal
} else {
ViewMode::Raw
};
self.dirty = true;
}
UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() || self.mode == ViewMode::Subview => {
self.mode = ViewMode::Normal;
self.subview.take();
self.dirty = true;
}
UIEventType::Input(Key::Char('a'))
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Normal =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
{
let envelope: &Envelope = self.wrapper.envelope();
if let Some(u) = envelope.body_bytes(self.wrapper.buffer()).attachments().get(lidx) {
match u.content_type() {
ContentType::MessageRfc822 => {
self.mode = ViewMode::Subview;
self.subview = Some(Box::new(Pager::from_str(&String::from_utf8_lossy(&decode_rec(u, None)).to_string(), None)));
},
ContentType::Text { .. } => {
self.mode = ViewMode::Attachment(lidx);
self.dirty = true;
}
ContentType::Multipart { .. } => {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(
"Multipart attachments are not supported yet.".to_string(),
),
});
return;
}
ContentType::Unsupported { .. } => {
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), None);
Command::new(&binary)
.arg(p.path())
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.unwrap_or_else(|_| {
panic!("Failed to start {}", binary.display())
});
context.temp_files.push(p);
} else {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(format!(
"Couldn't find a default application for type {}",
attachment_type
)),
});
return;
}
}
}
} else {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(format!(
"Attachment `{}` not found.",
lidx
)),
});
return;
}
};
}
UIEventType::Input(Key::Char('g'))
if !self.cmd_buf.is_empty() && self.mode == ViewMode::Url =>
{
let lidx = self.cmd_buf.parse::<usize>().unwrap();
self.cmd_buf.clear();
let url = {
let envelope: &Envelope = self.wrapper.envelope();
let finder = LinkFinder::new();
let mut t = envelope.body_bytes(self.wrapper.buffer()).text().to_string();
let links: Vec<Link> = finder.links(&t).collect();
if let Some(u) = links.get(lidx) {
u.as_str().to_string()
} else {
context.replies.push_back(UIEvent {
id: 0,
event_type: UIEventType::StatusNotification(format!(
"Link `{}` not found.",
lidx
)),
});
return;
}
};
Command::new("xdg-open")
.arg(url)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start xdg_open");
}
UIEventType::Input(Key::Char('u')) => {
match self.mode {
ViewMode::Normal => self.mode = ViewMode::Url,
ViewMode::Url => self.mode = ViewMode::Normal,
_ => {}
}
self.dirty = true;
}
_ => {}
}
if let Some(ref mut sub) = self.subview {
sub.process_event(event, context);
} else if let Some(ref mut p) = self.pager {
p.process_event(event, context);
}
}
fn is_dirty(&self) -> bool {
self.dirty
|| self.pager.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
|| self.subview.as_ref().map(|p| p.is_dirty()).unwrap_or(false)
}
fn set_dirty(&mut self) {
self.dirty = true;
}
}

View File

@ -28,6 +28,9 @@ pub use self::html::*;
mod thread;
pub use self::thread::*;
mod envelope;
pub use self::envelope::*;
use mime_apps::query_default_app;
#[derive(PartialEq, Debug)]
@ -74,7 +77,7 @@ impl MailView {
local_collection: Vec<usize>,
pager: Option<Pager>,
subview: Option<Box<Component>>,
) -> Self {
) -> Self {
MailView {
coordinates,
local_collection,
@ -90,17 +93,13 @@ impl MailView {
/// Returns the string to be displayed in the Viewer
fn attachment_to_text(&self, body: Attachment) -> String {
let finder = LinkFinder::new();
let body_text = if body.content_type().is_text_html() {
let mut s =
String::from("Text piped through `w3m`. Press `v` to open in web browser. \n\n");
s.extend(
String::from_utf8_lossy(&decode(
&body,
Some(Box::new(|a: &Attachment| {
let body_text = String::from_utf8_lossy(&decode_rec(
&body,
Some(Box::new(|a: &Attachment, v: &mut Vec<u8>| {
if a.content_type().is_text_html() {
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())
@ -112,17 +111,13 @@ impl MailView {
.stdin
.as_mut()
.unwrap()
.write_all(&raw)
.write_all(&v)
.expect("Failed to write to w3m stdin");
html_filter.wait_with_output().unwrap().stdout
})),
)).into_owned()
.chars(),
);
s
} else {
String::from_utf8_lossy(&decode_rec(&body, None)).into()
};
*v = b"Text piped through `w3m`. Press `v` to open in web browser. \n\n".to_vec();
v.extend(html_filter.wait_with_output().unwrap().stdout);
}
})),
)).into_owned();
match self.mode {
ViewMode::Normal | ViewMode::Subview => {
let mut t = body_text.to_string();
@ -364,7 +359,7 @@ impl Component for MailView {
};
self.dirty = true;
}
UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() => {
UIEventType::Input(Key::Char('r')) if self.mode.is_attachment() || self.mode == ViewMode::Subview => {
self.mode = ViewMode::Normal;
self.subview.take();
self.dirty = true;
@ -391,6 +386,12 @@ impl Component for MailView {
let op = context.accounts[self.coordinates.0].backend.operation(envelope.hash());
if let Some(u) = envelope.body(op).attachments().get(lidx) {
match u.content_type() {
ContentType::MessageRfc822 => {
self.mode = ViewMode::Subview;
let wrapper = EnvelopeWrapper::new(u.bytes().to_vec());
self.subview = Some(Box::new(EnvelopeView::new(wrapper, None, None)));
},
ContentType::Text { .. } => {
self.mode = ViewMode::Attachment(lidx);
self.dirty = true;

View File

@ -188,7 +188,7 @@ impl Default for CellBuffer {
impl<'a> From<&'a String> for CellBuffer {
fn from(s: &'a String) -> Self {
let lines: Vec<&str> = s.lines().map(|l| l.trim_right()).collect();
let len = s.len();
let len = s.len() + lines.len();
let mut buf = CellBuffer::new(len, 1, Cell::default());
let mut x = 0;
for l in lines.iter() {