mirror of
https://github.com/pimalaya/himalaya.git
synced 2026-06-17 05:07:55 +08:00
1501 lines
50 KiB
Rust
1501 lines
50 KiB
Rust
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<Self> {
|
|
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<Attachment>,
|
|
|
|
/// The flags of this msg.
|
|
pub flags: Flags,
|
|
|
|
/// All information of the headers (sender, from, to and so on)
|
|
// headers: HashMap<HeaderName, Vec<String>>,
|
|
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<u32>,
|
|
|
|
/// 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<String>,
|
|
|
|
/// The msg but in raw.
|
|
#[serde(skip_serializing)]
|
|
raw: Vec<u8>,
|
|
}
|
|
|
|
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
|
|
///
|
|
/// <details>
|
|
///
|
|
/// ```
|
|
/// # 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 <clement.douin@posteo.net>")],
|
|
/// // 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);
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// </details>
|
|
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<String> = 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::<Vec<String>>()
|
|
.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: <Your@address>
|
|
/// ...
|
|
///
|
|
/// > 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<Vec<u8>> {
|
|
// 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 <clement.douin@posteo.net>\n",
|
|
/// "From: TornaxO7 <tornax07@gmail.com>\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 <msg@address.net>".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<Message> {
|
|
// == 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<u32> {
|
|
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 <UID>`.
|
|
pub fn get_raw(&self) -> Vec<u8> {
|
|
self.raw.clone()
|
|
}
|
|
|
|
/// Returns the raw mail as a string instead of a Vector of bytes.
|
|
pub fn get_raw_as_string(&self) -> Result<String> {
|
|
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<Msg> {
|
|
// -- 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<Self> {
|
|
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<Msg>);
|
|
|
|
impl Msgs {
|
|
pub fn new() -> Self {
|
|
Self(Vec::new())
|
|
}
|
|
}
|
|
|
|
// -- From's --
|
|
impl<'mails> TryFrom<&'mails ZeroCopy<Vec<Fetch>>> for Msgs {
|
|
type Error = Error;
|
|
|
|
fn try_from(fetches: &'mails ZeroCopy<Vec<Fetch>>) -> Result<Self> {
|
|
// 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 <test@mail.com>")],
|
|
// ..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 <test@mail.com>")],
|
|
// ..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 <test@mail.com>")],
|
|
// ..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 <test@mail.com>")],
|
|
// ..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 <test@mail.com>")],
|
|
// 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 <jdoe@machine.example>".to_string()],
|
|
// to: vec!["Mary Smith <mary@example.net>".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 <someone@boss.asdf>".to_string()],
|
|
// to: vec![
|
|
// "msg@1.asdf".to_string(),
|
|
// "msg@2.asdf".to_string(),
|
|
// "Name <some@address.asdf>".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 <mary@example.net>".to_string()],
|
|
// to: vec!["John Doe <jdoe@machine.example>".to_string()],
|
|
// reply_to: Some(vec![
|
|
// "\"Mary Smith: Personal Account\" <smith@home.example>".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\" <smith@home.example>".to_string()],
|
|
// from: vec!["John Doe <jdoe@machine.example>".to_string()],
|
|
// subject: Some("Re: Saying Hello".to_string()),
|
|
// message_id: Some("<abcd.1234@local.machine.test>".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 <some@address.asdf>".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\" <smith@home.example>".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("<abcd.1234@local.machine.test>".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 <some@msg.asdf>")],
|
|
// 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 <some@msg.asdf>")],
|
|
// sender: Some(String::from("Name <some@address.asdf>")),
|
|
// 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 <some@mail.com>")]),
|
|
// cc: Some(vec![String::from("cc <some@mail.com>")]),
|
|
// subject: Some(String::from("Subject")),
|
|
// ..Headers::default()
|
|
// },
|
|
// );
|
|
|
|
// // == Expected Results ==
|
|
// let expected_msg = Msg {
|
|
// headers: Headers {
|
|
// from: vec![String::from("Name <some@address.asdf>")],
|
|
// to: vec![String::new()],
|
|
// // these fields should exist now
|
|
// subject: Some(String::from("Subject")),
|
|
// bcc: Some(vec![String::from("bcc <some@mail.com>")]),
|
|
// cc: Some(vec![String::from("cc <some@mail.com>")]),
|
|
// ..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 <user@msg.sf>\n",
|
|
// "Subject: Awesome Subject\n",
|
|
// "Bcc: mail1@rofl.lol,name <rofl@lol.asdf>\n",
|
|
// "To: To <name@msg.rofl>\n",
|
|
// "\n",
|
|
// "Account Signature\n",
|
|
// ];
|
|
|
|
// let content_with_custom_headers = concat![
|
|
// "From: Some <user@msg.sf>\n",
|
|
// "Subject: Awesome Subject\n",
|
|
// "Bcc: mail1@rofl.lol,name <rofl@lol.asdf>\n",
|
|
// "To: To <name@msg.rofl>\n",
|
|
// "CustomHeader1: Value1\n",
|
|
// "CustomHeader2: Value2\n",
|
|
// "\n",
|
|
// "Account Signature\n",
|
|
// ];
|
|
|
|
// // == Expected outputs ==
|
|
// let expect = Msg {
|
|
// headers: Headers {
|
|
// from: vec![String::from("Some <user@msg.sf>")],
|
|
// subject: Some(String::from("Awesome Subject")),
|
|
// bcc: Some(vec![
|
|
// String::from("name <rofl@lol.asdf>"),
|
|
// String::from("mail1@rofl.lol"),
|
|
// ]),
|
|
// to: vec![String::from("To <name@msg.rofl>")],
|
|
// ..Headers::default()
|
|
// },
|
|
// body: Body::new_with_text("Account Signature\n"),
|
|
// ..Msg::default()
|
|
// };
|
|
|
|
// // -- with custom headers --
|
|
// let mut custom_headers: HashMap<String, Vec<String>> = 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 <user@msg.sf>")],
|
|
// subject: Some(String::from("Awesome Subject")),
|
|
// bcc: Some(vec![
|
|
// String::from("name <rofl@lol.asdf>"),
|
|
// String::from("mail1@rofl.lol"),
|
|
// ]),
|
|
// to: vec![String::from("To <name@msg.rofl>")],
|
|
// 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)
|
|
// );
|
|
// }
|
|
// }
|