improve read and attachments commands

This commit is contained in:
Clément DOUIN 2021-03-10 16:46:47 +01:00
parent f7ed99d55f
commit 10c523fd2c
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
4 changed files with 180 additions and 51 deletions

1
Cargo.lock generated
View file

@ -240,6 +240,7 @@ dependencies = [
"serde_json", "serde_json",
"terminal_size", "terminal_size",
"toml", "toml",
"uuid",
] ]
[[package]] [[package]]

View file

@ -16,3 +16,4 @@ serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61" serde_json = "1.0.61"
terminal_size = "0.1.15" terminal_size = "0.1.15"
toml = "0.5.8" toml = "0.5.8"
uuid = { version = "0.8", features = ["v4"] }

View file

@ -8,12 +8,11 @@ mod smtp;
mod table; mod table;
use clap::{App, AppSettings, Arg, SubCommand}; use clap::{App, AppSettings, Arg, SubCommand};
use serde_json::json;
use std::{fmt, fs, process::exit, result}; use std::{fmt, fs, process::exit, result};
use crate::config::Config; use crate::config::Config;
use crate::imap::ImapConnector; use crate::imap::ImapConnector;
use crate::msg::Msg; use crate::msg::{Attachments, Msg, ReadableMsg};
use crate::output::print; use crate::output::print;
const DEFAULT_PAGE_SIZE: usize = 10; const DEFAULT_PAGE_SIZE: usize = 10;
@ -343,14 +342,15 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let account = config.find_account_by_name(account_name)?; let account = config.find_account_by_name(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?; let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let text_bodies = msg.text_bodies(&mime)?; let msg = imap_conn.read_msg(&mbox, &uid)?;
print(&output_type, json!({ "content": text_bodies }))?; let msg = ReadableMsg::from_bytes(&mime, &msg)?;
print(&output_type, msg)?;
imap_conn.logout(); imap_conn.logout();
} }
@ -358,21 +358,38 @@ fn run() -> Result<()> {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let account = config.find_account_by_name(account_name)?; let account = config.find_account_by_name(account_name)?;
let mut imap_conn = ImapConnector::new(&account)?; let mut imap_conn = ImapConnector::new(&account)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?);
let parts = msg.extract_attachments()?;
if parts.is_empty() { let msg = imap_conn.read_msg(&mbox, &uid)?;
println!("No attachment found for message {}", uid); let attachments = Attachments::from_bytes(&msg)?;
} else {
println!("{} attachment(s) found for message {}", parts.len(), uid); match output_type.as_str() {
parts.iter().for_each(|(filename, bytes)| { "text" => {
let filepath = config.downloads_filepath(&account, &filename); println!(
println!("Downloading {}", filename); "{} attachment(s) found for message {}",
fs::write(filepath, bytes).unwrap() attachments.0.len(),
}); uid
println!("Done!"); );
attachments.0.iter().for_each(|attachment| {
let filepath = config.downloads_filepath(&account, &attachment.filename);
println!("Downloading {}", &attachment.filename);
fs::write(filepath, &attachment.raw).unwrap()
});
println!("Done!");
}
"json" => {
attachments.0.iter().for_each(|attachment| {
let filepath = config.downloads_filepath(&account, &attachment.filename);
fs::write(filepath, &attachment.raw).unwrap()
});
print!("{{}}");
}
_ => (),
} }
imap_conn.logout(); imap_conn.logout();

View file

