Ver código fonte

improve read and attachments commands

Clément DOUIN 4 anos atrás
pai
commit
10c523fd2c
4 arquivos alterados com 180 adições e 51 exclusões
  1. 1 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 34 17
      src/main.rs
  4. 144 34
      src/msg.rs

+ 1 - 0
Cargo.lock

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

+ 1 - 0
Cargo.toml

@@ -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"] }

+ 34 - 17
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)?;
-        print(&output_type, json!({ "content": text_bodies }))?;
+        let msg = imap_conn.read_msg(&mbox, &uid)?;
+        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() {
-            println!("No attachment found for message {}", uid);
-        } else {
-            println!("{} attachment(s) found for message {}", parts.len(), uid);
-            parts.iter().for_each(|(filename, bytes)| {
-                let filepath = config.downloads_filepath(&account, &filename);
-                println!("Downloading {}…", filename);
-                fs::write(filepath, bytes).unwrap()
-            });
-            println!("Done!");
+        let msg = imap_conn.read_msg(&mbox, &uid)?;
+        let attachments = Attachments::from_bytes(&msg)?;
+
+        match output_type.as_str() {
+            "text" => {
+                println!(
+                    "{} attachment(s) found for message {}",
+                    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!");
+            }
+            "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();

+ 144 - 34
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)));