parent
41d8793412
commit
d146c81d48
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
Loading…
Reference in New Issue