@ -6,6 +6,7 @@ use serde::{
Serialize, Serialize,
}; };
use std::{fmt, result}; use std::{fmt, result};
use uuid::Uuid;
use crate::config::{Account, Config}; use crate::config::{Account, Config};
use crate::table::{self, DisplayRow, DisplayTable}; use crate::table::{self, DisplayRow, DisplayTable};
@ -66,6 +67,133 @@ impl Serialize for Tpl {
} }
} }
// Attachments
#[derive(Debug)]
pub struct Attachment {
pub filename: String,
pub raw: Vec<u8>,
}
impl<'a> Attachment {
// TODO: put in common with ReadableMsg
pub fn from_part(part: &'a mailparse::ParsedMail) -> Self {
Self {
filename: part
.get_content_disposition()
.params
.get("filename")
.unwrap_or(&Uuid::new_v4().to_simple().to_string())
.to_owned(),
raw: part.get_body_raw().unwrap_or_default(),
}
}
}
#[derive(Debug)]
pub struct Attachments(pub Vec<Attachment>);
impl<'a> Attachments {
fn extract_from_part(&'a mut self, part: &'a mailparse::ParsedMail) {
if part.subparts.is_empty() {
let ctype = part
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
if !ctype.starts_with("text") {
self.0.push(Attachment::from_part(part));
}
} else {
part.subparts
.iter()
.for_each(|part| self.extract_from_part(part));
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let msg = mailparse::parse_mail(bytes)?;
let mut attachments = Self(vec![]);
attachments.extract_from_part(&msg);
Ok(attachments)
}
}
// Readable message
#[derive(Debug)]
pub struct ReadableMsg {
pub content: String,
pub has_attachment: bool,
}
impl Serialize for ReadableMsg {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
let mut state = serializer.serialize_struct("ReadableMsg", 2)?;
state.serialize_field("content", &self.content)?;
state.serialize_field("hasAttachment", if self.has_attachment { &1 } else { &0 })?;
state.end()
}
}
impl fmt::Display for ReadableMsg {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.content)
}
}
impl<'a> ReadableMsg {
fn flatten_parts(part: &'a mailparse::ParsedMail) -> Vec<&'a mailparse::ParsedMail<'a>> {
if part.subparts.is_empty() {
vec![part]
} else {
part.subparts
.iter()
.flat_map(Self::flatten_parts)
.collect::<Vec<_>>()
}
}
pub fn from_bytes(mime: &str, bytes: &[u8]) -> Result<Self> {
let msg = mailparse::parse_mail(bytes)?;
let (text_part, html_part, has_attachment) = Self::flatten_parts(&msg).into_iter().fold(
(None, None, false),
|(mut text_part, mut html_part, mut has_attachment), part| {
let ctype = part
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
if text_part.is_none() && ctype.starts_with("text/plain") {
text_part = part.get_body().ok();
} else {
if html_part.is_none() && ctype.starts_with("text/html") {
html_part = part.get_body().ok();
} else {
has_attachment = true
};
};
(text_part, html_part, has_attachment)
},
);
let content = if mime == "text/plain" {
text_part.or(html_part).unwrap_or_default()
} else {
html_part.or(text_part).unwrap_or_default()
};
Ok(Self {
content,
has_attachment,
})
}
}
// Message // Message
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -219,43 +347,13 @@ impl<'a> Msg {
Ok(text_bodies.join("\r\n")) Ok(text_bodies.join("\r\n"))
} }
fn extract_attachments_into(part: &mailparse::ParsedMail, parts: &mut Vec<(String, Vec<u8>)>) {
match part.subparts.len() {
0 => {
let content_disp = part.get_content_disposition();
let content_type = part
.get_headers()
.get_first_value("content-type")
.unwrap_or_default();
let default_attachment_name = format!("attachment-{}", parts.len());
let attachment_name = content_disp
.params
.get("filename")
.unwrap_or(&default_attachment_name)
.to_owned();
if !content_type.starts_with("text") {
parts.push((attachment_name, part.get_body_raw().unwrap_or_default()))
}
}
_ => {
part.subparts
.iter()
.for_each(|part| Self::extract_attachments_into(part, parts));
}
}
}
pub fn extract_attachments(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut parts = vec![];
Self::extract_attachments_into(&self.parse()?, &mut parts);
Ok(parts)
}
pub fn build_new_tpl(config: &Config, account: &Account) -> Result<Tpl> { pub fn build_new_tpl(config: &Config, account: &Account) -> Result<Tpl> {
let mut tpl = vec![]; let mut tpl = vec![];
// "Content" headers
tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
tpl.push("Content-Transfer-Encoding: 8bit".to_string());
// "From" header // "From" header
tpl.push(format!("From: {}", config.address(account))); tpl.push(format!("From: {}", config.address(account)));
@ -273,6 +371,10 @@ impl<'a> Msg {
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "Content" headers
tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
tpl.push("Content-Transfer-Encoding: 8bit".to_string());
// "From" header // "From" header
tpl.push(format!("From: {}", config.address(account))); tpl.push(format!("From: {}", config.address(account)));
@ -313,6 +415,10 @@ impl<'a> Msg {
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "Content" headers
tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
tpl.push("Content-Transfer-Encoding: 8bit".to_string());
// "From" header // "From" header
tpl.push(format!("From: {}", config.address(account))); tpl.push(format!("From: {}", config.address(account)));
@ -394,6 +500,10 @@ impl<'a> Msg {
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
// "Content" headers
tpl.push("Content-Type: text/plain; charset=utf-8".to_string());
tpl.push("Content-Transfer-Encoding: 8bit".to_string());
// "From" header // "From" header
tpl.push(format!("From: {}", config.address(account))); tpl.push(format!("From: {}", config.address(account)));