diff --git a/src/ctx.rs b/src/ctx.rs index 1ce8af5b..dc8c71e5 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -22,7 +22,6 @@ impl<'a> Ctx<'a> { mbox: S, arg_matches: clap::ArgMatches<'a>, ) -> Self { - let mbox = mbox.to_string(); Self { diff --git a/src/msg/body.rs b/src/msg/body.rs index a3eb136a..185335ce 100644 --- a/src/msg/body.rs +++ b/src/msg/body.rs @@ -1,14 +1,13 @@ use error_chain::error_chain; use std::fmt; -use std::ops::{Deref, DerefMut}; use serde::Serialize; // == Macros == error_chain! { foreign_links { - ParseContentType(lettre::message::header::ContentTypeErr); + ParseContentType(lettre::message::header::ContentTypeErr); } } @@ -24,91 +23,129 @@ error_chain! { /// /// This part of the msg/msg would be stored in this struct. #[derive(Clone, Serialize, Debug, PartialEq, Eq)] -pub struct Body(String); +pub struct Body { + /// The text version of a body (if available) + pub text: Option, + + /// The html version of a body (if available) + pub html: Option, +} impl Body { - /// This function just returns a clone of it's content. If we use the - /// example from above, than you'll get a clone of the whole text. + /// Returns a new instance of `Body` without any attributes set. (Same as `Body::default()`) /// /// # Example - /// ``` - /// # use himalaya::msg::body::Body; - /// # fn main() { - /// let body = concat![ - /// "Dear Mr. Boss,\n", - /// "I like rust. It's an awesome language. *Change my mind*....\n", - /// "\n", - /// "Sincerely", - /// ]; + /// ```rust + /// use himalaya::msg::body::Body; /// - /// // create a new instance of `Body` - /// let body_struct = Body::from(body); + /// fn main() { + /// let body = Body::new(); /// - /// assert_eq!(body_struct.get_content(), body); - /// # } + /// let expected_body = Body { + /// text: None, + /// html: None, + /// }; + /// + /// assert_eq!(body, expected_body); + /// } /// ``` - pub fn get_content(&self) -> String { - self.0.clone() + pub fn new() -> Self { + Self::default() } -} -// == Traits == -impl Default for Body { - fn default() -> Self { - Self(String::new()) + /// Returns a new instance of `Body` with `text` set. + /// + /// # Example + /// ```rust + /// use himalaya::msg::body::Body; + /// + /// fn main() { + /// let body = Body::new_with_text("Text body"); + /// + /// let expected_body = Body { + /// text: Some("Text body".to_string()), + /// html: None, + /// }; + /// + /// assert_eq!(body, expected_body); + /// } + /// ``` + pub fn new_with_text(text: S) -> Self { + Self { + text: Some(text.to_string()), + html: None, + } } -} -impl fmt::Display for Body { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!(formatter, "{}", &self.0) + /// Returns a new instance of `Body` with `html` set. + /// + /// # Example + /// ```rust + /// use himalaya::msg::body::Body; + /// + /// fn main() { + /// let body = Body::new_with_html("Html body"); + /// + /// let expected_body = Body { + /// text: None, + /// html: Some("Html body".to_string()), + /// }; + /// + /// assert_eq!(body, expected_body); + /// } + /// ``` + pub fn new_with_html(html: S) -> Self { + Self { + text: None, + html: Some(html.to_string()), + } } -} - -impl Deref for Body { - type Target = String; - fn deref(&self) -> &Self::Target { - &self.0 + /// Returns a new isntance of `Body` with `text` and `html` set. + /// + /// # Example + /// ```rust + /// use himalaya::msg::body::Body; + /// + /// fn main() { + /// let body = Body::new_with_both("Text body", "Html body"); + /// + /// let expected_body = Body { + /// text: Some("Text body".to_string()), + /// html: Some("Html body".to_string()), + /// }; + /// + /// assert_eq!(body, expected_body); + /// } + /// ``` + pub fn new_with_both(text: S, html: S) -> Self { + Self { + text: Some(text.to_string()), + html: Some(html.to_string()), + } } } -impl DerefMut for Body { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 +// == Traits == +impl Default for Body { + fn default() -> Self { + Self { + text: None, + html: None, + } } } -// -- From's -- -/// Give in a `&str` to create a new instance of `Body`. -/// -/// # Example -/// ``` -/// # use himalaya::msg::body::Body; -/// # fn main() { -/// Body::from("An awesome string!"); -/// # } -/// ``` -impl From<&str> for Body { - fn from(string: &str) -> Self { - Self(string.to_string()) - } -} +impl fmt::Display for Body { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let content = if let Some(text) = self.text.clone() { + text + } else if let Some(html) = self.html.clone() { + html + } else { + String::new() + }; -/// Give in a [`String`] to create a new instance of `Body`. -/// -/// # Example -/// ``` -/// # use himalaya::msg::body::Body; -/// # fn main() { -/// let body_content = String::from("A awesome content."); -/// Body::from(body_content); -/// # } -/// ``` -/// -/// [`String`]: https://doc.rust-lang.org/std/string/struct.String.html -impl From for Body { - fn from(string: String) -> Self { - Self(string) + write!(formatter, "{}", content) } } diff --git a/src/msg/cli.rs b/src/msg/cli.rs index 7d89544c..a27d329b 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -1,6 +1,6 @@ use super::body::Body; use super::envelope::Envelope; -use super::model::{Msg, Msgs}; +use super::model::{Msg, Msgs, MsgSerialized}; use url::Url; use atty::Stream; @@ -348,9 +348,8 @@ fn msg_matches_read(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { if raw { ctx.output.print(msg.get_raw()?); } else { - ctx.output.print(msg.body); + ctx.output.print(MsgSerialized::from(&msg)); } - imap_conn.logout(); Ok(true) } @@ -503,7 +502,7 @@ pub fn msg_matches_mailto(ctx: &Ctx, url: &Url) -> Result<()> { }; let mut msg = Msg::new_with_envelope(&ctx, envelope); - msg.body = Body::from(body.as_ref()); + msg.body = Body::new_with_text(body); msg_interaction(&ctx, &mut msg, &mut imap_conn)?; imap_conn.logout(); @@ -753,7 +752,7 @@ fn override_msg_with_args(msg: &mut Msg, matches: &clap::ArgMatches) { } }; - let body = Body::from(body); + let body = Body::new_with_text(body); // -- Creating and printing -- let envelope = Envelope { @@ -779,7 +778,7 @@ fn tpl_matches_new(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { override_msg_with_args(&mut msg, &matches); trace!("Message: {:?}", msg); - ctx.output.print(msg); + ctx.output.print(MsgSerialized::from(&msg)); Ok(true) } @@ -797,7 +796,7 @@ fn tpl_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { override_msg_with_args(&mut msg, &matches); trace!("Message: {:?}", msg); - ctx.output.print(msg); + ctx.output.print(MsgSerialized::from(&msg)); Ok(true) } @@ -815,7 +814,7 @@ fn tpl_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { override_msg_with_args(&mut msg, &matches); trace!("Message: {:?}", msg); - ctx.output.print(msg); + ctx.output.print(MsgSerialized::from(&msg)); Ok(true) } diff --git a/src/msg/model.rs b/src/msg/model.rs index 2ae745f1..b80ab744 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -1,5 +1,3 @@ -use std::ops::Deref; - use super::attachment::Attachment; use super::body::Body; use super::envelope::Envelope; @@ -36,9 +34,8 @@ use colorful::Colorful; // == Macros == error_chain::error_chain! { errors { - // An error appeared, when it tried to parse the body of the msg! ParseBody (err: String) { - description("Couldn't get the body of the parsed msg."), + description("An error appeared, when trying to parse the body of the msg!"), display("Couldn't get the body of the parsed msg: {}", err), } @@ -72,10 +69,39 @@ error_chain::error_chain! { } // == Msg == +/// Represents the msg in a serializeable form with additional values. +/// This struct-type makes it also possible to print the msg in a serialized form or in a normal +/// form. +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct MsgSerialized { + /// First of all, the messge in general + #[serde(flatten)] + pub msg: Msg, + + /// A bool which indicates if the current msg includes attachments or not. + pub has_attachment: bool, +} + +impl From<&Msg> for MsgSerialized { + fn from(msg: &Msg) -> Self { + let has_attachment = msg.attachments.is_empty(); + + Self { + msg: msg.clone(), + has_attachment, + } + } +} + +impl fmt::Display for MsgSerialized { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "{}", self.msg.body) + } +} + /// This struct represents a whole msg with its attachments, body-content /// and its envelope. -#[derive(Debug, Serialize, PartialEq, Eq, Clone)] -#[serde(rename_all = "camelCase")] +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] pub struct Msg { /// All added attachments are listed in this vector. pub attachments: Vec, @@ -103,7 +129,6 @@ pub struct Msg { date: Option, /// The msg but in raw. - #[serde(skip_serializing)] raw: Vec, } @@ -170,7 +195,7 @@ impl Msg { envelope.signature = ctx.config.signature(&ctx.account); } - let body = Body::from(if let Some(sig) = envelope.signature.as_ref() { + let body = Body::new_with_text(if let Some(sig) = envelope.signature.as_ref() { format!("\n{}", sig) } else { String::from("\n") @@ -277,6 +302,9 @@ impl Msg { // each line which includes a string. let mut new_body = self .body + .text + .clone() + .unwrap_or_default() .lines() .map(|line| { let space = if line.starts_with(">") { "" } else { " " }; @@ -291,7 +319,7 @@ impl Msg { new_body.push_str(&sig) } - self.body = Body::from(new_body); + self.body = Body::new_with_text(new_body); self.envelope = new_envelope; self.attachments.clear(); @@ -352,10 +380,10 @@ impl Msg { // apply a line which should indicate where the forwarded message begins body.push_str(&format!( "\n\n---------- Forwarded Message ----------\n{}", - &self.body.deref().replace("\r", ""), + self.body.text.clone().unwrap_or_default().replace("\r", ""), )); - self.body = Body::from(body); + self.body = Body::new_with_text(body); } /// Returns the bytes of the *sendable message* of the struct! @@ -467,7 +495,7 @@ impl Msg { self.envelope = Envelope::from(&parsed); match parsed.get_body() { - Ok(body) => self.body = Body::from(body), + Ok(body) => self.body = Body::new_with_text(body), Err(err) => return Err(ErrorKind::ParseBody(err.to_string()).into()), }; @@ -524,7 +552,7 @@ impl Msg { /// }, /// ); /// - /// msg.body = Body::from("A little text."); + /// msg.body = Body::new_with_text("A little text."); /// let sendable_msg = msg.to_sendable_msg().unwrap(); /// /// // now send the msg. Hint: Do the appropriate error handling here! @@ -675,11 +703,19 @@ impl Msg { let mut msg_parts = MultiPart::mixed().build(); // -- Body -- - let msg_body = SinglePart::builder() - .header(ContentType::TEXT_PLAIN) - .header(self.envelope.encoding) - .body(self.body.get_content()); - msg_parts = msg_parts.singlepart(msg_body); + if self.body.text.is_some() && self.body.html.is_some() { + msg_parts = msg_parts.multipart(MultiPart::alternative_plain_html( + self.body.text.clone().unwrap(), + self.body.html.clone().unwrap(), + )); + } else { + let msg_body = SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .header(self.envelope.encoding) + .body(self.body.text.clone().unwrap_or_default()); + + msg_parts = msg_parts.singlepart(msg_body); + } // -- Attachments -- for attachment in self.attachments.iter() { @@ -725,6 +761,11 @@ impl Msg { pub fn get_encoding(&self) -> ContentTransferEncoding { self.envelope.encoding } + + /// Returns the whole message: Header + Body as a String + pub fn get_full_message(&self) -> String { + format!("{}\n{}", self.envelope.get_header_as_string(), self.body) + } } // -- Traits -- @@ -746,12 +787,7 @@ impl Default for Msg { impl fmt::Display for Msg { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - write!( - formatter, - "{}\n{}", - self.envelope.get_header_as_string(), - self.body, - ) + write!(formatter, "{}", self.body) } } @@ -837,27 +873,25 @@ impl TryFrom<&Fetch> for Msg { }; // -- Storing the information (body) -- - let mut body = String::new(); + let mut body = Body::new(); if let Some(parsed) = parsed { // Ok, so some mails have their mody wrapped in a multipart, some // don't. This condition hits, if the body isn't in a multipart, so we can // immediately fetch the body from the first part of the mail. - if parsed.ctype.mimetype == "text/plain" { - // Apply the body (if there exists one) - if let Ok(parsed_body) = parsed.get_body() { - debug!("Stored the body of the msg."); - body = parsed_body; - } - } + match parsed.ctype.mimetype.as_ref() { + "text/plain" => body.text = parsed.get_body().ok(), + "text/html" => body.html = parsed.get_body().ok(), + _ => (), + }; for subpart in &parsed.subparts { // now it might happen, that the body is *in* a multipart, if // that's the case, look, if we've already applied a body // (body.is_empty()) and set it, if needed - if body.is_empty() && subpart.ctype.mimetype == "text/plain" { - if let Ok(subpart_body) = subpart.get_body() { - body = subpart_body; - } + if body.text.is_none() && subpart.ctype.mimetype == "text/plain" { + body.text = subpart.get_body().ok(); + } else if body.html.is_none() && subpart.ctype.mimetype == "text/html" { + body.html = subpart.get_body().ok(); } // otherise it's a normal attachment, like a PNG file or // something like that @@ -880,7 +914,7 @@ impl TryFrom<&Fetch> for Msg { attachments, flags, envelope, - body: Body::from(body), + body: Body::new_with_text(body), uid, date, raw, @@ -1015,7 +1049,7 @@ mod tests { ..Envelope::default() }, // The signature should be added automatically - body: Body::from("\n"), + body: Body::new_with_text("\n"), ..Msg::default() }; @@ -1048,7 +1082,7 @@ mod tests { signature: Some(String::from("\n-- \nSignature")), ..Envelope::default() }, - body: Body::from("\n\n-- \nSignature"), + body: Body::new_with_text("\n\n-- \nSignature"), ..Msg::default() }; @@ -1089,7 +1123,7 @@ mod tests { message_id: Some("<1234@local.machine.example>".to_string()), ..Envelope::default() }, - body: Body::from(concat![ + body: Body::new_with_text(concat![ "This is a message just to say hello.\n", "So, \"Hello\".", ]), @@ -1121,7 +1155,7 @@ mod tests { subject: Some("Have you heard of himalaya?".to_string()), ..Envelope::default() }, - body: Body::from(concat!["A body test\n", "\n", "Sincerely",]), + body: Body::new_with_text(concat!["A body test\n", "\n", "Sincerely",]), ..Msg::default() }; @@ -1140,7 +1174,7 @@ mod tests { in_reply_to: Some("<1234@local.machine.example>".to_string()), ..Envelope::default() }, - body: Body::from(concat![ + body: Body::new_with_text(concat![ "> This is a message just to say hello.\n", "> So, \"Hello\".", ]), @@ -1157,7 +1191,7 @@ mod tests { in_reply_to: Some("<3456@example.net>".to_string()), ..Envelope::default() }, - body: Body::from(concat![ + body: Body::new_with_text(concat![ ">> This is a message just to say hello.\n", ">> So, \"Hello\".", ]), @@ -1181,7 +1215,7 @@ mod tests { subject: Some("Re: Have you heard of himalaya?".to_string()), ..Envelope::default() }, - body: Body::from(concat!["> A body test\n", "> \n", "> Sincerely"]), + body: Body::new_with_text(concat!["> A body test\n", "> \n", "> Sincerely"]), ..Msg::default() }; @@ -1256,7 +1290,7 @@ mod tests { }, ); - msg.body = Body::from(concat!["The body text, nice!\n", "Himalaya is nice!",]); + msg.body = Body::new_with_text(concat!["The body text, nice!\n", "Himalaya is nice!",]); // == Expected Results == let expected_msg = Msg { @@ -1267,7 +1301,7 @@ mod tests { subject: Some(String::from("Fwd: Test subject")), ..Envelope::default() }, - body: Body::from(concat![ + body: Body::new_with_text(concat![ "\n-- \nlol\n", "\n", "---------- Forwarded Message ----------\n", @@ -1320,7 +1354,7 @@ mod tests { cc: Some(vec![String::from("")]), ..Envelope::default() }, - body: Body::from("\n\n-- \nAccount Signature"), + body: Body::new_with_text("\n\n-- \nAccount Signature"), ..Msg::default() }; @@ -1382,7 +1416,7 @@ mod tests { to: vec![String::from("To ")], ..Envelope::default() }, - body: Body::from("Account Signature\n"), + body: Body::new_with_text("Account Signature\n"), ..Msg::default() }; @@ -1403,7 +1437,7 @@ mod tests { custom_headers: Some(custom_headers), ..Envelope::default() }, - body: Body::from("Account Signature\n"), + body: Body::new_with_text("Account Signature\n"), ..Msg::default() };