mirror of
https://github.com/soywod/himalaya.git
synced 2025-04-22 17:23:27 +00:00
improve read and attachments commands
This commit is contained in:
parent
f7ed99d55f
commit
10c523fd2c
4 changed files with 180 additions and 51 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -240,6 +240,7 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
"toml",
|
"toml",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
47
src/main.rs
47
src/main.rs
|
@ -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,22 +358,39 @@ 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
|
||||||
|
);
|
||||||
|
|
||||||
|
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!");
|
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();
|
||||||
}
|
}
|
||||||
|
|
178
src/msg.rs
178
src/msg.rs
|
@ -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)));
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue