use anyhow::{anyhow, Context, Error, Result}; use imap::types::{Fetch, Flag, ZeroCopy}; use log::debug; use mailparse; use super::{attachment::Attachment, body::Body, headers::Headers}; use crate::{ config::entity::Account, domain::msg::flag::entity::Flags, ui::table::{Cell, Row, Table}, }; #[cfg(not(test))] use crate::input; use serde::Serialize; use lettre::message::{ header::ContentTransferEncoding, header::ContentType, Attachment as lettre_Attachment, Mailbox, Message, MultiPart, SinglePart, }; use std::{ convert::{From, TryFrom}, fmt, }; /// 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, /// The raw mail as a string pub raw: String, } impl TryFrom<&Msg> for MsgSerialized { type Error = Error; fn try_from(msg: &Msg) -> Result { let has_attachment = msg.attachments.is_empty(); let raw = msg.get_raw_as_string()?; Ok(Self { msg: msg.clone(), has_attachment, raw, }) } } impl fmt::Display for MsgSerialized { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!(formatter, "{}", self.msg) } } /// This struct represents a whole msg with its attachments, body-content /// and its headers. #[derive(Debug, PartialEq, Eq, Clone, Serialize)] pub struct Msg { /// All added attachments are listed in this vector. pub attachments: Vec, /// The flags of this msg. pub flags: Flags, /// All information of the headers (sender, from, to and so on) // headers: HashMap>, pub headers: Headers, /// This variable stores the body of the msg. /// This includes the general content text and the signature. pub body: Body, /// The UID of the msg. In general, a message should already have one, unless you're writing a /// new message, then we're generating it. uid: Option, /// The origination date field. Read [the RFC here] here for more /// information. /// /// [the RFC here]: /// https://www.rfc-editor.org/rfc/rfc5322.html#section-3.6.1 date: Option, /// The msg but in raw. #[serde(skip_serializing)] raw: Vec, } impl Msg { /// Creates a completely new msg where two header fields are set: /// - [`from`] /// - and [`signature`] /// /// [`from`]: struct.Headers.html#structfield.from /// [`signature`]: struct.Headers.html#structfield.signature /// /// # Example /// ///
/// /// ``` /// # use himalaya::msg::model::Msg; /// # use himalaya::msg::headers::Headers; /// # use himalaya::config::model::Account; /// # use himalaya::ctx::Ctx; /// /// # fn main() { /// // -- Accounts -- /// let ctx1 = Ctx { /// account: Account::new_with_signature(Some("Soywod"), "clement.douin@posteo.net", /// Some("Account Signature") /// ), /// .. Ctx::default() /// }; /// let ctx2 = Ctx { /// account: Account::new(None, "tornax07@gmail.com"), /// .. Ctx::default() /// }; /// /// // Creating messages /// let msg1 = Msg::new(&ctx1); /// let msg2 = Msg::new(&ctx2); /// /// let expected_headers1 = Headers { /// from: vec![String::from("Soywod ")], /// // the signature of the account is stored as well /// signature: Some(String::from("\n-- \nAccount Signature")), /// ..Headers::default() /// }; /// /// let expected_headers2 = Headers { /// from: vec![String::from("tornax07@gmail.com")], /// ..Headers::default() /// }; /// /// assert_eq!(msg1.headers, expected_headers1, /// "{:#?}, {:#?}", /// msg1.headers, expected_headers1); /// assert_eq!(msg2.headers, expected_headers2, /// "{:#?}, {:#?}", /// msg2.headers, expected_headers2); /// # } /// ``` /// ///
pub fn new(account: &Account) -> Self { Self::new_with_headers(&account, Headers::default()) } /// This function does the same as [`Msg::new`] but you can apply a custom /// [`headers`] when calling the function instead of using the default one /// from the [`Msg::new`] function. /// /// [`Msg::new`]: struct.Msg.html#method.new /// [`headers`]: struct.Headers.html pub fn new_with_headers(account: &Account, mut headers: Headers) -> Self { if headers.from.is_empty() { headers.from = vec![account.address()]; } if let None = headers.signature { headers.signature = Some(account.signature.to_owned()); } let body = Body::new_with_text(if let Some(sig) = headers.signature.as_ref() { format!("\n{}", sig) } else { String::from("\n") }); Self { headers, body, ..Self::default() } } /// Converts the message into a Reply message. /// An [`Account`] struct is needed to set the `From:` field. /// /// # Changes /// The value on the left side, represents the header *after* the function /// call, while the value on the right side shows the data *before* the /// function call. So if we pick up the first example of `reply_all = /// false`, then we can see, that the value of `ReplyTo:` is moved into the /// `To:` header field in this function call. /// /// - `reply_all = false`: /// - `To:` = `ReplyTo:` otherwise from `From:` /// - attachments => cleared /// - `From:` = Emailaddress of the current user account /// - `Subject:` = "Re:" + `Subject` /// - `in_reply_to` = Old Message ID /// - `Cc:` = cleared /// /// - `reply_all = true`: /// - `To:` = `ReplyTo:` + Addresses in `To:` /// - `Cc:` = All CC-Addresses /// - The rest: Same as in `reply_all = false` /// /// It'll add for each line in the body the `>` character in the beginning /// of each line. /// /// # Example /// [Here] you can see an example how a discussion with replies could look /// like. /// /// [Here]: https://www.rfc-editor.org/rfc/rfc5322.html#page-46 /// [`Account`]: struct.Account.html /// // TODO: References field is missing, but the imap-crate can't implement it // currently. pub fn change_to_reply(&mut self, account: &Account, reply_all: bool) -> Result<()> { let subject = self .headers .subject .as_ref() .map(|sub| { if sub.starts_with("Re:") { sub.to_owned() } else { format!("Re: {}", sub) } }) .unwrap_or_default(); // The new fields let mut to: Vec = Vec::new(); let mut cc = None; if reply_all { let email_addr: lettre::Address = account.email.parse()?; for addr in self.headers.to.iter() { let addr_parsed: Mailbox = addr.parse()?; // we don't want to receive the msg which we have just sent, // don't we? if addr_parsed.email != email_addr { to.push(addr.to_string()); } } // Also use the addresses in the "Cc:" field cc = self.headers.cc.clone(); } // Now add the addresses in the `Reply-To:` Field or from the `From:` // field. if let Some(reply_to) = &self.headers.reply_to { to.append(&mut reply_to.clone()); } else { // if the "Reply-To" wasn't set from the sender, then we're just // replying to the addresses in the "From:" field to.append(&mut self.headers.from.clone()); }; let new_headers = Headers { from: vec![account.address()], to, cc, subject: Some(subject), in_reply_to: self.headers.message_id.clone(), signature: Some(account.signature.to_owned()), // and clear the rest of the fields ..Headers::default() }; // comment "out" the body of the msg, by adding the `>` characters to // 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 { " " }; format!(">{}{}", space, line) }) .collect::>() .join("\n"); // also add the the signature in the end if let Some(sig) = new_headers.signature.as_ref() { new_body.push('\n'); new_body.push_str(&sig) } self.body = Body::new_with_text(new_body); self.headers = new_headers; self.attachments.clear(); Ok(()) } /// Changes the msg/msg to a forwarding msg/msg. /// /// # Changes /// Calling this function will change apply the following to the current /// message: /// /// - `Subject:`: `"Fwd: "` will be added in front of the "old" subject /// - `"---------- Forwarded Message ----------"` will be added on top of /// the body. /// /// # Example /// ```text /// Subject: Test subject /// ... /// /// Hi, /// I use Himalaya /// /// Sincerely /// ``` /// /// will be changed to /// /// ```text /// Subject: Fwd: Test subject /// Sender: /// ... /// /// > Hi, /// > I use Himalaya /// > /// > Sincerely /// ``` pub fn change_to_forwarding(&mut self, account: &Account) { // -- Header -- let old_subject = self.headers.subject.clone().unwrap_or(String::new()); self.headers = Headers { subject: Some(format!("Fwd: {}", old_subject)), sender: Some(account.address()), // and use the rest of the headers ..self.headers.clone() }; let mut body = String::new(); // -- Body -- // apply a line which should indicate where the forwarded message begins body.push_str(&format!( "\n---------- Forwarded Message ----------\n{}", self.body.text.clone().unwrap_or_default().replace("\r", ""), )); body.push_str(&account.signature); self.body = Body::new_with_text(body); } /// Returns the bytes of the *sendable message* of the struct! pub fn into_bytes(&mut self) -> Result> { // parse the whole msg first let parsed = self.to_sendable_msg()?; return Ok(parsed.formatted()); } /// Let the user edit the body of the msg. /// /// It'll enter the headers of the headers into the draft-file *if they're /// not [`None`]!*. /// /// # Example /// ```no_run /// use himalaya::config::model::Account; /// use himalaya::msg::model::Msg; /// use himalaya::ctx::Ctx; /// /// fn main() { /// let ctx = Ctx { /// account: Account::new(Some("Name"), "some@msg.asdf"), /// .. Ctx::default() /// }; /// let mut msg = Msg::new(&ctx); /// /// // In this case, only the header fields "From:" and "To:" are gonna /// // be editable, because the other headers fields are set to "None" /// // per default! /// msg.edit_body().unwrap(); /// } /// ``` /// /// Now enable some headers: /// /// ```no_run /// use himalaya::config::model::Account; /// use himalaya::msg::{headers::Headers, model::Msg}; /// use himalaya::ctx::Ctx; /// /// fn main() { /// let ctx = Ctx { /// account: Account::new(Some("Name"), "some@msg.asdf"), /// .. Ctx::default() /// }; /// /// let mut msg = Msg::new_with_headers( /// &ctx, /// Headers { /// bcc: Some(Vec::new()), /// cc: Some(Vec::new()), /// ..Headers::default() /// }, /// ); /// /// // The "Bcc:" and "Cc:" header fields are gonna be editable as well /// msg.edit_body().unwrap(); /// } /// ``` /// /// # Errors /// In generel an error should appear if /// - The draft or changes couldn't be saved /// - The changed msg can't be parsed! (You wrote some things wrong...) pub fn edit_body(&mut self) -> Result<()> { // First of all, we need to create our template for the user. This // means, that the header needs to be added as well! let msg = self.to_string(); // We don't let this line compile, if we're doing // tests, because we just need to look, if the headers are set // correctly #[cfg(not(test))] let msg = input::open_editor_with_tpl(msg.as_bytes())?; // refresh the state of the msg self.parse_from_str(&msg)?; Ok(()) } /// Read the string of the argument `content` and store it's values into the /// struct. It stores the headers-fields and the body of the msg. /// /// **Hint: The signature can't be fetched of the content at the moment!** /// /// # Example /// ``` /// use himalaya::config::model::Account; /// use himalaya::msg::model::Msg; /// use himalaya::ctx::Ctx; /// /// fn main() { /// let content = concat![ /// "Subject: Himalaya is nice\n", /// "To: Soywod \n", /// "From: TornaxO7 \n", /// "Bcc: third_person@msg.com,rofl@yeet.com\n", /// "\n", /// "You should use himalaya, it's a nice program :D\n", /// "\n", /// "Sincerely\n", /// ]; /// /// let ctx = Ctx { /// account: Account::new(Some("Username"), "some@msg.com"), /// .. Ctx::default() /// }; /// /// // create the message /// let mut msg = Msg::new(&ctx); /// /// // store the information given by the `content` variable which /// // represents our current msg /// msg.parse_from_str(content); /// } /// ``` pub fn parse_from_str(&mut self, content: &str) -> Result<()> { let parsed = mailparse::parse_mail(content.as_bytes()) .with_context(|| format!("How the message looks like currently:\n{}", self))?; self.headers = Headers::from(&parsed); match parsed.get_body() { Ok(body) => self.body = Body::new_with_text(body), Err(err) => return Err(anyhow!(err.to_string())), }; Ok(()) } /// Add an attachment to the msg from the local machine by the given path. /// /// # Example /// ``` /// use himalaya::config::model::Account; /// use himalaya::msg::headers::Headers; /// use himalaya::msg::model::Msg; /// use himalaya::ctx::Ctx; /// /// fn main() { /// let ctx = Ctx { /// account: Account::new(Some("Name"), "address@msg.com"), /// .. Ctx::default() /// }; /// let mut msg = Msg::new(&ctx); /// /// // suppose we have a Screenshot saved in our home directory /// // Remember: Currently himalaya can't expand tilde ('~') and shell variables /// msg.add_attachment("/home/bruh/Screenshot.png"); /// } /// ``` /// // THOUGHT: Error handling? pub fn add_attachment(&mut self, path: &str) { if let Ok(new_attachment) = Attachment::try_from(path) { self.attachments.push(new_attachment); } } /// This function will use the information of the `Msg` struct and creates /// a sendable msg with it. It uses the `Msg.headers` and /// `Msg.attachments` fields for that. /// /// # Example /// ```no_run /// use himalaya::config::model::Account; /// use himalaya::smtp; /// /// use himalaya::msg::{body::Body, headers::Headers, model::Msg}; /// /// use himalaya::imap::model::ImapConnector; /// /// use himalaya::ctx::Ctx; /// /// use imap::types::Flag; /// /// fn main() { /// let ctx = Ctx { /// account: Account::new(Some("Name"), "name@msg.net"), /// .. Ctx::default() /// }; /// /// let mut imap_conn = ImapConnector::new(&ctx.account).unwrap(); /// let mut msg = Msg::new_with_headers( /// &ctx, /// Headers { /// to: vec!["someone ".to_string()], /// ..Headers::default() /// }, /// ); /// /// 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! /// smtp::send(&ctx.account, &sendable_msg).unwrap(); /// /// // also say to the server of the account user, that we've just sent /// // new message /// msg.flags.insert(Flag::Seen); /// imap_conn.append_msg("Sent", &mut msg).unwrap(); /// /// imap_conn.logout(); /// } /// ``` pub fn to_sendable_msg(&mut self) -> Result { // == Header of Msg == // This variable will hold all information of our msg let mut msg = Message::builder(); // -- Must-have-fields -- // add "from" for mailaddress in &self.headers.from { msg = msg.from( match mailaddress .parse() .with_context(|| "cannot parse `From` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } // add "to" for mailaddress in &self.headers.to { msg = msg.to( match mailaddress .parse() .with_context(|| "cannot parse `To` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } // -- Optional fields -- // add "bcc" if let Some(bcc) = &self.headers.bcc { for mailaddress in bcc { msg = msg.bcc( match mailaddress .parse() .with_context(|| "cannot parse `Bcc` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } } // add "cc" if let Some(cc) = &self.headers.cc { for mailaddress in cc { msg = msg.cc( match mailaddress .parse() .with_context(|| "cannot parse `Cc` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } } // add "in_reply_to" if let Some(in_reply_to) = &self.headers.in_reply_to { msg = msg.in_reply_to( match in_reply_to .parse() .with_context(|| "cannot parse `In-Reply-To` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } // add message-id if it exists msg = match self.headers.message_id.clone() { Some(message_id) => msg.message_id(Some(message_id)), None => { // extract the domain like "gmail.com" let mailbox: lettre::message::Mailbox = self.headers.from[0].parse()?; let domain = mailbox.email.domain(); // generate a new UUID let new_msg_id = format!("{}@{}", uuid::Uuid::new_v4().to_string(), domain); msg.message_id(Some(new_msg_id)) } }; // add "reply-to" if let Some(reply_to) = &self.headers.reply_to { for mailaddress in reply_to { msg = msg.reply_to( match mailaddress .parse() .with_context(|| "cannot parse `Reply-To` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } } // add "sender" if let Some(sender) = &self.headers.sender { msg = msg.sender( match sender .parse() .with_context(|| "cannot parse `Sender` header") { Ok(from) => from, Err(err) => return Err(anyhow!(err.to_string())), }, ); } // add subject if let Some(subject) = &self.headers.subject { msg = msg.subject(subject); } // -- Body + Attachments -- // In this part, we'll add the content of the msg. This means the body // and the attachments of the msg. // this variable will store all "sections" or attachments of the msg let mut msg_parts = MultiPart::mixed().build(); // -- 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.headers.encoding) .body(self.body.text.clone().unwrap_or_default()); msg_parts = msg_parts.singlepart(msg_body); } // -- Attachments -- for attachment in self.attachments.iter() { let msg_attachment = lettre_Attachment::new(attachment.filename.clone()); let msg_attachment = msg_attachment.body(attachment.body_raw.clone(), attachment.content_type.clone()); msg_parts = msg_parts.singlepart(msg_attachment); } Ok(msg .multipart(msg_parts) // whenever an error appears, print out the messge as well to see what might be the // error .context(format!("-- Current Message --\n{}", self))?) } /// Returns the uid of the msg. /// /// # Hint /// The uid is set if you *send* a *new* message or if you receive a message of the server. So /// in general you can only get a `Some(...)` from this function, if it's a fetched msg /// otherwise you'll get `None`. pub fn get_uid(&self) -> Option { self.uid } /// It returns the raw version of the Message. In general it's the structure /// how you get it if you get the data from the fetch. It's the output if /// you read a message with the `--raw` flag like this: `himalaya read /// --raw `. pub fn get_raw(&self) -> Vec { self.raw.clone() } /// Returns the raw mail as a string instead of a Vector of bytes. pub fn get_raw_as_string(&self) -> Result { let raw_message = String::from_utf8(self.raw.clone()) .context(format!("cannot parse raw message as string"))?; Ok(raw_message) } /// Returns the [`ContentTransferEncoding`] of the body. pub fn get_encoding(&self) -> ContentTransferEncoding { self.headers.encoding } /// Returns the whole message: Header + Body as a String pub fn get_full_message(&self) -> String { format!("{}\n{}", self.headers.get_header_as_string(), self.body) } } // -- Traits -- impl Default for Msg { fn default() -> Self { Self { attachments: Vec::new(), flags: Flags::default(), headers: Headers::default(), body: Body::default(), // the uid is generated in the "to_sendable_msg" function if the server didn't apply a // message id to it. uid: None, date: None, raw: Vec::new(), } } } impl fmt::Display for Msg { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { write!( formatter, "{}\n{}", self.headers.get_header_as_string(), self.body ) } } impl Table for Msg { fn head() -> Row { Row::new() .cell(Cell::new("UID").bold().underline().white()) .cell(Cell::new("FLAGS").bold().underline().white()) .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) .cell(Cell::new("FROM").bold().underline().white()) .cell(Cell::new("DATE").bold().underline().white()) } fn row(&self) -> Row { let is_seen = !self.flags.contains(&Flag::Seen); // The data which will be shown in the row let uid = self.get_uid().unwrap_or(0); let flags = self.flags.get_signs(); let subject = self.headers.subject.clone().unwrap_or_default(); let mut from = String::new(); let date = self.date.clone().unwrap_or(String::new()); for from_addr in self.headers.from.iter() { let mut address_iter = from_addr.split_ascii_whitespace(); if let Some(name) = address_iter.next() { from.push_str(&format!("{}, ", name)); } else if let Some(address) = address_iter.next() { from.push_str(&format!("{}, ", address)); } else { from.push_str("UNKNWON"); } } // remove trailing whitespace + the ',' let mut from = from.trim_end().to_string(); from.pop(); Row::new() .cell(Cell::new(&uid.to_string()).bold_if(is_seen).red()) .cell(Cell::new(&flags).bold_if(is_seen).white()) .cell(Cell::new(&subject).shrinkable().bold_if(is_seen).green()) .cell(Cell::new(&from).bold_if(is_seen).blue()) .cell(Cell::new(&date).bold_if(is_seen).yellow()) } } // -- From's -- /// Load the data from a fetched msg and store them in the msg-struct. /// Please make sure that the fetch includes the following query: /// /// - UID (optional) /// - FLAGS (optional) /// - ENVELOPE (optional) /// - INTERNALDATE /// - BODY[] (optional) impl TryFrom<&Fetch> for Msg { type Error = Error; fn try_from(fetch: &Fetch) -> Result { // -- Preparations -- // We're preparing the variables first, which will hold the data of the // fetched msg. let mut attachments = Vec::new(); let flags = Flags::from(fetch.flags()); let headers = Headers::try_from(fetch.envelope())?; let uid = fetch.uid; let date = fetch .internal_date() .map(|date| date.naive_local().to_string()); let raw = match fetch.body() { Some(body) => body.to_vec(), None => Vec::new(), }; // Get the content of the msg. Here we have to look (important!) if // the fetch even includes a body or not, since the `BODY[]` query is // only *optional*! let parsed = // the empty array represents an invalid body, so we can enter the // `Err` arm if the body-query wasn't applied match mailparse::parse_mail(raw.as_slice()) { Ok(parsed) => { debug!("Fetch has a body to parse."); Some(parsed) }, Err(_) => { debug!("Fetch hasn't a body to parse."); None }, }; // -- Storing the information (body) -- 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. 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.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 else if let Some(attachment) = Attachment::from_parsed_mail(subpart) { attachments.push(attachment); } // this shouldn't happen, since this would mean, that's neither an attachment nor // the body of the mail but something else. Log that! else { println!( "Unknown attachment with the following mime-type: {}", subpart.ctype.mimetype, ); } } } Ok(Self { attachments, flags, headers, body: Body::new_with_text(body), uid, date, raw, }) } } impl TryFrom<&str> for Msg { type Error = Error; fn try_from(content: &str) -> Result { let mut msg = Msg::default(); msg.parse_from_str(content)?; Ok(msg) } } // == Msgs == /// A Type-Safety struct which stores a vector of Messages. #[derive(Debug, Serialize)] pub struct Msgs(pub Vec); impl Msgs { pub fn new() -> Self { Self(Vec::new()) } } // -- From's -- impl<'mails> TryFrom<&'mails ZeroCopy>> for Msgs { type Error = Error; fn try_from(fetches: &'mails ZeroCopy>) -> Result { // the content of the Msgs-struct let mut mails = Vec::new(); for fetch in fetches.iter().rev() { mails.push(Msg::try_from(fetch)?); } Ok(Self(mails)) } } // -- Traits -- impl fmt::Display for Msgs { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { writeln!(formatter, "\n{}", Table::render(&self.0)) } } // FIXME: fix tests // #[cfg(test)] // mod tests { // use crate::{ // ctx::Ctx, // domain::{account::entity::Account, config::entity::Config}, // msg::{body::Body, headers::Headers, model::Msg}, // }; // #[test] // fn test_new() { // let ctx = Ctx { // account: Account::new_with_signature(None, "test@mail.com", None), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // ..Ctx::default() // }; // let msg = Msg::new(&ctx); // let expected_headers = Headers { // from: vec![String::from("Config Name ")], // ..Headers::default() // }; // assert_eq!( // msg.headers, expected_headers, // "{:#?}, {:#?}", // msg.headers, expected_headers // ); // assert!(msg.get_raw_as_string().unwrap().is_empty()); // } // #[test] // fn test_new_with_account_name() { // let ctx = Ctx { // account: Account::new_with_signature(Some("Account Name"), "test@mail.com", None), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let msg = Msg::new(&ctx); // let expected_headers = Headers { // from: vec![String::from("Account Name ")], // ..Headers::default() // }; // assert_eq!( // msg.headers, expected_headers, // "{:#?}, {:#?}", // msg.headers, expected_headers // ); // assert!(msg.get_raw_as_string().unwrap().is_empty()); // } // #[test] // fn test_new_with_headers() { // let ctx = Ctx { // account: Account::new(Some("Account Name"), "test@mail.com"), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let msg_with_custom_from = Msg::new_with_headers( // &ctx, // Headers { // from: vec![String::from("Account Name ")], // ..Headers::default() // }, // ); // let expected_with_custom_from = Msg { // headers: Headers { // // the Msg::new_with_headers function should use the from // // address in the headers struct, not the from address of the // // account // from: vec![String::from("Account Name ")], // ..Headers::default() // }, // // The signature should be added automatically // body: Body::new_with_text("\n"), // ..Msg::default() // }; // assert_eq!( // msg_with_custom_from, expected_with_custom_from, // "Left: {:#?}, Right: {:#?}", // msg_with_custom_from, expected_with_custom_from // ); // } // #[test] // fn test_new_with_headers_and_signature() { // let ctx = Ctx { // account: Account::new_with_signature( // Some("Account Name"), // "test@mail.com", // Some("Signature"), // ), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let msg_with_custom_signature = Msg::new_with_headers(&ctx, Headers::default()); // let expected_with_custom_signature = Msg { // headers: Headers { // from: vec![String::from("Account Name ")], // signature: Some(String::from("\n-- \nSignature")), // ..Headers::default() // }, // body: Body::new_with_text("\n\n-- \nSignature"), // ..Msg::default() // }; // assert_eq!( // msg_with_custom_signature, // expected_with_custom_signature, // "Left: {:?}, Right: {:?}", // dbg!(&msg_with_custom_signature), // dbg!(&expected_with_custom_signature) // ); // } // #[test] // fn test_change_to_reply() { // // in this test, we are gonna reproduce the same situation as shown // // here: https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2 // // == Preparations == // // -- rfc test -- // // accounts for the rfc test // let config = Config { // name: String::from("Config Name"), // ..Config::default() // }; // let john_doe = Ctx { // account: Account::new(Some("John Doe"), "jdoe@machine.example"), // config: config.clone(), // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let mary_smith = Ctx { // account: Account::new(Some("Mary Smith"), "mary@example.net"), // config: config.clone(), // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let msg_rfc_test = Msg { // headers: Headers { // from: vec!["John Doe ".to_string()], // to: vec!["Mary Smith ".to_string()], // subject: Some("Saying Hello".to_string()), // message_id: Some("<1234@local.machine.example>".to_string()), // ..Headers::default() // }, // body: Body::new_with_text(concat![ // "This is a message just to say hello.\n", // "So, \"Hello\".", // ]), // ..Msg::default() // }; // // -- for general tests -- // let ctx = Ctx { // account: Account::new(Some("Name"), "some@address.asdf"), // config, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // // -- for reply_all -- // // a custom test to look what happens, if we want to reply to all addresses. // // Take a look into the doc of the "change_to_reply" what should happen, if we // // set "reply_all" to "true". // let mut msg_reply_all = Msg { // headers: Headers { // from: vec!["Boss ".to_string()], // to: vec![ // "msg@1.asdf".to_string(), // "msg@2.asdf".to_string(), // "Name ".to_string(), // ], // cc: Some(vec![ // "test@testing".to_string(), // "test2@testing".to_string(), // ]), // message_id: Some("RandomID123".to_string()), // reply_to: Some(vec!["Reply@Mail.rofl".to_string()]), // subject: Some("Have you heard of himalaya?".to_string()), // ..Headers::default() // }, // body: Body::new_with_text(concat!["A body test\n", "\n", "Sincerely",]), // ..Msg::default() // }; // // == Expected output(s) == // // -- rfc test -- // // the first step // let expected_rfc1 = Msg { // headers: Headers { // from: vec!["Mary Smith ".to_string()], // to: vec!["John Doe ".to_string()], // reply_to: Some(vec![ // "\"Mary Smith: Personal Account\" ".to_string(), // ]), // subject: Some("Re: Saying Hello".to_string()), // message_id: Some("<3456@example.net>".to_string()), // in_reply_to: Some("<1234@local.machine.example>".to_string()), // ..Headers::default() // }, // body: Body::new_with_text(concat![ // "> This is a message just to say hello.\n", // "> So, \"Hello\".", // ]), // ..Msg::default() // }; // // then the response the the first respone above // let expected_rfc2 = Msg { // headers: Headers { // to: vec!["\"Mary Smith: Personal Account\" ".to_string()], // from: vec!["John Doe ".to_string()], // subject: Some("Re: Saying Hello".to_string()), // message_id: Some("".to_string()), // in_reply_to: Some("<3456@example.net>".to_string()), // ..Headers::default() // }, // body: Body::new_with_text(concat![ // ">> This is a message just to say hello.\n", // ">> So, \"Hello\".", // ]), // ..Msg::default() // }; // // -- reply all -- // let expected_reply_all = Msg { // headers: Headers { // from: vec!["Name ".to_string()], // to: vec![ // "msg@1.asdf".to_string(), // "msg@2.asdf".to_string(), // "Reply@Mail.rofl".to_string(), // ], // cc: Some(vec![ // "test@testing".to_string(), // "test2@testing".to_string(), // ]), // in_reply_to: Some("RandomID123".to_string()), // subject: Some("Re: Have you heard of himalaya?".to_string()), // ..Headers::default() // }, // body: Body::new_with_text(concat!["> A body test\n", "> \n", "> Sincerely"]), // ..Msg::default() // }; // // == Testing == // // -- rfc test -- // // represents the message for the first reply // let mut rfc_reply_1 = msg_rfc_test.clone(); // rfc_reply_1.change_to_reply(&mary_smith, false).unwrap(); // // the user would enter this normally // rfc_reply_1.headers = Headers { // message_id: Some("<3456@example.net>".to_string()), // reply_to: Some(vec![ // "\"Mary Smith: Personal Account\" ".to_string(), // ]), // ..rfc_reply_1.headers.clone() // }; // // represents the message for the reply to the reply // let mut rfc_reply_2 = rfc_reply_1.clone(); // rfc_reply_2.change_to_reply(&john_doe, false).unwrap(); // rfc_reply_2.headers = Headers { // message_id: Some("".to_string()), // ..rfc_reply_2.headers.clone() // }; // assert_eq!( // rfc_reply_1, // expected_rfc1, // "Left: {:?}, Right: {:?}", // dbg!(&rfc_reply_1), // dbg!(&expected_rfc1) // ); // assert_eq!( // rfc_reply_2, // expected_rfc2, // "Left: {:?}, Right: {:?}", // dbg!(&rfc_reply_2), // dbg!(&expected_rfc2) // ); // // -- custom tests -— // msg_reply_all.change_to_reply(&ctx, true).unwrap(); // assert_eq!( // msg_reply_all, // expected_reply_all, // "Left: {:?}, Right: {:?}", // dbg!(&msg_reply_all), // dbg!(&expected_reply_all) // ); // } // #[test] // fn test_change_to_forwarding() { // // == Preparations == // let ctx = Ctx { // account: Account::new_with_signature(Some("Name"), "some@address.asdf", Some("lol")), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let mut msg = Msg::new_with_headers( // &ctx, // Headers { // from: vec![String::from("ThirdPerson ")], // subject: Some(String::from("Test subject")), // ..Headers::default() // }, // ); // msg.body = Body::new_with_text(concat!["The body text, nice!\n", "Himalaya is nice!",]); // // == Expected Results == // let expected_msg = Msg { // headers: Headers { // from: vec![String::from("ThirdPerson ")], // sender: Some(String::from("Name ")), // signature: Some(String::from("\n-- \nlol")), // subject: Some(String::from("Fwd: Test subject")), // ..Headers::default() // }, // body: Body::new_with_text(concat![ // "\n", // "---------- Forwarded Message ----------\n", // "The body text, nice!\n", // "Himalaya is nice!\n", // "\n-- \nlol" // ]), // ..Msg::default() // }; // // == Tests == // msg.change_to_forwarding(&ctx); // assert_eq!( // msg, // expected_msg, // "Left: {:?}, Right: {:?}", // dbg!(&msg), // dbg!(&expected_msg) // ); // } // #[test] // fn test_edit_body() { // // == Preparations == // let ctx = Ctx { // account: Account::new_with_signature(Some("Name"), "some@address.asdf", None), // ..Ctx::default() // }; // let mut msg = Msg::new_with_headers( // &ctx, // Headers { // bcc: Some(vec![String::from("bcc ")]), // cc: Some(vec![String::from("cc ")]), // subject: Some(String::from("Subject")), // ..Headers::default() // }, // ); // // == Expected Results == // let expected_msg = Msg { // headers: Headers { // from: vec![String::from("Name ")], // to: vec![String::new()], // // these fields should exist now // subject: Some(String::from("Subject")), // bcc: Some(vec![String::from("bcc ")]), // cc: Some(vec![String::from("cc ")]), // ..Headers::default() // }, // body: Body::new_with_text("\n"), // ..Msg::default() // }; // // == Tests == // msg.edit_body().unwrap(); // assert_eq!( // msg, expected_msg, // "Left: {:#?}, Right: {:#?}", // msg, expected_msg // ); // } // #[test] // fn test_parse_from_str() { // use std::collections::HashMap; // // == Preparations == // let ctx = Ctx { // account: Account::new_with_signature(Some("Name"), "some@address.asdf", None), // config: Config { // name: String::from("Config Name"), // ..Config::default() // }, // mbox: String::from("INBOX"), // ..Ctx::default() // }; // let msg_template = Msg::new(&ctx); // let normal_content = concat![ // "From: Some \n", // "Subject: Awesome Subject\n", // "Bcc: mail1@rofl.lol,name \n", // "To: To \n", // "\n", // "Account Signature\n", // ]; // let content_with_custom_headers = concat![ // "From: Some \n", // "Subject: Awesome Subject\n", // "Bcc: mail1@rofl.lol,name \n", // "To: To \n", // "CustomHeader1: Value1\n", // "CustomHeader2: Value2\n", // "\n", // "Account Signature\n", // ]; // // == Expected outputs == // let expect = Msg { // headers: Headers { // from: vec![String::from("Some ")], // subject: Some(String::from("Awesome Subject")), // bcc: Some(vec![ // String::from("name "), // String::from("mail1@rofl.lol"), // ]), // to: vec![String::from("To ")], // ..Headers::default() // }, // body: Body::new_with_text("Account Signature\n"), // ..Msg::default() // }; // // -- with custom headers -- // let mut custom_headers: HashMap> = HashMap::new(); // custom_headers.insert("CustomHeader1".to_string(), vec!["Value1".to_string()]); // custom_headers.insert("CustomHeader2".to_string(), vec!["Value2".to_string()]); // let expect_custom_header = Msg { // headers: Headers { // from: vec![String::from("Some ")], // subject: Some(String::from("Awesome Subject")), // bcc: Some(vec![ // String::from("name "), // String::from("mail1@rofl.lol"), // ]), // to: vec![String::from("To ")], // custom_headers: Some(custom_headers), // ..Headers::default() // }, // body: Body::new_with_text("Account Signature\n"), // ..Msg::default() // }; // // == Testing == // let mut msg1 = msg_template.clone(); // let mut msg2 = msg_template.clone(); // msg1.parse_from_str(normal_content).unwrap(); // msg2.parse_from_str(content_with_custom_headers).unwrap(); // assert_eq!( // msg1, // expect, // "Left: {:?}, Right: {:?}", // dbg!(&msg1), // dbg!(&expect) // ); // assert_eq!( // msg2, // expect_custom_header, // "Left: {:?}, Right: {:?}", // dbg!(&msg2), // dbg!(&expect_custom_header) // ); // } // }