mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-23 03:20:20 +00:00
refactor msg model (#173)
* Adding Mail structure Adding a main structure which can be used for *everything* which has to do with a mail: - Writing a new mail - Fetching the information of a mail * Write mails User can write mails now * Writing mail When mail is converted to a sendable message, it'll print out a nice little error message what to do and which field is missing a value. * Mail List subcommand works with new struct now. * Forwarding Started implementation for forwarding message * Breaking Commit This is just a "backup" commit * First finished Himalaya can compile successfully now. * Removed uneccessary files - Moved everything from msg/mail to msg/model - Removed uneccessary files * Renaming Renamed all "Mail" and "Mails" struct to "Msg" and "Msgs". * Cleaning Removed an CLI-Subcommand which can't be used anymore * Flags Fixed flags to vector and added the template subcommand back * Changes to Flags Changed the datatype from Vec<Flag<'static>> to HashSet<Flag<'static>>, because each Message/Mail can include only one flag-type, so why not a HashSet for this job? * Cargo.toml changes Fixed the lettre-dependencie which points to the pull request with the given serde implementation for ContentType (needed for Attachments). * Fix Template bug and removed unnecessary files. - Removed the msg/flag/flag.rs file since we can use the imap::types::Flag implementation now - `himalaya template new` printed the template two times. This should be fixed now * Template command Fixed formatting when printing out template * Sending Mail Fixed bug that user can't send a mail * Msg Moved the body from the attachment-vector out to an external attribute of the struct. * Msg listing and changed Msg::from to Msg::try_from - Fixed bug that listing didn't showed up addresses in the `From:` field for example - Made each `from` trait function to `try_from` for better error-handling * Tests - Fixed tests in `tests/imap_conn.rs` * Cargo.toml changes, Bug fixes, Documentation - Updated mailparse to 0.13.4 - Added new "new" function to Account - Cleaned up some functions (removed some) - Added Eq and PartialEq derives for msg - Bugfix: It couldn't get the body of some mails, because they were inside a multipart/alternative part. Now the mail is iterating through all subparts and picks up the firs text/plain "attachment" and uses it as the body. * Changed Msg attributes viewability - Made the "main attributes" of the Msg struct public - Removed to getter functions * Big envelope changes - Added documentation - Removed the getter functions, beacuse the attributes are public * Documentation and Cleanup - Removed the `new` constructor of the envelope, since it's actually the same as Envelope::default() - Addded tests and Documentation to Attachments.rs * Documentation and Tests - Added docuemntation for msg/body.rs - Fixed some syntax errors in the doc strings * General msg - Added `get_raw` function and `raw` field for the `Msg` struct. - Fixed raw output of msg - Started documentation + tests for the Msg struct * Changes to Msg - Added Clone derive - Added documentation for change_to_reply method - Added tests to change_to_reply method * Msg tests and Account changes - Changed `Account::new()` function - Added more documentation to Msg struct - Added more tests to Msg struct * Removed an unknown file Removed src/.rust_info.json (don't know where it came) * Msgs finished(?) Added final documentation to the Msg struct. * ImapConnector Fix Fixed the bug, when trying to move a msg, the envelope wasn't applied to the fetch. Fixed that in the `get_msg` method. * Msg - Bug fixes: - Adding Message ID and Subject in the to_sendable_msg function - Removed an println statement for debugging - Added more error messages * Cargo.toml Changed order and added some comments to the dependencies. * Msg Removed an unnecessary documentation part. * Fixed documentation * Removing non-debugflags for dev profile Removing debug=false for the dev profile since it was just for me. * Cleanup Removed the comment blocks and reduced some comments * Cleanup Reformatted some stuff * Cleanup Replaced the word "mail" with "msg". * Formatting Fixed formatting in src/flag/model.rs file * Little fix * Changes and tests - New "feature": If you reply to a reply, the subject won't look like this for example: Re: Re: Re: Re: Re: Re: The subject - Fixed tests. All tests pass now (run `cargo test`) * Idea(?) Renamed all <module>_matches/_subcmds to general "matches" and "subcmds()". All modules have the same: "matches()" and "subcmds()" * Little fix Changed the name from "imap_conn" to "conn" by mistake. Fixed that * Bug fix When sending a message, himalaya will generate a UUID on its own if there's no message-id for the message yet. * Bug fix Removed angle brackets, since they are added through the lettre library. * Bugfix Removed an unnecessary (old) line. * Cleanup Removed the last comment blocks. * Fixed lettre dependencie * Bugfixes and Error handling - When calling the msg_interaction function, the user can edit the msg first, before the prompt comes up - Also added a error output, if the msg couldn't be converted into a sendable message. * Error handling Improved output of error * Bug fixes, Error Handling - Improved error handling for the string parsing - Added attempt to fix the bug that a whitespace is added in the end of an address * Trimming Added trims to avoid invalid white spaces in the addresses. * Fixing whitespace bug All addresses are gonna trimmed before adding to a header now * Adding encoding, Changed dependencie - Added encoding for the body part - Changed the lettre dependencie of lettre to TornaxO7's fork of lettre, because the "ContentTypeEncoding" struct needs the "Eq", "Serialize" and "Deserialize" derives. * Improved Error handling Added a warning, if a message included an unknown attachment. * Fixed tests Fixed the documentation for passing the tests. * Doc change * Bugfis: When replying, signature is added now as well * Bugfix: Forwarding Message When forwarding a message, himalaya, put the signature in the end of the mail/msg. Now it's added above the '-------Forwarded Message---------' line. * Readjusted tests and new method - Changed the way to create a new account: - Account::new => Sets signautre to "None" - Account::new_with_signature => Sets signature to the given argument This makes it more flexible to create specifique accounts for tests for example. - Fixed the tests so all are passing now * improve sig and sig delim concat process * add signature delim struct comment * fix signatures + tests * fix body and signature new lines * Adding [serde(rename_all = "camelCase")] to structs * fix reply indentation and signature new lines * add default rustfmt.toml * apply fmt on all the project * fix msg tests * Makeing Ctx struct independent - The Ctx struct doesn't include references anymore. This makes it easier to create new Ctx instances by doing the following: Ctx { <attribute>: <value>, .. Ctx::default() } This helps especially for writing tests. Also the attributes of the Ctx struct in the main-entry function aren't used anymore after creating the Ctx struct. So there's no need to have only references in the Ctx struct. * Fixing JSON output - JSON of message includes `hasAttachment` key now - JSON output shows both body types: Text and Html - Changed `Body` struct so it can store html and text now. * Tests Updated tests with the latest Body implementation * Fixes - Removed suspicious println macro in serializer of msg... *cough cough* - Fixed output in the "read" command - othe small fixes * Formatting Formatted all files * Msg - Adding 'get_full_message' method which prints out all information of the message in a string * New Msg-Struct Adding MsgSerialized, a struct, which represents the "correct" serialized version of a message because it includes another attribute: `has_attachment`. * Cleanup Removed the manual serialize implementation of `Msg` and added a little more info about the MsgSerialized. * Test fixes Adjusted all tests so all are passing now. * Little changes - Used a better condition for checking if the message includes attachments or not - format fixes * Fixing tests and Docs - Provided more docs - Refactored tests and added more tests * Expanding specials Added more "special characters" which will add some quotes around the name if it includes at least one of them. * Fixing test Improved the detection if the mail-name includes a special character or not. * Variable renaming Renamed a variable for better readability. * Envelope renaming * Small change Renamed the variable of the `TryFrom` implementation for the imap_proto::Envelope. * Last stuff - Making the attributes of mboxes independent. We can store them now as well! - Added more docs - Added type-safety for flags - Expanded flags a bit - Added more tests - Added a short summary of the file-structure in the beginning of the doc. * Help command fix Fixing help command description. * Small doc change * Doc fix Fixing the link to the mbox delimiter. * Fixing typo * Doc fix * Added docs for Output struct * Fixing tests Fixing a little test issue * Formatting changes + doc change - Removed bold + capital words for logout-doc - Run format on each *.rs file * Fixing tests - Testing the return value of the flags struct as a string doesn't really work since it's a HashSet => Converted it into a Vec (in the test) to set the order as well. - Fixed imap test by reverting the changes in the test. * Error handling Changed error output when creating an Imap-Connection. Should help debugging :) * Formatting fixes and refactoring - Using `trim_end_matches` instead of "pop"s now. - Executed `cargo fmt` * Trying to fix test workflow * Fixes Updated dependencies with `cargo update` and let cargo point to master branch of TornaxO7's lettre-fork because this should probably fix the issue with the nix-build. * Test fix Fixing the workflow. * Workflow fix Removing semicolon * Starting workflow Added a new line to be able to push. * Workflow Reverting the workflow command. * Workflows Reverting workflow to master workflow. * let actions/checkout@v2 run first * Forwarded message's signature misplaced Changes the order of the signature for forwarded messages. * Output change Changed the output if an error occurs. * Fixing output for template-building * Template shows raw data with JSON format #23 When printing the message in json, the raw message is printed out as a string now. * the_sender_is_not_displayed_properly_in_table_and_json #21 - When displaying the table, we'll look first, if a name exists, if yes => use it otherwise use the email address. - Added the rfc2047_decoder for parsing addresses * Formatting Run 'cargo fmt' Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com> Co-authored-by: Erik <erik1000@protonmail.com>
This commit is contained in:
parent
2ac2f53f31
commit
0e68801a35
30 changed files with 4381 additions and 1967 deletions
5
.github/workflows/tests.yaml
vendored
5
.github/workflows/tests.yaml
vendored
|
@ -10,11 +10,12 @@ jobs:
|
|||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Start GreenMail testing server
|
||||
run: |
|
||||
docker run --rm -d -e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' -p 3025:3025 -p 3110:3110 -p 3143:3143 -p 3465:3465 -p 3993:3993 -p 3995:3995 greenmail/standalone:1.6.2
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
|
|
658
Cargo.lock
generated
658
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
17
Cargo.toml
17
Cargo.toml
|
@ -8,16 +8,21 @@ edition = "2018"
|
|||
[dependencies]
|
||||
atty = "0.2.14"
|
||||
chrono = "0.4.19"
|
||||
clap = {version = "2.33.3", default-features = false, features = ["suggestions", "color"]}
|
||||
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
|
||||
colorful = "0.2.1"
|
||||
env_logger = "0.8.3"
|
||||
error-chain = "0.12.4"
|
||||
imap = "3.0.0-alpha.3"
|
||||
lettre = "0.10.0-rc.1"
|
||||
imap = "3.0.0-alpha.4"
|
||||
imap-proto = "0.14.3"
|
||||
# This commit includes the de/serialization of the ContentType
|
||||
# lettre = { version = "0.10.0-rc.1", features = ["serde"] }
|
||||
lettre = {git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] }
|
||||
lettre_email = "0.9.4"
|
||||
log = "0.4.14"
|
||||
mailparse = "0.13.1"
|
||||
mailparse = "0.13.4"
|
||||
native-tls = "0.2"
|
||||
rfc2047-decoder = "0.1.2"
|
||||
serde = {version = "1.0.118", features = ["derive"]}
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0.61"
|
||||
shellexpand = "2.1.0"
|
||||
terminal_size = "0.1.15"
|
||||
|
@ -25,4 +30,4 @@ toml = "0.5.8"
|
|||
tree_magic = "0.2.3"
|
||||
unicode-width = "0.1.7"
|
||||
url = "2.2.2"
|
||||
uuid = {version = "0.8", features = ["v4"]}
|
||||
uuid = { version = "0.8", features = ["v4"] }
|
||||
|
|
|
@ -9,4 +9,4 @@
|
|||
sha256 = lock.nodes.flake-compat.locked.narHash; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).defaultNix
|
||||
}).defaultNix
|
||||
|
|
75
rustfmt.toml
Normal file
75
rustfmt.toml
Normal file
|
@ -0,0 +1,75 @@
|
|||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
indent_style = "Block"
|
||||
use_small_heuristics = "Default"
|
||||
fn_call_width = 60
|
||||
attr_fn_like_width = 70
|
||||
struct_lit_width = 18
|
||||
struct_variant_width = 35
|
||||
array_width = 60
|
||||
chain_width = 60
|
||||
single_line_if_else_max_width = 50
|
||||
wrap_comments = false
|
||||
format_code_in_doc_comments = false
|
||||
comment_width = 80
|
||||
normalize_comments = false
|
||||
normalize_doc_attributes = false
|
||||
license_template_path = ""
|
||||
format_strings = false
|
||||
format_macro_matchers = false
|
||||
format_macro_bodies = true
|
||||
empty_item_single_line = true
|
||||
struct_lit_single_line = true
|
||||
fn_single_line = false
|
||||
where_single_line = false
|
||||
imports_indent = "Block"
|
||||
imports_layout = "Mixed"
|
||||
imports_granularity = "Preserve"
|
||||
group_imports = "Preserve"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
reorder_impl_items = false
|
||||
type_punctuation_density = "Wide"
|
||||
space_before_colon = false
|
||||
space_after_colon = true
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Front"
|
||||
remove_nested_parens = true
|
||||
combine_control_expr = true
|
||||
overflow_delimited_expr = false
|
||||
struct_field_align_threshold = 0
|
||||
enum_discrim_align_threshold = 0
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
force_multiline_blocks = false
|
||||
fn_args_layout = "Tall"
|
||||
brace_style = "SameLineWhere"
|
||||
control_brace_style = "AlwaysSameLine"
|
||||
trailing_semicolon = true
|
||||
trailing_comma = "Vertical"
|
||||
match_block_trailing_comma = false
|
||||
blank_lines_upper_bound = 1
|
||||
blank_lines_lower_bound = 0
|
||||
edition = "2015"
|
||||
version = "One"
|
||||
inline_attribute_width = 0
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Auto"
|
||||
required_version = "1.4.37"
|
||||
unstable_features = false
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
hide_parse_errors = false
|
||||
error_on_line_overflow = false
|
||||
error_on_unformatted = false
|
||||
report_todo = "Never"
|
||||
report_fixme = "Never"
|
||||
ignore = []
|
||||
emit_mode = "Files"
|
||||
make_backup = false
|
|
@ -9,4 +9,4 @@
|
|||
sha256 = lock.nodes.flake-compat.locked.narHash; }
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
}).shellNix
|
||||
|
|
|
@ -5,7 +5,8 @@ use std::io;
|
|||
|
||||
error_chain! {}
|
||||
|
||||
pub fn comp_subcmds<'s>() -> Vec<App<'s, 's>> {
|
||||
// == Main functions ==
|
||||
pub fn subcmds<'s>() -> Vec<App<'s, 's>> {
|
||||
vec![SubCommand::with_name("completion")
|
||||
.about("Generates the completion script for the given shell")
|
||||
.args(&[Arg::with_name("shell")
|
||||
|
@ -13,7 +14,7 @@ pub fn comp_subcmds<'s>() -> Vec<App<'s, 's>> {
|
|||
.required(true)])]
|
||||
}
|
||||
|
||||
pub fn comp_matches<'a>(app: fn() -> App<'a, 'a>, matches: &ArgMatches) -> Result<bool> {
|
||||
pub fn matches<'a>(app: fn() -> App<'a, 'a>, matches: &ArgMatches) -> Result<bool> {
|
||||
if let Some(matches) = matches.subcommand_matches("completion") {
|
||||
debug!("completion command matched");
|
||||
let shell = match matches.value_of("shell").unwrap() {
|
||||
|
|
|
@ -19,9 +19,11 @@ error_chain! {}
|
|||
|
||||
const DEFAULT_PAGE_SIZE: usize = 10;
|
||||
|
||||
// Account
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
// --- Account ---
|
||||
/// Represents an account section in your config file.
|
||||
///
|
||||
/// [account section]: https://github.com/soywod/himalaya/wiki/Configuration:config-file#account-specific-settings
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Account {
|
||||
// Override
|
||||
|
@ -52,12 +54,30 @@ pub struct Account {
|
|||
}
|
||||
|
||||
impl Account {
|
||||
/// Returns the imap-host address + the port usage of the account
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use himalaya::config::model::Account;
|
||||
/// fn main () {
|
||||
/// let account = Account {
|
||||
/// imap_host: String::from("hostExample"),
|
||||
/// imap_port: 42,
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// let expected_output = ("hostExample", 42);
|
||||
///
|
||||
/// assert_eq!(account.imap_addr(), expected_output);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn imap_addr(&self) -> (&str, u16) {
|
||||
debug!("host: {}", self.imap_host);
|
||||
debug!("port: {}", self.imap_port);
|
||||
(&self.imap_host, self.imap_port)
|
||||
}
|
||||
|
||||
/// Runs the given command in your password string and returns it.
|
||||
pub fn imap_passwd(&self) -> Result<String> {
|
||||
let passwd = run_cmd(&self.imap_passwd_cmd).chain_err(|| "Cannot run IMAP passwd cmd")?;
|
||||
let passwd = passwd
|
||||
|
@ -109,6 +129,83 @@ impl Account {
|
|||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new account with the given values and returns it. All other attributes of the
|
||||
/// account are gonna be empty/None.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use himalaya::config::model::Account;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let account1 = Account::new(Some("Name1"), "email@address.com");
|
||||
/// let account2 = Account::new(None, "email@address.com");
|
||||
///
|
||||
/// let expected1 = Account {
|
||||
/// name: Some("Name1".to_string()),
|
||||
/// email: "email@address.com".to_string(),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// let expected2 = Account {
|
||||
/// email: "email@address.com".to_string(),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(account1, expected1);
|
||||
/// assert_eq!(account2, expected2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new<S: ToString>(name: Option<S>, email_addr: S) -> Self {
|
||||
Self {
|
||||
name: name.and_then(|name| Some(name.to_string())),
|
||||
email: email_addr.to_string(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new account with a custom signature. Passing `None` to `signature` sets the
|
||||
/// signature to `Account Signature`.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use himalaya::config::model::Account;
|
||||
///
|
||||
/// fn main() {
|
||||
///
|
||||
/// // the testing accounts
|
||||
/// let account_with_custom_signature = Account::new_with_signature(
|
||||
/// Some("Email name"), "some@mail.com", Some("Custom signature! :)"));
|
||||
/// let account_with_default_signature = Account::new_with_signature(
|
||||
/// Some("Email name"), "some@mail.com", None);
|
||||
///
|
||||
/// // How they should look like
|
||||
/// let account_cmp1 = Account {
|
||||
/// name: Some("Email name".to_string()),
|
||||
/// email: "some@mail.com".to_string(),
|
||||
/// signature: Some("Custom signature! :)".to_string()),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// let account_cmp2 = Account {
|
||||
/// name: Some("Email name".to_string()),
|
||||
/// email: "some@mail.com".to_string(),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(account_with_custom_signature, account_cmp1);
|
||||
/// assert_eq!(account_with_default_signature, account_cmp2);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new_with_signature<S: AsRef<str> + ToString>(
|
||||
name: Option<S>,
|
||||
email_addr: S,
|
||||
signature: Option<S>,
|
||||
) -> Self {
|
||||
let mut account = Account::new(name, email_addr);
|
||||
account.signature = signature.and_then(|signature| Some(signature.to_string()));
|
||||
account
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Account {
|
||||
|
@ -138,14 +235,15 @@ impl Default for Account {
|
|||
}
|
||||
}
|
||||
|
||||
// Config
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
// --- Config ---
|
||||
/// Represents the whole config file.
|
||||
#[derive(Debug, Default, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Config {
|
||||
pub name: String,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Option to override the default signature delimiter "`--\n `".
|
||||
pub signature_delimiter: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub default_page_size: Option<usize>,
|
||||
|
@ -195,6 +293,7 @@ impl Config {
|
|||
Ok(path)
|
||||
}
|
||||
|
||||
/// Parses the config file by the given path and stores the values into the struct.
|
||||
pub fn new(path: Option<PathBuf>) -> Result<Self> {
|
||||
let path = match path {
|
||||
Some(path) => path,
|
||||
|
@ -212,6 +311,8 @@ impl Config {
|
|||
Ok(toml::from_slice(&content).chain_err(|| "Cannot parse config file")?)
|
||||
}
|
||||
|
||||
/// Returns the account by the given name.
|
||||
/// If `name` is `None`, then the default account is returned.
|
||||
pub fn find_account_by_name(&self, name: Option<&str>) -> Result<&Account> {
|
||||
match name {
|
||||
Some("") | None => self
|
||||
|
@ -227,6 +328,11 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the path to the given filename in the download directory.
|
||||
/// You can imagine this as:
|
||||
/// ```skip
|
||||
/// Account-specifique-download-dir-path + Attachment-Filename
|
||||
/// ```
|
||||
pub fn downloads_filepath(&self, account: &Account, filename: &str) -> PathBuf {
|
||||
account
|
||||
.downloads_dir
|
||||
|
@ -245,12 +351,62 @@ impl Config {
|
|||
.join(filename)
|
||||
}
|
||||
|
||||
/// This is a little helper-function like which uses the the name and email
|
||||
/// of the account to create a valid address for the header of the headers
|
||||
/// of a msg.
|
||||
///
|
||||
/// # Hint
|
||||
/// If the name includes some special characters like a whitespace, comma or semicolon, then
|
||||
/// the name will be automatically wrapped between two `"`.
|
||||
///
|
||||
/// # Exapmle
|
||||
/// ```
|
||||
/// use himalaya::config::model::{Account, Config};
|
||||
///
|
||||
/// fn main() {
|
||||
/// let config = Config::default();
|
||||
///
|
||||
/// let normal_account = Account::new(Some("Acc1"), "acc1@mail.com");
|
||||
/// // notice the semicolon in the name!
|
||||
/// let special_account = Account::new(Some("TL;DR"), "acc2@mail.com");
|
||||
///
|
||||
/// // -- Expeced outputs --
|
||||
/// let expected_normal = Account {
|
||||
/// name: Some("Acc1".to_string()),
|
||||
/// email: "acc1@mail.com".to_string(),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// let expected_special = Account {
|
||||
/// name: Some("\"TL;DR\"".to_string()),
|
||||
/// email: "acc2@mail.com".to_string(),
|
||||
/// .. Account::default()
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(config.address(&normal_account), "Acc1 <acc1@mail.com>");
|
||||
/// assert_eq!(config.address(&special_account), "\"TL;DR\" <acc2@mail.com>");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn address(&self, account: &Account) -> String {
|
||||
let name = account.name.as_ref().unwrap_or(&self.name);
|
||||
format!("{} <{}>", name, account.email)
|
||||
|
||||
let has_special_chars: bool =
|
||||
"()<>[]:;@.,".contains(|special_char| name.contains(special_char));
|
||||
|
||||
if name.is_empty() {
|
||||
format!("{}", account.email)
|
||||
} else if has_special_chars {
|
||||
// so the name has special characters => Wrap it with '"'
|
||||
format!("\"{}\" <{}>", name, account.email)
|
||||
} else {
|
||||
format!("{} <{}>", name, account.email)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_notify_cmd(&self, subject: &str, sender: &str) -> Result<()> {
|
||||
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
|
||||
let subject = subject.as_ref();
|
||||
let sender = sender.as_ref();
|
||||
|
||||
let default_cmd = format!(r#"notify-send "📫 {}" "{}""#, sender, subject);
|
||||
let cmd = self
|
||||
.notify_cmd
|
||||
|
@ -263,6 +419,32 @@ impl Config {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the signature of the given acccount in combination witht the sigantion delimiter.
|
||||
/// If the account doesn't have a signature, then the global signature is used.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use himalaya::config::model::{Config, Account};
|
||||
///
|
||||
/// fn main() {
|
||||
/// let config = Config {
|
||||
/// signature: Some("Global signature".to_string()),
|
||||
/// .. Config::default()
|
||||
/// };
|
||||
///
|
||||
/// // a config without a global signature
|
||||
/// let config_no_global = Config::default();
|
||||
///
|
||||
/// let account1 = Account::new_with_signature(Some("Account Name"), "mail@address.com", Some("Cya"));
|
||||
/// let account2 = Account::new(Some("Bruh"), "mail@address.com");
|
||||
///
|
||||
/// // Hint: Don't forget the default signature delimiter: '\n-- \n'
|
||||
/// assert_eq!(config.signature(&account1), Some("\n-- \nCya".to_string()));
|
||||
/// assert_eq!(config.signature(&account2), Some("\n-- \nGlobal signature".to_string()));
|
||||
///
|
||||
/// assert_eq!(config_no_global.signature(&account2), None);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn signature(&self, account: &Account) -> Option<String> {
|
||||
let default_sig_delim = String::from("-- \n");
|
||||
let sig_delim = account
|
||||
|
@ -278,7 +460,7 @@ impl Config {
|
|||
.map(|sig| sig.to_string())
|
||||
.and_then(|sig| fs::read_to_string(sig).ok())
|
||||
.or_else(|| sig.map(|sig| sig.to_owned()))
|
||||
.map(|sig| String::new() + sig_delim + sig.as_ref())
|
||||
.map(|sig| format!("\n{}{}", sig_delim, sig))
|
||||
}
|
||||
|
||||
pub fn default_page_size(&self, account: &Account) -> usize {
|
||||
|
@ -312,17 +494,57 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
downloads_dir: None,
|
||||
notify_cmd: None,
|
||||
signature_delimiter: None,
|
||||
signature: None,
|
||||
default_page_size: None,
|
||||
watch_cmds: None,
|
||||
accounts: HashMap::new(),
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[cfg(test)]
|
||||
mod config_test {
|
||||
|
||||
use crate::config::model::{Account, Config};
|
||||
|
||||
// a quick way to get a config instance for testing
|
||||
fn get_config() -> Config {
|
||||
Config {
|
||||
name: String::from("Config Name"),
|
||||
..Config::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_account_by_name() {
|
||||
let mut config = get_config();
|
||||
|
||||
let account1 = Account::new(None, "one@mail.com");
|
||||
let account2 = Account::new(Some("Two"), "two@mail.com");
|
||||
|
||||
// add some accounts
|
||||
config.accounts.insert("One".to_string(), account1.clone());
|
||||
config.accounts.insert("Two".to_string(), account2.clone());
|
||||
|
||||
let ret1 = config.find_account_by_name(Some("One")).unwrap();
|
||||
let ret2 = config.find_account_by_name(Some("Two")).unwrap();
|
||||
|
||||
assert_eq!(*ret1, account1);
|
||||
assert_eq!(*ret2, account2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_address() {
|
||||
let config = get_config();
|
||||
|
||||
let account1 = Account::new(None, "one@mail.com");
|
||||
let account2 = Account::new(Some("Two"), "two@mail.com");
|
||||
let account3 = Account::new(Some("TL;DR"), "three@mail.com");
|
||||
let account4 = Account::new(Some("TL,DR"), "lol@mail.com");
|
||||
let account5 = Account::new(Some("TL:DR"), "rofl@mail.com");
|
||||
let account6 = Account::new(Some("TL.DR"), "rust@mail.com");
|
||||
|
||||
assert_eq!(&config.address(&account1), "Config Name <one@mail.com>");
|
||||
assert_eq!(&config.address(&account2), "Two <two@mail.com>");
|
||||
assert_eq!(&config.address(&account3), "\"TL;DR\" <three@mail.com>");
|
||||
assert_eq!(&config.address(&account4), "\"TL,DR\" <lol@mail.com>");
|
||||
assert_eq!(&config.address(&account5), "\"TL:DR\" <rofl@mail.com>");
|
||||
assert_eq!(&config.address(&account6), "\"TL.DR\" <rust@mail.com>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
27
src/ctx.rs
27
src/ctx.rs
|
@ -5,22 +5,27 @@ use crate::{
|
|||
output::model::Output,
|
||||
};
|
||||
|
||||
/// `Ctx` stands for `Context` and includes the most "important" structs which are used quite often
|
||||
/// in this crate.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Ctx<'a> {
|
||||
pub config: &'a Config,
|
||||
pub account: &'a Account,
|
||||
pub output: &'a Output,
|
||||
pub mbox: &'a str,
|
||||
pub arg_matches: &'a clap::ArgMatches<'a>,
|
||||
pub config: Config,
|
||||
pub account: Account,
|
||||
pub output: Output,
|
||||
pub mbox: String,
|
||||
pub arg_matches: clap::ArgMatches<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Ctx<'a> {
|
||||
pub fn new(
|
||||
config: &'a Config,
|
||||
account: &'a Account,
|
||||
output: &'a Output,
|
||||
mbox: &'a str,
|
||||
arg_matches: &'a clap::ArgMatches<'a>,
|
||||
pub fn new<S: ToString>(
|
||||
config: Config,
|
||||
account: Account,
|
||||
output: Output,
|
||||
mbox: S,
|
||||
arg_matches: clap::ArgMatches<'a>,
|
||||
) -> Self {
|
||||
let mbox = mbox.to_string();
|
||||
|
||||
Self {
|
||||
config,
|
||||
account,
|
||||
|
|
|
@ -2,7 +2,7 @@ use clap;
|
|||
use error_chain::error_chain;
|
||||
use log::debug;
|
||||
|
||||
use crate::{ctx::Ctx, imap::model::ImapConnector, msg::cli::uid_arg};
|
||||
use crate::{ctx::Ctx, flag::model::Flags, imap::model::ImapConnector, msg::cli::uid_arg};
|
||||
|
||||
error_chain! {
|
||||
links {
|
||||
|
@ -12,13 +12,13 @@ error_chain! {
|
|||
|
||||
fn flags_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("flags")
|
||||
.help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11)")
|
||||
.help("IMAP flags (see https://tools.ietf.org/html/rfc3501#page-11). Just write the flag name without the backslash. Example: --flags \"Seen Answered\"")
|
||||
.value_name("FLAGS…")
|
||||
.multiple(true)
|
||||
.required(true)
|
||||
}
|
||||
|
||||
pub fn flag_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![clap::SubCommand::with_name("flags")
|
||||
.about("Handles flags")
|
||||
.subcommand(
|
||||
|
@ -42,7 +42,7 @@ pub fn flag_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
|||
)]
|
||||
}
|
||||
|
||||
pub fn flag_matches(ctx: &Ctx) -> Result<bool> {
|
||||
pub fn matches(ctx: &Ctx) -> Result<bool> {
|
||||
if let Some(matches) = ctx.arg_matches.subcommand_matches("set") {
|
||||
debug!("set command matched");
|
||||
|
||||
|
@ -51,9 +51,10 @@ pub fn flag_matches(ctx: &Ctx) -> Result<bool> {
|
|||
|
||||
let flags = matches.value_of("flags").unwrap();
|
||||
debug!("flags: {}", flags);
|
||||
let flags = Flags::from(flags);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
imap_conn.set_flags(ctx.mbox, uid, flags)?;
|
||||
imap_conn.set_flags(&ctx.mbox, uid, flags)?;
|
||||
|
||||
imap_conn.logout();
|
||||
return Ok(true);
|
||||
|
@ -67,9 +68,10 @@ pub fn flag_matches(ctx: &Ctx) -> Result<bool> {
|
|||
|
||||
let flags = matches.value_of("flags").unwrap();
|
||||
debug!("flags: {}", flags);
|
||||
let flags = Flags::from(flags);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
imap_conn.add_flags(ctx.mbox, uid, flags)?;
|
||||
imap_conn.add_flags(&ctx.mbox, uid, flags)?;
|
||||
|
||||
imap_conn.logout();
|
||||
return Ok(true);
|
||||
|
@ -83,9 +85,10 @@ pub fn flag_matches(ctx: &Ctx) -> Result<bool> {
|
|||
|
||||
let flags = matches.value_of("flags").unwrap();
|
||||
debug!("flags: {}", flags);
|
||||
let flags = Flags::from(flags);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
imap_conn.remove_flags(ctx.mbox, uid, flags)?;
|
||||
imap_conn.remove_flags(&ctx.mbox, uid, flags)?;
|
||||
|
||||
imap_conn.logout();
|
||||
return Ok(true);
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
pub(crate) use imap::types::Flag;
|
||||
use serde::ser::{Serialize, SerializeSeq, Serializer};
|
||||
use std::ops::Deref;
|
||||
|
||||
// Serializable wrapper for `imap::types::Flag`
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct SerializableFlag<'f>(&'f imap::types::Flag<'f>);
|
||||
use std::convert::From;
|
||||
|
||||
impl<'f> Serialize for SerializableFlag<'f> {
|
||||
/// Serializable wrapper for `imap::types::Flag`
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct SerializableFlag<'flag>(&'flag imap::types::Flag<'flag>);
|
||||
|
||||
impl<'flag> Serialize for SerializableFlag<'flag> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
|
@ -26,19 +30,22 @@ impl<'f> Serialize for SerializableFlag<'f> {
|
|||
}
|
||||
}
|
||||
|
||||
// Flags
|
||||
/// This struct type includes all flags which belong to a given mail.
|
||||
/// It's used in the [`Msg.flags`] attribute field of the `Msg` struct. To be more clear: It's just
|
||||
/// a wrapper for the [`imap::types::Flag`] but without a lifetime.
|
||||
///
|
||||
/// [`Msg.flags`]: struct.Msg.html#structfield.flags
|
||||
/// [`imap::types::Flag`]: https://docs.rs/imap/2.4.1/imap/types/enum.Flag.html
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Flags(pub HashSet<Flag<'static>>);
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Flags<'f>(&'f [Flag<'f>]);
|
||||
|
||||
impl<'f> Flags<'f> {
|
||||
pub fn new(flags: &'f [imap::types::Flag<'f>]) -> Self {
|
||||
Self(flags)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> ToString for Flags<'f> {
|
||||
fn to_string(&self) -> String {
|
||||
impl Flags {
|
||||
/// Returns the flags of their respective flag value in the following order:
|
||||
///
|
||||
/// 1. Seen
|
||||
/// 2. Answered
|
||||
/// 3. Flagged
|
||||
pub fn get_signs(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
|
||||
flags.push_str(if self.0.contains(&Flag::Seen) {
|
||||
|
@ -63,25 +70,189 @@ impl<'f> ToString for Flags<'f> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'f> Deref for Flags<'f> {
|
||||
type Target = &'f [Flag<'f>];
|
||||
impl ToString for Flags {
|
||||
fn to_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
|
||||
for flag in &self.0 {
|
||||
match flag {
|
||||
Flag::Seen => flags.push_str("\\Seen "),
|
||||
Flag::Answered => flags.push_str("\\Answered "),
|
||||
Flag::Flagged => flags.push_str("\\Flagged "),
|
||||
Flag::Deleted => flags.push_str("\\Deleted "),
|
||||
Flag::Draft => flags.push_str("\\Draft "),
|
||||
Flag::Recent => flags.push_str("\\Recent "),
|
||||
Flag::MayCreate => flags.push_str("\\MayCreate "),
|
||||
Flag::Custom(cow) => flags.push_str(&format!("\\{} ", cow)),
|
||||
_ => panic!("Unknown flag!"),
|
||||
}
|
||||
}
|
||||
|
||||
// remove the trailing whitespaces
|
||||
flags = flags.trim_end_matches(' ').to_string();
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&[imap::types::Flag<'a>]> for Flags {
|
||||
fn from(flags: &[imap::types::Flag<'a>]) -> Self {
|
||||
Self(
|
||||
flags
|
||||
.iter()
|
||||
.map(|flag| convert_to_static(flag).unwrap())
|
||||
.collect::<HashSet<Flag<'static>>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<imap::types::Flag<'a>>> for Flags {
|
||||
fn from(flags: Vec<imap::types::Flag<'a>>) -> Self {
|
||||
Self(
|
||||
flags
|
||||
.iter()
|
||||
.map(|flag| convert_to_static(flag).unwrap())
|
||||
.collect::<HashSet<Flag<'static>>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converst a string of flags into their appropriate flag representation. For example `"Seen"` is
|
||||
/// gonna be convertred to `Flag::Seen`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use himalaya::flag::model::Flags;
|
||||
/// use imap::types::Flag;
|
||||
/// use std::collections::HashSet;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let flags = "Seen Answered";
|
||||
///
|
||||
/// let mut expected = HashSet::new();
|
||||
/// expected.insert(Flag::Seen);
|
||||
/// expected.insert(Flag::Answered);
|
||||
///
|
||||
/// let output = Flags::from(flags);
|
||||
///
|
||||
/// assert_eq!(output.0, expected);
|
||||
/// }
|
||||
/// ```
|
||||
impl From<&str> for Flags {
|
||||
fn from(flags: &str) -> Self {
|
||||
let mut content: HashSet<Flag<'static>> = HashSet::new();
|
||||
|
||||
for flag in flags.split_ascii_whitespace() {
|
||||
match flag {
|
||||
"Seen" => content.insert(Flag::Seen),
|
||||
"Answered" => content.insert(Flag::Answered),
|
||||
"Deleted" => content.insert(Flag::Flagged),
|
||||
"Draft" => content.insert(Flag::Draft),
|
||||
"Recent" => content.insert(Flag::Recent),
|
||||
"MayCreate" => content.insert(Flag::MayCreate),
|
||||
_other => content.insert(Flag::Custom(Cow::Owned(_other.to_string()))),
|
||||
};
|
||||
}
|
||||
|
||||
Self(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Flags {
|
||||
type Target = HashSet<Flag<'static>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'f> Serialize for Flags<'f> {
|
||||
impl DerefMut for Flags {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Flags {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
|
||||
for flag in self.0 {
|
||||
for flag in &self.0 {
|
||||
seq.serialize_element(&SerializableFlag(flag))?;
|
||||
}
|
||||
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
// == Helper Functions ==
|
||||
/// HINT: This function is only needed as long this pull request hasn't been
|
||||
/// merged yet: https://github.com/jonhoo/rust-imap/pull/206
|
||||
fn convert_to_static<'func>(flag: &'func Flag) -> Result<Flag<'static>, ()> {
|
||||
match flag {
|
||||
Flag::Seen => Ok(Flag::Seen),
|
||||
Flag::Answered => Ok(Flag::Answered),
|
||||
Flag::Flagged => Ok(Flag::Flagged),
|
||||
Flag::Deleted => Ok(Flag::Deleted),
|
||||
Flag::Draft => Ok(Flag::Draft),
|
||||
Flag::Recent => Ok(Flag::Recent),
|
||||
Flag::MayCreate => Ok(Flag::MayCreate),
|
||||
Flag::Custom(cow) => Ok(Flag::Custom(Cow::Owned(cow.to_string()))),
|
||||
&_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::flag::model::Flags;
|
||||
use imap::types::Flag;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn test_get_signs() {
|
||||
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
|
||||
|
||||
assert_eq!(flags.get_signs(), " ↵ ".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string() {
|
||||
let flags = Flags::from("Seen Answered");
|
||||
|
||||
let expected = Flags::from(vec![Flag::Seen, Flag::Answered]);
|
||||
|
||||
assert_eq!(flags, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
|
||||
|
||||
// since we can't influence the order in the HashSet, we're gonna convert it into a vec,
|
||||
// sort it according to the names and compare it aftwards.
|
||||
let flag_string = flags.to_string();
|
||||
let mut flag_vec: Vec<String> = flag_string
|
||||
.split_ascii_whitespace()
|
||||
.map(|word| word.to_string())
|
||||
.collect();
|
||||
flag_vec.sort();
|
||||
|
||||
assert_eq!(
|
||||
flag_vec,
|
||||
vec!["\\Answered".to_string(), "\\Seen".to_string()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_vec() {
|
||||
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
|
||||
|
||||
let mut expected = HashSet::new();
|
||||
expected.insert(Flag::Seen);
|
||||
expected.insert(Flag::Answered);
|
||||
|
||||
assert_eq!(flags.0, expected);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ error_chain! {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn imap_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![
|
||||
clap::SubCommand::with_name("notify")
|
||||
.about("Notifies when new messages arrive in the given mailbox")
|
||||
|
@ -37,7 +37,7 @@ pub fn imap_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
|||
]
|
||||
}
|
||||
|
||||
pub fn imap_matches(ctx: &Ctx) -> Result<bool> {
|
||||
pub fn matches(ctx: &Ctx) -> Result<bool> {
|
||||
if let Some(matches) = ctx.arg_matches.subcommand_matches("notify") {
|
||||
debug!("notify command matched");
|
||||
|
||||
|
|
|
@ -2,16 +2,42 @@ use error_chain::error_chain;
|
|||
use imap;
|
||||
use log::{debug, trace};
|
||||
use native_tls::{self, TlsConnector, TlsStream};
|
||||
use std::{collections::HashSet, iter::FromIterator, net::TcpStream};
|
||||
use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream};
|
||||
|
||||
use crate::{config::model::Account, ctx::Ctx, flag::model::Flag, msg::model::Msg};
|
||||
use crate::config::model::Account;
|
||||
use crate::ctx::Ctx;
|
||||
use crate::flag::model::Flags;
|
||||
use crate::msg::model::Msg;
|
||||
|
||||
error_chain! {
|
||||
links {
|
||||
Config(crate::config::model::Error, crate::config::model::ErrorKind);
|
||||
MessageError(crate::msg::model::Error, crate::msg::model::ErrorKind);
|
||||
}
|
||||
}
|
||||
|
||||
/// A little helper function to create a similiar error output. (to avoid duplicated code)
|
||||
fn format_err_msg(description: &str, account: &Account) -> String {
|
||||
format!("{}. Your account settings: \n{:#?}", description, account)
|
||||
}
|
||||
|
||||
/// The main struct to create a connection to your imap-server.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use himalaya::imap::model::ImapConnector;
|
||||
/// use himalaya::config::model::Account;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let account = Account::default();
|
||||
/// let mut imap_conn = ImapConnector::new(&account).unwrap();
|
||||
///
|
||||
/// // do you stuff with the connection...
|
||||
///
|
||||
/// // Be nice to the server and say 'Bye!'
|
||||
/// imap_conn.logout();
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ImapConnector<'a> {
|
||||
pub account: &'a Account,
|
||||
|
@ -19,33 +45,41 @@ pub struct ImapConnector<'a> {
|
|||
}
|
||||
|
||||
impl<'a> ImapConnector<'a> {
|
||||
/// Creates a new connection with the settings of the given account.
|
||||
///
|
||||
/// Please call the `logout` method below if you don't need the connection anymore! Be nice
|
||||
/// to the server ;)
|
||||
pub fn new(account: &'a Account) -> Result<Self> {
|
||||
debug!("create TLS builder");
|
||||
let insecure = account.imap_insecure();
|
||||
let tls = TlsConnector::builder()
|
||||
let ssl_conn = TlsConnector::builder()
|
||||
.danger_accept_invalid_certs(insecure)
|
||||
.danger_accept_invalid_hostnames(insecure)
|
||||
.build()
|
||||
.chain_err(|| "Could not create TLS connector")?;
|
||||
.chain_err(|| format_err_msg("Could not create TLS connector", account))?;
|
||||
|
||||
debug!("create client");
|
||||
let client = if account.imap_starttls() {
|
||||
imap::connect_starttls(account.imap_addr(), &account.imap_host, &tls)
|
||||
.chain_err(|| "Could not connect using STARTTLS")
|
||||
} else {
|
||||
imap::connect(account.imap_addr(), &account.imap_host, &tls)
|
||||
.chain_err(|| "Could not connect using TLS")
|
||||
}?;
|
||||
let mut client_builder = imap::ClientBuilder::new(&account.imap_host, account.imap_port);
|
||||
if account.imap_starttls() {
|
||||
debug!("enable STARTTLS");
|
||||
client_builder.starttls();
|
||||
}
|
||||
let client = client_builder
|
||||
.connect(|domain, tcp| Ok(TlsConnector::connect(&ssl_conn, domain, tcp)?))
|
||||
.chain_err(|| format_err_msg("Could not connect to IMAP server", account))?;
|
||||
|
||||
debug!("create session");
|
||||
let sess = client
|
||||
.login(&account.imap_login, &account.imap_passwd()?)
|
||||
.map_err(|res| res.0)
|
||||
.chain_err(|| "Could not login to IMAP server")?;
|
||||
.chain_err(|| format_err_msg("Could not login to IMAP server", account))?;
|
||||
|
||||
Ok(Self { account, sess })
|
||||
}
|
||||
|
||||
/// Closes the connection.
|
||||
///
|
||||
/// Always call this if you don't need the connection anymore!
|
||||
pub fn logout(&mut self) {
|
||||
debug!("logout");
|
||||
match self.sess.logout() {
|
||||
|
@ -53,7 +87,30 @@ impl<'a> ImapConnector<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn set_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
|
||||
/// Applies the given flags to the msg.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use himalaya::imap::model::ImapConnector;
|
||||
/// use himalaya::config::model::Account;
|
||||
/// use himalaya::flag::model::Flags;
|
||||
/// use imap::types::Flag;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let account = Account::default();
|
||||
/// let mut imap_conn = ImapConnector::new(&account).unwrap();
|
||||
/// let flags = Flags::from(vec![Flag::Seen]);
|
||||
///
|
||||
/// // Mark the message with the UID 42 in the mailbox "rofl" as "Seen" and wipe all other
|
||||
/// // flags
|
||||
/// imap_conn.set_flags("rofl", "42", flags).unwrap();
|
||||
///
|
||||
/// imap_conn.logout();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn set_flags(&mut self, mbox: &str, uid_seq: &str, flags: Flags) -> Result<()> {
|
||||
let flags: String = flags.to_string();
|
||||
|
||||
self.sess
|
||||
.select(mbox)
|
||||
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
|
||||
|
@ -65,7 +122,29 @@ impl<'a> ImapConnector<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
|
||||
/// Add the given flags to the given mail.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use himalaya::imap::model::ImapConnector;
|
||||
/// use himalaya::config::model::Account;
|
||||
/// use himalaya::flag::model::Flags;
|
||||
/// use imap::types::Flag;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let account = Account::default();
|
||||
/// let mut imap_conn = ImapConnector::new(&account).unwrap();
|
||||
/// let flags = Flags::from(vec![Flag::Seen]);
|
||||
///
|
||||
/// // Mark the message with the UID 42 in the mailbox "rofl" as "Seen"
|
||||
/// imap_conn.add_flags("rofl", "42", flags).unwrap();
|
||||
///
|
||||
/// imap_conn.logout();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn add_flags(&mut self, mbox: &str, uid_seq: &str, flags: Flags) -> Result<()> {
|
||||
let flags: String = flags.to_string();
|
||||
|
||||
self.sess
|
||||
.select(mbox)
|
||||
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
|
||||
|
@ -77,7 +156,11 @@ impl<'a> ImapConnector<'a> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_flags(&mut self, mbox: &str, uid_seq: &str, flags: &str) -> Result<()> {
|
||||
/// Remove the flags to the message by the given information. Take a look on the example above.
|
||||
/// It's pretty similar.
|
||||
pub fn remove_flags(&mut self, mbox: &str, uid_seq: &str, flags: Flags) -> Result<()> {
|
||||
let flags = flags.to_string();
|
||||
|
||||
self.sess
|
||||
.select(mbox)
|
||||
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
|
||||
|
@ -147,11 +230,14 @@ impl<'a> ImapConnector<'a> {
|
|||
.chain_err(|| "Could not fetch new messages enveloppe")?;
|
||||
|
||||
for fetch in fetches.iter() {
|
||||
let msg = Msg::from(fetch);
|
||||
let msg = Msg::try_from(fetch)?;
|
||||
let uid = fetch.uid.ok_or_else(|| {
|
||||
format!("Could not retrieve message {}'s UID", fetch.message)
|
||||
})?;
|
||||
ctx.config.run_notify_cmd(&msg.subject, &msg.sender)?;
|
||||
|
||||
let subject = msg.headers.subject.clone().unwrap_or_default();
|
||||
ctx.config.run_notify_cmd(&subject, &msg.headers.from[0])?;
|
||||
|
||||
debug!("notify message: {}", uid);
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
|
@ -266,25 +352,30 @@ impl<'a> ImapConnector<'a> {
|
|||
Ok(Some(fetches))
|
||||
}
|
||||
|
||||
pub fn read_msg(&mut self, mbox: &str, uid: &str) -> Result<Vec<u8>> {
|
||||
/// Get the message according to the given `mbox` and `uid`.
|
||||
pub fn get_msg(&mut self, mbox: &str, uid: &str) -> Result<Msg> {
|
||||
self.sess
|
||||
.select(mbox)
|
||||
.chain_err(|| format!("Could not select mailbox `{}`", mbox))?;
|
||||
|
||||
match self
|
||||
.sess
|
||||
.uid_fetch(uid, "(FLAGS BODY[])")
|
||||
.uid_fetch(uid, "(FLAGS BODY[] ENVELOPE INTERNALDATE)")
|
||||
.chain_err(|| "Could not fetch bodies")?
|
||||
.first()
|
||||
{
|
||||
None => Err(format!("Could not find message `{}`", uid).into()),
|
||||
Some(fetch) => Ok(fetch.body().unwrap_or(&[]).to_vec()),
|
||||
Some(fetch) => Ok(Msg::try_from(fetch)?),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_msg(&mut self, mbox: &str, msg: &[u8], flags: Vec<Flag>) -> Result<()> {
|
||||
/// Append the given `msg` to `mbox`.
|
||||
pub fn append_msg(&mut self, mbox: &str, msg: &mut Msg) -> Result<()> {
|
||||
let body = msg.into_bytes()?;
|
||||
let flags: HashSet<imap::types::Flag<'static>> = (*msg.flags).clone();
|
||||
|
||||
self.sess
|
||||
.append(mbox, msg)
|
||||
.append(mbox, &body)
|
||||
.flags(flags)
|
||||
.finish()
|
||||
.chain_err(|| format!("Could not append message to `{}`", mbox))?;
|
||||
|
|
|
@ -49,19 +49,19 @@ pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> {
|
|||
}
|
||||
}
|
||||
|
||||
debug!("[input] create draft");
|
||||
debug!("[Input] create draft");
|
||||
File::create(&draft_path)
|
||||
.chain_err(|| format!("Could not create draft file {:?}", draft_path))?
|
||||
.write(tpl)
|
||||
.chain_err(|| format!("Could not write draft file {:?}", draft_path))?;
|
||||
|
||||
debug!("[input] open editor");
|
||||
debug!("[Input] open editor");
|
||||
Command::new(env::var("EDITOR").chain_err(|| "Could not find `EDITOR` env var")?)
|
||||
.arg(&draft_path)
|
||||
.status()
|
||||
.chain_err(|| "Could not launch editor")?;
|
||||
|
||||
debug!("[input] read draft");
|
||||
debug!("[Input] read draft");
|
||||
let mut draft = String::new();
|
||||
File::open(&draft_path)
|
||||
.chain_err(|| format!("Could not open draft file {:?}", draft_path))?
|
||||
|
|
38
src/lib.rs
38
src/lib.rs
|
@ -1,11 +1,49 @@
|
|||
//! # Welcome to Himalaya!
|
||||
//! Here's a little summary of how to read the code of himalaya:
|
||||
//! Each module includes three "main" files:
|
||||
//! - `model.rs`: **The "main" file** of each module which includes the main implementation of the given
|
||||
//! module.
|
||||
//! - `cli.rs`: Includes the subcommands and arguments which are related to the module.
|
||||
//!
|
||||
//! For example the `read` subcommand is in the `msg/cli.rs` file because it's related to the
|
||||
//! msg you want to read.
|
||||
//!
|
||||
//! - `mod.rs`: Includes all other files in the module. Click [here] for more information.
|
||||
//!
|
||||
//! [here]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html
|
||||
|
||||
/// `comp` stands for `completion`. This module makes it possible to create autocompletion-settings
|
||||
/// for himalaya for your shell :)
|
||||
pub mod comp;
|
||||
|
||||
/// Everything which is related to the config files. For example the structure of your config file.
|
||||
pub mod config;
|
||||
|
||||
/// A often used-struct to help us to access the most often used structs.
|
||||
pub mod ctx;
|
||||
|
||||
/// A wrapper for representing a flag of a message or mailbox. For example the delete-flag or
|
||||
/// read-flag.
|
||||
pub mod flag;
|
||||
|
||||
/// A wrapper for creating connections easier to the IMAP-Servers.
|
||||
pub mod imap;
|
||||
|
||||
/// Handles the input-interaction with the user. For example if you want to edit the body of your
|
||||
/// message, his module takes care of the draft and calls your ~(neo)vim~ your favourite editor.
|
||||
pub mod input;
|
||||
|
||||
/// Everything which is related to mboxes, for example creating or deleting some.
|
||||
pub mod mbox;
|
||||
|
||||
/// Includes everything related to a message. This means: Body, Headers, Attachments, etc.
|
||||
pub mod msg;
|
||||
|
||||
/// Handles the output. For example the JSON and HTML output.
|
||||
pub mod output;
|
||||
|
||||
/// This module takes care for sending your mails!
|
||||
pub mod smtp;
|
||||
|
||||
/// The TUI for listing the mails for example.
|
||||
pub mod table;
|
||||
|
|
42
src/main.rs
42
src/main.rs
|
@ -6,13 +6,11 @@ use std::{env, path::PathBuf, process::exit};
|
|||
use url::{self, Url};
|
||||
|
||||
use himalaya::{
|
||||
comp::cli::{comp_matches, comp_subcmds},
|
||||
comp,
|
||||
config::{cli::config_args, model::Config},
|
||||
ctx::Ctx,
|
||||
flag::cli::{flag_matches, flag_subcmds},
|
||||
imap::cli::{imap_matches, imap_subcmds},
|
||||
mbox::cli::{mbox_matches, mbox_source_arg, mbox_subcmds},
|
||||
msg::cli::{msg_matches, msg_matches_mailto, msg_subcmds},
|
||||
flag, imap, mbox,
|
||||
msg::{self, cli::msg_matches_mailto},
|
||||
output::{cli::output_args, model::Output},
|
||||
};
|
||||
|
||||
|
@ -38,12 +36,12 @@ fn parse_args<'a>() -> clap::App<'a, 'a> {
|
|||
.setting(clap::AppSettings::InferSubcommands)
|
||||
.args(&output_args())
|
||||
.args(&config_args())
|
||||
.arg(mbox_source_arg())
|
||||
.subcommands(flag_subcmds())
|
||||
.subcommands(imap_subcmds())
|
||||
.subcommands(mbox_subcmds())
|
||||
.subcommands(msg_subcmds())
|
||||
.subcommands(comp_subcmds())
|
||||
.arg(mbox::cli::source_arg())
|
||||
.subcommands(flag::cli::subcmds())
|
||||
.subcommands(imap::cli::subcmds())
|
||||
.subcommands(mbox::cli::subcmds())
|
||||
.subcommands(msg::cli::subcmds())
|
||||
.subcommands(comp::cli::subcmds())
|
||||
}
|
||||
|
||||
fn run() -> Result<()> {
|
||||
|
@ -52,13 +50,15 @@ fn run() -> Result<()> {
|
|||
);
|
||||
|
||||
let raw_args: Vec<String> = env::args().collect();
|
||||
|
||||
// This is used if you click on a mailaddress in the webbrowser
|
||||
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
|
||||
let config = Config::new(None)?;
|
||||
let account = config.find_account_by_name(None)?;
|
||||
let account = config.find_account_by_name(None)?.clone();
|
||||
let output = Output::new("plain");
|
||||
let mbox = "INBOX";
|
||||
let arg_matches = ArgMatches::default();
|
||||
let app = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let app = Ctx::new(config, account, output, mbox, arg_matches);
|
||||
let url = Url::parse(&raw_args[1])?;
|
||||
return Ok(msg_matches_mailto(&app, &url)?);
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ fn run() -> Result<()> {
|
|||
let arg_matches = args.get_matches();
|
||||
|
||||
// Check completion before init config
|
||||
if comp_matches(parse_args, &arg_matches)? {
|
||||
if comp::cli::matches(parse_args, &arg_matches)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -75,6 +75,7 @@ fn run() -> Result<()> {
|
|||
debug!("output: {:?}", output);
|
||||
|
||||
debug!("init config");
|
||||
|
||||
let custom_config: Option<PathBuf> = arg_matches.value_of("config").map(|s| s.into());
|
||||
debug!("custom config path: {:?}", custom_config);
|
||||
let config = Config::new(custom_config)?;
|
||||
|
@ -82,16 +83,19 @@ fn run() -> Result<()> {
|
|||
|
||||
let account_name = arg_matches.value_of("account");
|
||||
debug!("init account: {}", account_name.unwrap_or("default"));
|
||||
let account = config.find_account_by_name(account_name)?;
|
||||
let account = config.find_account_by_name(account_name)?.clone();
|
||||
trace!("account: {:?}", account);
|
||||
|
||||
let mbox = arg_matches.value_of("mailbox").unwrap();
|
||||
let mbox = arg_matches.value_of("mailbox").unwrap().to_string();
|
||||
debug!("mailbox: {}", mbox);
|
||||
|
||||
debug!("begin matching");
|
||||
let app = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let _matched =
|
||||
mbox_matches(&app)? || flag_matches(&app)? || imap_matches(&app)? || msg_matches(&app)?;
|
||||
|
||||
let app = Ctx::new(config, account, output, mbox, arg_matches);
|
||||
let _matched = mbox::cli::matches(&app)?
|
||||
|| flag::cli::matches(&app)?
|
||||
|| imap::cli::matches(&app)?
|
||||
|| msg::cli::matches(&app)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -10,28 +10,14 @@ error_chain! {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn mbox_source_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mailbox")
|
||||
.short("m")
|
||||
.long("mailbox")
|
||||
.help("Selects a specific mailbox")
|
||||
.value_name("MAILBOX")
|
||||
.default_value("INBOX")
|
||||
}
|
||||
|
||||
pub fn mbox_target_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("target")
|
||||
.help("Specifies the targetted mailbox")
|
||||
.value_name("TARGET")
|
||||
}
|
||||
|
||||
pub fn mbox_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
// == Main functions ==
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![clap::SubCommand::with_name("mailboxes")
|
||||
.aliases(&["mailbox", "mboxes", "mbox", "m"])
|
||||
.about("Lists all mailboxes")]
|
||||
}
|
||||
|
||||
pub fn mbox_matches(ctx: &Ctx) -> Result<bool> {
|
||||
pub fn matches(ctx: &Ctx) -> Result<bool> {
|
||||
if let Some(_) = ctx.arg_matches.subcommand_matches("mailboxes") {
|
||||
debug!("mailboxes command matched");
|
||||
|
||||
|
@ -49,3 +35,19 @@ pub fn mbox_matches(ctx: &Ctx) -> Result<bool> {
|
|||
debug!("nothing matched");
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
// == Argument Functions ==
|
||||
pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mailbox")
|
||||
.short("m")
|
||||
.long("mailbox")
|
||||
.help("Selects a specific mailbox")
|
||||
.value_name("MAILBOX")
|
||||
.default_value("INBOX")
|
||||
}
|
||||
|
||||
pub fn mbox_target_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("target")
|
||||
.help("Specifies the targetted mailbox")
|
||||
.value_name("TARGET")
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use imap;
|
||||
use imap::types::NameAttribute;
|
||||
use serde::{
|
||||
ser::{self, SerializeSeq},
|
||||
Serialize,
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
|
||||
use crate::table::{Cell, Row, Table};
|
||||
|
@ -10,16 +12,16 @@ use crate::table::{Cell, Row, Table};
|
|||
// Attribute
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct SerializableAttribute<'a>(&'a imap::types::NameAttribute<'a>);
|
||||
struct SerializableAttribute<'a>(&'a NameAttribute<'a>);
|
||||
|
||||
impl<'a> Into<&'a str> for &'a SerializableAttribute<'a> {
|
||||
fn into(self) -> &'a str {
|
||||
match &self.0 {
|
||||
imap::types::NameAttribute::NoInferiors => "\\NoInferiors",
|
||||
imap::types::NameAttribute::NoSelect => "\\NoSelect",
|
||||
imap::types::NameAttribute::Marked => "\\Marked",
|
||||
imap::types::NameAttribute::Unmarked => "\\Unmarked",
|
||||
imap::types::NameAttribute::Custom(cow) => cow,
|
||||
NameAttribute::NoInferiors => "\\NoInferiors",
|
||||
NameAttribute::NoSelect => "\\NoSelect",
|
||||
NameAttribute::Marked => "\\Marked",
|
||||
NameAttribute::Unmarked => "\\Unmarked",
|
||||
NameAttribute::Custom(cow) => cow,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,41 +35,47 @@ impl<'a> ser::Serialize for SerializableAttribute<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Represents the attributes of a mailbox.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct Attributes<'a>(&'a [imap::types::NameAttribute<'a>]);
|
||||
pub struct Attributes(pub HashSet<NameAttribute<'static>>);
|
||||
|
||||
impl<'a> From<&'a [imap::types::NameAttribute<'a>]> for Attributes<'a> {
|
||||
fn from(attrs: &'a [imap::types::NameAttribute<'a>]) -> Self {
|
||||
Self(attrs)
|
||||
impl<'a> From<&[NameAttribute<'a>]> for Attributes {
|
||||
fn from(attrs: &[NameAttribute<'a>]) -> Self {
|
||||
Self(
|
||||
attrs
|
||||
.iter()
|
||||
.map(|attribute| convert_to_static(attribute).unwrap())
|
||||
.collect::<HashSet<NameAttribute<'static>>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToString for Attributes<'a> {
|
||||
impl ToString for Attributes {
|
||||
fn to_string(&self) -> String {
|
||||
match self.0.len() {
|
||||
0 => String::new(),
|
||||
1 => {
|
||||
let attr = &SerializableAttribute(&self.0[0]);
|
||||
let attr: &str = attr.into();
|
||||
attr.to_owned()
|
||||
}
|
||||
_ => {
|
||||
let attr = &SerializableAttribute(&self.0[0]);
|
||||
let attr: &str = attr.into();
|
||||
format!("{}, {}", attr, Attributes(&self.0[1..]).to_string())
|
||||
}
|
||||
let mut attributes = String::new();
|
||||
|
||||
for attribute in &self.0 {
|
||||
let attribute = SerializableAttribute(&attribute);
|
||||
attributes.push_str((&attribute).into());
|
||||
attributes.push_str(", ");
|
||||
}
|
||||
|
||||
// remove the trailing whitespace with the comma
|
||||
attributes = attributes.trim_end_matches(' ').to_string();
|
||||
attributes.pop();
|
||||
|
||||
attributes
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ser::Serialize for Attributes<'a> {
|
||||
impl ser::Serialize for Attributes {
|
||||
fn serialize<T>(&self, serializer: T) -> Result<T::Ok, T::Error>
|
||||
where
|
||||
T: ser::Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
|
||||
|
||||
for attr in self.0 {
|
||||
for attr in &self.0 {
|
||||
seq.serialize_element(&SerializableAttribute(attr))?;
|
||||
}
|
||||
|
||||
|
@ -75,16 +83,23 @@ impl<'a> ser::Serialize for Attributes<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Mailbox
|
||||
|
||||
// --- Mailbox ---
|
||||
/// Represents a general mailbox.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Mbox<'a> {
|
||||
pub struct Mbox {
|
||||
/// The [hierarchie delimiter].
|
||||
///
|
||||
/// [hierarchie delimiter]: https://docs.rs/imap/2.4.1/imap/types/struct.Name.html#method.delimiter
|
||||
pub delim: String,
|
||||
|
||||
/// The name of the mailbox.
|
||||
pub name: String,
|
||||
pub attributes: Attributes<'a>,
|
||||
|
||||
/// Its attributes.
|
||||
pub attributes: Attributes,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a imap::types::Name> for Mbox<'a> {
|
||||
impl<'a> From<&'a imap::types::Name> for Mbox {
|
||||
fn from(name: &'a imap::types::Name) -> Self {
|
||||
Self {
|
||||
delim: name.delimiter().unwrap_or_default().to_owned(),
|
||||
|
@ -94,7 +109,7 @@ impl<'a> From<&'a imap::types::Name> for Mbox<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a> Table for Mbox<'a> {
|
||||
impl Table for Mbox {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
||||
|
@ -116,19 +131,32 @@ impl<'a> Table for Mbox<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// Mboxes
|
||||
|
||||
// --- Mboxes ---
|
||||
/// A simple wrapper to acces a bunch of mboxes which are in this vector.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Mboxes<'a>(pub Vec<Mbox<'a>>);
|
||||
pub struct Mboxes(pub Vec<Mbox>);
|
||||
|
||||
impl<'a> From<&'a imap::types::ZeroCopy<Vec<imap::types::Name>>> for Mboxes<'a> {
|
||||
impl<'a> From<&'a imap::types::ZeroCopy<Vec<imap::types::Name>>> for Mboxes {
|
||||
fn from(names: &'a imap::types::ZeroCopy<Vec<imap::types::Name>>) -> Self {
|
||||
Self(names.iter().map(Mbox::from).collect::<Vec<_>>())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Mboxes<'_> {
|
||||
impl fmt::Display for Mboxes {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "\n{}", Table::render(&self.0))
|
||||
}
|
||||
}
|
||||
|
||||
// == Helper Functions ==
|
||||
fn convert_to_static<'func>(
|
||||
attribute: &'func NameAttribute<'func>,
|
||||
) -> Result<NameAttribute<'static>, ()> {
|
||||
match attribute {
|
||||
NameAttribute::NoInferiors => Ok(NameAttribute::NoInferiors),
|
||||
NameAttribute::NoSelect => Ok(NameAttribute::NoSelect),
|
||||
NameAttribute::Marked => Ok(NameAttribute::Marked),
|
||||
NameAttribute::Unmarked => Ok(NameAttribute::Unmarked),
|
||||
NameAttribute::Custom(cow) => Ok(NameAttribute::Custom(Cow::Owned(cow.to_string()))),
|
||||
}
|
||||
}
|
||||
|
|
159
src/msg/attachment.rs
Normal file
159
src/msg/attachment.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
use lettre::message::header::ContentType;
|
||||
|
||||
use mailparse::{DispositionType, ParsedMail};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use error_chain::error_chain;
|
||||
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
ContentType(lettre::message::header::ContentTypeErr);
|
||||
FileSytem(std::io::Error);
|
||||
}
|
||||
}
|
||||
|
||||
// == Structs ==
|
||||
/// This struct represents an attachment.
|
||||
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Attachment {
|
||||
/// Holds the filename of an attachment.
|
||||
pub filename: String,
|
||||
|
||||
/// Holds the mime-type of the attachment. For example `text/plain`.
|
||||
pub content_type: ContentType,
|
||||
|
||||
/// Holds the data of the attachment.
|
||||
#[serde(skip_serializing)]
|
||||
pub body_raw: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Attachment {
|
||||
/// Creates a new attachment.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use himalaya::msg::attachment::Attachment;
|
||||
/// let attachment = Attachment::new(
|
||||
/// "VIP Text",
|
||||
/// "text/plain",
|
||||
/// "Some very important text".as_bytes().to_vec());
|
||||
///
|
||||
/// ```
|
||||
pub fn new(filename: &str, content_type: &str, body_raw: Vec<u8>) -> Self {
|
||||
// Use the mime type `text/plain` per default
|
||||
let content_type: ContentType = match content_type.parse() {
|
||||
Ok(lettre_type) => lettre_type,
|
||||
Err(_) => ContentType::TEXT_PLAIN,
|
||||
};
|
||||
|
||||
Self {
|
||||
filename: filename.to_string(),
|
||||
content_type,
|
||||
body_raw,
|
||||
}
|
||||
}
|
||||
|
||||
/// This from function extracts one attachment of a parsed msg.
|
||||
/// If it couldn't create an attachment with the given parsed msg, than it will
|
||||
/// return `None`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use himalaya::msg::attachment::Attachment;
|
||||
///
|
||||
/// let parsed = mailparse::parse_mail(concat![
|
||||
/// "Content-Type: text/plain; charset=utf-8\n",
|
||||
/// "Content-Transfer-Encoding: quoted-printable\n",
|
||||
/// "\n",
|
||||
/// "A plaintext attachment.",
|
||||
/// ].as_bytes()).unwrap();
|
||||
///
|
||||
/// let attachment = Attachment::from_parsed_mail(&parsed);
|
||||
/// ```
|
||||
pub fn from_parsed_mail(parsed_mail: &ParsedMail) -> Option<Self> {
|
||||
if parsed_mail.get_content_disposition().disposition == DispositionType::Attachment {
|
||||
let disposition = parsed_mail.get_content_disposition();
|
||||
let filename = disposition.params.get("filename").unwrap().to_string();
|
||||
let body_raw = parsed_mail.get_body_raw().unwrap_or(Vec::new());
|
||||
let content_type: ContentType = tree_magic::from_u8(&body_raw).parse().unwrap();
|
||||
|
||||
return Some(Self {
|
||||
filename,
|
||||
content_type,
|
||||
body_raw,
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// == Traits ==
|
||||
/// Creates an Attachment with the follwing values:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use himalaya::msg::attachment::Attachment;
|
||||
/// use lettre::message::header::ContentType;
|
||||
///
|
||||
/// let attachment = Attachment {
|
||||
/// filename: String::new(),
|
||||
/// content_type: ContentType::TEXT_PLAIN,
|
||||
/// body_raw: Vec::new(),
|
||||
/// };
|
||||
/// ```
|
||||
impl Default for Attachment {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
filename: String::new(),
|
||||
content_type: ContentType::TEXT_PLAIN,
|
||||
body_raw: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- From Implementations --
|
||||
/// Tries to convert the given file (by the given path) into an attachment.
|
||||
/// It'll try to detect the mime-type/data-type automatically.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// use himalaya::msg::attachment::Attachment;
|
||||
/// use std::convert::TryFrom;
|
||||
///
|
||||
/// let attachment = Attachment::try_from("/some/path.png");
|
||||
/// ```
|
||||
impl<'from> TryFrom<&'from str> for Attachment {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(path: &'from str) -> Result<Self> {
|
||||
let path = Path::new(path);
|
||||
|
||||
// -- Get attachment information --
|
||||
let filename = if let Some(filename) = path.file_name() {
|
||||
filename
|
||||
// `&OsStr` -> `Option<&str>`
|
||||
.to_str()
|
||||
// get rid of the `Option` wrapper
|
||||
.unwrap_or(&String::new())
|
||||
.to_string()
|
||||
} else {
|
||||
// use an empty string
|
||||
String::new()
|
||||
};
|
||||
|
||||
let file_content = fs::read(&path)?;
|
||||
let content_type: ContentType = tree_magic::from_filepath(&path).parse()?;
|
||||
|
||||
Ok(Self {
|
||||
filename,
|
||||
content_type,
|
||||
body_raw: file_content,
|
||||
})
|
||||
}
|
||||
}
|
151
src/msg/body.rs
Normal file
151
src/msg/body.rs
Normal file
|
@ -0,0 +1,151 @@
|
|||
use error_chain::error_chain;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
// == Macros ==
|
||||
error_chain! {
|
||||
foreign_links {
|
||||
ParseContentType(lettre::message::header::ContentTypeErr);
|
||||
}
|
||||
}
|
||||
|
||||
// == Structs ==
|
||||
/// This struct represents the body/content of a msg. For example:
|
||||
///
|
||||
/// ```text
|
||||
/// Dear Mr. Boss,
|
||||
/// I like rust. It's an awesome language. *Change my mind*....
|
||||
///
|
||||
/// Sincerely
|
||||
/// ```
|
||||
///
|
||||
/// This part of the msg/msg would be stored in this struct.
|
||||
#[derive(Clone, Serialize, Debug, PartialEq, Eq)]
|
||||
pub struct Body {
|
||||
/// The text version of a body (if available)
|
||||
pub text: Option<String>,
|
||||
|
||||
/// The html version of a body (if available)
|
||||
pub html: Option<String>,
|
||||
}
|
||||
|
||||
impl Body {
|
||||
/// Returns a new instance of `Body` without any attributes set. (Same as `Body::default()`)
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use himalaya::msg::body::Body;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let body = Body::new();
|
||||
///
|
||||
/// let expected_body = Body {
|
||||
/// text: None,
|
||||
/// html: None,
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(body, expected_body);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// 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<S: ToString>(text: S) -> Self {
|
||||
Self {
|
||||
text: Some(text.to_string()),
|
||||
html: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<S: ToString>(html: S) -> Self {
|
||||
Self {
|
||||
text: None,
|
||||
html: Some(html.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<S: ToString>(text: S, html: S) -> Self {
|
||||
Self {
|
||||
text: Some(text.to_string()),
|
||||
html: Some(html.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == Traits ==
|
||||
impl Default for Body {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
text: None,
|
||||
html: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
write!(formatter, "{}", content)
|
||||
}
|
||||
}
|
773
src/msg/cli.rs
773
src/msg/cli.rs
|
@ -1,27 +1,26 @@
|
|||
use super::body::Body;
|
||||
use super::headers::Headers;
|
||||
use super::model::{Msg, MsgSerialized, Msgs};
|
||||
use url::Url;
|
||||
|
||||
use atty::Stream;
|
||||
use clap;
|
||||
use error_chain::error_chain;
|
||||
use lettre::message::header::ContentTransferEncoding;
|
||||
use log::{debug, error, trace};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
ops::Deref,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use imap::types::Flag;
|
||||
|
||||
use crate::{
|
||||
ctx::Ctx,
|
||||
flag::model::Flag,
|
||||
imap::model::ImapConnector,
|
||||
input,
|
||||
mbox::cli::mbox_target_arg,
|
||||
msg::{
|
||||
model::{Attachments, Msg, Msgs, ReadableMsg},
|
||||
tpl::{
|
||||
cli::{tpl_matches, tpl_subcommand},
|
||||
model::Tpl,
|
||||
},
|
||||
},
|
||||
ctx::Ctx, flag::model::Flags, imap::model::ImapConnector, input, mbox::cli::mbox_target_arg,
|
||||
smtp,
|
||||
};
|
||||
|
||||
|
@ -29,8 +28,7 @@ error_chain! {
|
|||
links {
|
||||
Imap(crate::imap::model::Error, crate::imap::model::ErrorKind);
|
||||
Input(crate::input::Error, crate::input::ErrorKind);
|
||||
MsgModel(crate::msg::model::Error, crate::msg::model::ErrorKind);
|
||||
TplCli(crate::msg::tpl::cli::Error, crate::msg::tpl::cli::ErrorKind);
|
||||
MsgModel(super::model::Error, super::model::ErrorKind);
|
||||
Smtp(crate::smtp::Error, crate::smtp::ErrorKind);
|
||||
}
|
||||
foreign_links {
|
||||
|
@ -38,47 +36,7 @@ error_chain! {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn uid_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("uid")
|
||||
.help("Specifies the targetted message")
|
||||
.value_name("UID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("reply-all")
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
fn page_size_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("page-size")
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
fn page_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("page")
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("1")
|
||||
}
|
||||
|
||||
fn attachment_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("attachments")
|
||||
.help("Adds attachment to the message")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
pub fn msg_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![
|
||||
clap::SubCommand::with_name("list")
|
||||
.aliases(&["lst"])
|
||||
|
@ -151,11 +109,34 @@ pub fn msg_subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
|||
.aliases(&["remove", "rm"])
|
||||
.about("Deletes a message")
|
||||
.arg(uid_arg()),
|
||||
tpl_subcommand(),
|
||||
clap::SubCommand::with_name("template")
|
||||
.aliases(&["tpl"])
|
||||
.about("Generates a message template")
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("new")
|
||||
.aliases(&["n"])
|
||||
.about("Generates a new message template")
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Generates a reply message template")
|
||||
.arg(uid_arg())
|
||||
.arg(reply_all_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a forward message template")
|
||||
.arg(uid_arg())
|
||||
.args(&tpl_args()),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn msg_matches(ctx: &Ctx) -> Result<bool> {
|
||||
pub fn matches(ctx: &Ctx) -> Result<bool> {
|
||||
match ctx.arg_matches.subcommand() {
|
||||
("attachments", Some(matches)) => msg_matches_attachments(ctx, matches),
|
||||
("copy", Some(matches)) => msg_matches_copy(ctx, matches),
|
||||
|
@ -169,13 +150,106 @@ pub fn msg_matches(ctx: &Ctx) -> Result<bool> {
|
|||
("send", Some(matches)) => msg_matches_send(ctx, matches),
|
||||
("write", Some(matches)) => msg_matches_write(ctx, matches),
|
||||
|
||||
("template", Some(matches)) => Ok(tpl_matches(ctx, matches)?),
|
||||
|
||||
("template", Some(matches)) => Ok(msg_matches_tpl(ctx, matches)?),
|
||||
("list", opt_matches) => msg_matches_list(ctx, opt_matches),
|
||||
(_other, opt_matches) => msg_matches_list(ctx, opt_matches),
|
||||
}
|
||||
}
|
||||
|
||||
// == Argument Functions ==
|
||||
/// Returns an Clap-Argument to be able to use `<UID>` in the commandline like
|
||||
/// for the `himalaya read` subcommand.
|
||||
pub(crate) fn uid_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("uid")
|
||||
.help("Specifies the targetted message")
|
||||
.value_name("UID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("reply-all")
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
fn page_size_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("page-size")
|
||||
.help("Page size")
|
||||
.short("s")
|
||||
.long("size")
|
||||
.value_name("INT")
|
||||
}
|
||||
|
||||
fn page_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("page")
|
||||
.help("Page number")
|
||||
.short("p")
|
||||
.long("page")
|
||||
.value_name("INT")
|
||||
.default_value("0")
|
||||
}
|
||||
|
||||
fn attachment_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("attachments")
|
||||
.help("Adds attachment to the message")
|
||||
.short("a")
|
||||
.long("attachment")
|
||||
.value_name("PATH")
|
||||
.multiple(true)
|
||||
}
|
||||
|
||||
fn tpl_args<'a>() -> Vec<clap::Arg<'a, 'a>> {
|
||||
vec![
|
||||
clap::Arg::with_name("subject")
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
clap::Arg::with_name("from")
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("to")
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("cc")
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("bcc")
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("header")
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("KEY: VAL")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("body")
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
clap::Arg::with_name("signature")
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
// == Match functions ==
|
||||
fn msg_matches_list(ctx: &Ctx, opt_matches: Option<&clap::ArgMatches>) -> Result<bool> {
|
||||
debug!("list command matched");
|
||||
|
||||
|
@ -192,12 +266,13 @@ fn msg_matches_list(ctx: &Ctx, opt_matches: Option<&clap::ArgMatches>) -> Result
|
|||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msgs = imap_conn.list_msgs(&ctx.mbox, &page_size, &page)?;
|
||||
let msgs = if let Some(ref fetches) = msgs {
|
||||
Msgs::from(fetches)
|
||||
Msgs::try_from(fetches)?
|
||||
} else {
|
||||
Msgs::new()
|
||||
};
|
||||
|
||||
trace!("messages: {:?}", msgs);
|
||||
|
||||
ctx.output.print(msgs);
|
||||
|
||||
imap_conn.logout();
|
||||
|
@ -249,7 +324,7 @@ fn msg_matches_search(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msgs = imap_conn.search_msgs(&ctx.mbox, &query, &page_size, &page)?;
|
||||
let msgs = if let Some(ref fetches) = msgs {
|
||||
Msgs::from(fetches)
|
||||
Msgs::try_from(fetches)?
|
||||
} else {
|
||||
Msgs::new()
|
||||
};
|
||||
|
@ -271,17 +346,13 @@ fn msg_matches_read(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
debug!("raw: {}", raw);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = imap_conn.read_msg(&ctx.mbox, &uid)?;
|
||||
if raw {
|
||||
let msg =
|
||||
String::from_utf8(msg).chain_err(|| "Could not decode raw message as utf8 string")?;
|
||||
let msg = msg.trim_end_matches("\n");
|
||||
ctx.output.print(msg);
|
||||
} else {
|
||||
let msg = ReadableMsg::from_bytes(&mime, &msg)?;
|
||||
ctx.output.print(msg);
|
||||
}
|
||||
let msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
if raw {
|
||||
ctx.output.print(msg.get_raw_as_string()?);
|
||||
} else {
|
||||
ctx.output.print(MsgSerialized::try_from(&msg)?);
|
||||
}
|
||||
imap_conn.logout();
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -292,30 +363,38 @@ fn msg_matches_attachments(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool
|
|||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", &uid);
|
||||
|
||||
// get the msg and than it's attachments
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = imap_conn.read_msg(&ctx.mbox, &uid)?;
|
||||
let attachments = Attachments::from_bytes(&msg)?;
|
||||
let msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
let attachments = msg.attachments.clone();
|
||||
|
||||
debug!(
|
||||
"{} attachment(s) found for message {}",
|
||||
&attachments.0.len(),
|
||||
&attachments.len(),
|
||||
&uid
|
||||
);
|
||||
for attachment in attachments.0.iter() {
|
||||
|
||||
// Iterate through all attachments and download them to the download
|
||||
// directory of the account.
|
||||
for attachment in &attachments {
|
||||
let filepath = ctx
|
||||
.config
|
||||
.downloads_filepath(&ctx.account, &attachment.filename);
|
||||
|
||||
debug!("downloading {}…", &attachment.filename);
|
||||
fs::write(&filepath, &attachment.raw)
|
||||
|
||||
fs::write(&filepath, &attachment.body_raw)
|
||||
.chain_err(|| format!("Could not save attachment {:?}", filepath))?;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"{} attachment(s) successfully downloaded",
|
||||
&attachments.0.len()
|
||||
&attachments.len()
|
||||
);
|
||||
|
||||
ctx.output.print(format!(
|
||||
"{} attachment(s) successfully downloaded",
|
||||
&attachments.0.len()
|
||||
&attachments.len()
|
||||
));
|
||||
|
||||
imap_conn.logout();
|
||||
|
@ -326,186 +405,163 @@ fn msg_matches_write(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
debug!("write command matched");
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let attachments = matches
|
||||
|
||||
// create the new msg
|
||||
// TODO: Make the header starting customizeable like from template
|
||||
let mut msg = Msg::new_with_headers(
|
||||
&ctx,
|
||||
Headers {
|
||||
subject: Some(String::new()),
|
||||
to: Vec::new(),
|
||||
..Headers::default()
|
||||
},
|
||||
);
|
||||
|
||||
// take care of the attachments
|
||||
let attachment_paths: Vec<&str> = matches
|
||||
.values_of("attachments")
|
||||
.unwrap_or_default()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
let tpl = Tpl::new(&ctx);
|
||||
let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?;
|
||||
let mut msg = Msg::from(content);
|
||||
msg.attachments = attachments;
|
||||
.collect();
|
||||
|
||||
loop {
|
||||
match input::post_edit_choice() {
|
||||
Ok(choice) => match choice {
|
||||
input::PostEditChoice::Send => {
|
||||
debug!("sending message…");
|
||||
let msg = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &msg)?;
|
||||
imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully sent");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Edit => {
|
||||
let content = input::open_editor_with_draft()?;
|
||||
msg = Msg::from(content);
|
||||
}
|
||||
input::PostEditChoice::LocalDraft => break,
|
||||
input::PostEditChoice::RemoteDraft => {
|
||||
debug!("saving to draft…");
|
||||
imap_conn.append_msg("Drafts", &msg.to_vec()?, vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully saved to Drafts");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Discard => {
|
||||
input::remove_draft()?;
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => error!("{}", err),
|
||||
}
|
||||
}
|
||||
attachment_paths
|
||||
.iter()
|
||||
.for_each(|path| msg.add_attachment(path));
|
||||
|
||||
msg_interaction(&ctx, &mut msg, &mut imap_conn)?;
|
||||
|
||||
// let's be nice to the server and say "bye" to the server
|
||||
imap_conn.logout();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn msg_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("reply command matched");
|
||||
|
||||
// -- Preparations --
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
debug!("uid: {}", uid);
|
||||
let attachments = matches
|
||||
|
||||
// Change the msg to a reply-msg.
|
||||
msg.change_to_reply(&ctx, matches.is_present("reply-all"))?;
|
||||
|
||||
// Apply the given attachments to the reply-msg.
|
||||
let attachments: Vec<&str> = matches
|
||||
.values_of("attachments")
|
||||
.unwrap_or_default()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
.collect();
|
||||
|
||||
attachments.iter().for_each(|path| msg.add_attachment(path));
|
||||
|
||||
debug!("found {} attachments", attachments.len());
|
||||
trace!("attachments: {:?}", attachments);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = Msg::from(imap_conn.read_msg(&ctx.mbox, &uid)?);
|
||||
let tpl = if matches.is_present("reply-all") {
|
||||
msg.build_reply_all_tpl(&ctx.config, &ctx.account)?
|
||||
} else {
|
||||
msg.build_reply_tpl(&ctx.config, &ctx.account)?
|
||||
};
|
||||
|
||||
let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?;
|
||||
let mut msg = Msg::from(content);
|
||||
msg.attachments = attachments;
|
||||
|
||||
loop {
|
||||
match input::post_edit_choice() {
|
||||
Ok(choice) => match choice {
|
||||
input::PostEditChoice::Send => {
|
||||
debug!("sending message…");
|
||||
let msg = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &msg)?;
|
||||
imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?;
|
||||
imap_conn.add_flags(&ctx.mbox, uid, "\\Answered")?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully sent");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Edit => {
|
||||
let content = input::open_editor_with_draft()?;
|
||||
msg = Msg::from(content);
|
||||
}
|
||||
input::PostEditChoice::LocalDraft => break,
|
||||
input::PostEditChoice::RemoteDraft => {
|
||||
debug!("saving to draft…");
|
||||
imap_conn.append_msg("Drafts", &msg.to_vec()?, vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully saved to Drafts");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Discard => {
|
||||
input::remove_draft()?;
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => error!("{}", err),
|
||||
}
|
||||
}
|
||||
msg_interaction(&ctx, &mut msg, &mut imap_conn)?;
|
||||
|
||||
imap_conn.logout();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn msg_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("forward command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", uid);
|
||||
let attachments = matches
|
||||
.values_of("attachments")
|
||||
.unwrap_or_default()
|
||||
.map(String::from)
|
||||
.collect::<Vec<_>>();
|
||||
debug!("found {} attachments", attachments.len());
|
||||
trace!("attachments: {:?}", attachments);
|
||||
pub fn msg_matches_mailto(ctx: &Ctx, url: &Url) -> Result<()> {
|
||||
debug!("mailto command matched");
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = Msg::from(imap_conn.read_msg(&ctx.mbox, &uid)?);
|
||||
let tpl = msg.build_forward_tpl(&ctx.config, &ctx.account)?;
|
||||
let content = input::open_editor_with_tpl(&tpl.to_string().as_bytes())?;
|
||||
let mut msg = Msg::from(content);
|
||||
msg.attachments = attachments;
|
||||
|
||||
loop {
|
||||
match input::post_edit_choice() {
|
||||
Ok(choice) => match choice {
|
||||
input::PostEditChoice::Send => {
|
||||
debug!("sending message…");
|
||||
let msg = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &msg)?;
|
||||
imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully sent");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Edit => {
|
||||
let content = input::open_editor_with_draft()?;
|
||||
msg = Msg::from(content);
|
||||
}
|
||||
input::PostEditChoice::LocalDraft => break,
|
||||
input::PostEditChoice::RemoteDraft => {
|
||||
debug!("saving to draft…");
|
||||
imap_conn.append_msg("Drafts", &msg.to_vec()?, vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully saved to Drafts");
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Discard => {
|
||||
input::remove_draft()?;
|
||||
break;
|
||||
}
|
||||
},
|
||||
Err(err) => error!("{}", err),
|
||||
let mut cc = Vec::new();
|
||||
let mut bcc = Vec::new();
|
||||
let mut subject = Cow::default();
|
||||
let mut body = Cow::default();
|
||||
|
||||
for (key, val) in url.query_pairs() {
|
||||
match key.as_bytes() {
|
||||
b"cc" => {
|
||||
cc.push(val.into());
|
||||
}
|
||||
b"bcc" => {
|
||||
bcc.push(val.into());
|
||||
}
|
||||
b"subject" => {
|
||||
subject = val;
|
||||
}
|
||||
b"body" => {
|
||||
body = val;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
let headers = Headers {
|
||||
from: vec![ctx.config.address(&ctx.account)],
|
||||
to: vec![url.path().to_string()],
|
||||
encoding: ContentTransferEncoding::Base64,
|
||||
bcc: Some(bcc),
|
||||
cc: Some(cc),
|
||||
signature: ctx.config.signature(&ctx.account),
|
||||
subject: Some(subject.into()),
|
||||
..Headers::default()
|
||||
};
|
||||
|
||||
let mut msg = Msg::new_with_headers(&ctx, headers);
|
||||
msg.body = Body::new_with_text(body);
|
||||
msg_interaction(&ctx, &mut msg, &mut imap_conn)?;
|
||||
|
||||
imap_conn.logout();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn msg_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("forward command matched");
|
||||
|
||||
// fetch the msg
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
debug!("uid: {}", uid);
|
||||
|
||||
// prepare to forward it
|
||||
msg.change_to_forwarding(&ctx);
|
||||
|
||||
let attachments: Vec<&str> = matches
|
||||
.values_of("attachments")
|
||||
.unwrap_or_default()
|
||||
.collect();
|
||||
|
||||
attachments.iter().for_each(|path| msg.add_attachment(path));
|
||||
|
||||
debug!("found {} attachments", attachments.len());
|
||||
trace!("attachments: {:?}", attachments);
|
||||
|
||||
// apply changes
|
||||
msg_interaction(&ctx, &mut msg, &mut imap_conn)?;
|
||||
|
||||
imap_conn.logout();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn msg_matches_copy(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("copy command matched");
|
||||
|
||||
// fetch the message to be copyied
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", &uid);
|
||||
let target = matches.value_of("target").unwrap();
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
debug!("uid: {}", &uid);
|
||||
debug!("target: {}", &target);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = Msg::from(imap_conn.read_msg(&ctx.mbox, &uid)?);
|
||||
let mut flags = msg.flags.deref().to_vec();
|
||||
flags.push(Flag::Seen);
|
||||
imap_conn.append_msg(target, &msg.raw, flags)?;
|
||||
// the message, which will be in the new mailbox doesn't need to be seen
|
||||
msg.flags.insert(Flag::Seen);
|
||||
|
||||
imap_conn.append_msg(target, &mut msg)?;
|
||||
|
||||
debug!("message {} successfully copied to folder `{}`", uid, target);
|
||||
|
||||
ctx.output.print(format!(
|
||||
"Message {} successfully copied to folder `{}`",
|
||||
uid, target
|
||||
|
@ -518,24 +574,30 @@ fn msg_matches_copy(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
fn msg_matches_move(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("move command matched");
|
||||
|
||||
// fetch the msg which should be moved
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", &uid);
|
||||
let target = matches.value_of("target").unwrap();
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
debug!("uid: {}", &uid);
|
||||
debug!("target: {}", &target);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = Msg::from(imap_conn.read_msg(&ctx.mbox, &uid)?);
|
||||
let mut flags = msg.flags.to_vec();
|
||||
flags.push(Flag::Seen);
|
||||
imap_conn.append_msg(target, &msg.raw, flags)?;
|
||||
imap_conn.add_flags(&ctx.mbox, uid, "\\Seen \\Deleted")?;
|
||||
// create the msg in the target-msgbox
|
||||
msg.flags.insert(Flag::Seen);
|
||||
imap_conn.append_msg(target, &mut msg)?;
|
||||
|
||||
debug!("message {} successfully moved to folder `{}`", uid, target);
|
||||
ctx.output.print(format!(
|
||||
"Message {} successfully moved to folder `{}`",
|
||||
uid, target
|
||||
));
|
||||
|
||||
// delete the msg in the old mailbox
|
||||
let flags = vec![Flag::Seen, Flag::Deleted];
|
||||
imap_conn.add_flags(&ctx.mbox, uid, Flags::from(flags))?;
|
||||
imap_conn.expunge(&ctx.mbox)?;
|
||||
|
||||
imap_conn.logout();
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -543,16 +605,18 @@ fn msg_matches_move(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
fn msg_matches_delete(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("delete command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", &uid);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
imap_conn.add_flags(&ctx.mbox, uid, "\\Seen \\Deleted")?;
|
||||
|
||||
// remove the message according to its UID
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
let flags = vec![Flag::Seen, Flag::Deleted];
|
||||
imap_conn.add_flags(&ctx.mbox, uid, Flags::from(flags))?;
|
||||
imap_conn.expunge(&ctx.mbox)?;
|
||||
|
||||
debug!("message {} successfully deleted", uid);
|
||||
ctx.output
|
||||
.print(format!("Message {} successfully deleted", uid));
|
||||
|
||||
imap_conn.expunge(&ctx.mbox)?;
|
||||
imap_conn.logout();
|
||||
Ok(true)
|
||||
}
|
||||
|
@ -574,15 +638,23 @@ fn msg_matches_send(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
.lines()
|
||||
.filter_map(|ln| ln.ok())
|
||||
.map(|ln| ln.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
let msg = Msg::from(msg.to_string());
|
||||
let msg = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &msg)?;
|
||||
imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?;
|
||||
|
||||
let mut msg = Msg::try_from(msg.as_str())?;
|
||||
|
||||
// send the message/msg
|
||||
let sendable = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &sendable)?;
|
||||
debug!("message sent!");
|
||||
|
||||
// add the message/msg to the Sent-Mailbox of the user
|
||||
msg.flags.insert(Flag::Seen);
|
||||
imap_conn.append_msg("Sent", &mut msg)?;
|
||||
|
||||
imap_conn.logout();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
|
@ -590,44 +662,235 @@ fn msg_matches_save(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
|||
debug!("save command matched");
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = matches.value_of("message").unwrap();
|
||||
let msg = Msg::from(msg.to_string());
|
||||
imap_conn.append_msg(&ctx.mbox, &msg.to_vec()?, vec![Flag::Seen])?;
|
||||
let msg: &str = matches.value_of("message").unwrap();
|
||||
|
||||
let mut msg = Msg::try_from(msg)?;
|
||||
|
||||
msg.flags.insert(Flag::Seen);
|
||||
imap_conn.append_msg(&ctx.mbox, &mut msg)?;
|
||||
|
||||
imap_conn.logout();
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn msg_matches_mailto(ctx: &Ctx, url: &Url) -> Result<()> {
|
||||
debug!("mailto command matched");
|
||||
pub fn msg_matches_tpl(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
match matches.subcommand() {
|
||||
("new", Some(matches)) => tpl_matches_new(ctx, matches),
|
||||
("reply", Some(matches)) => tpl_matches_reply(ctx, matches),
|
||||
("forward", Some(matches)) => tpl_matches_forward(ctx, matches),
|
||||
|
||||
// TODO: find a way to show the help message for template subcommand
|
||||
_ => Err("Subcommand not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
// == Helper functions ==
|
||||
// -- Template Subcommands --
|
||||
// These functions are more used for the "template" subcommand
|
||||
fn override_msg_with_args(msg: &mut Msg, matches: &clap::ArgMatches) {
|
||||
// -- Collecting credentials --
|
||||
let from: Vec<String> = match matches.values_of("from") {
|
||||
Some(from) => from.map(|arg| arg.to_string()).collect(),
|
||||
None => msg.headers.from.clone(),
|
||||
};
|
||||
|
||||
let to: Vec<String> = match matches.values_of("to") {
|
||||
Some(to) => to.map(|arg| arg.to_string()).collect(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
let subject = matches
|
||||
.value_of("subject")
|
||||
.and_then(|subject| Some(subject.to_string()));
|
||||
|
||||
let cc: Option<Vec<String>> = matches
|
||||
.values_of("cc")
|
||||
.and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect()));
|
||||
|
||||
let bcc: Option<Vec<String>> = matches
|
||||
.values_of("bcc")
|
||||
.and_then(|bcc| Some(bcc.map(|arg| arg.to_string()).collect()));
|
||||
|
||||
let signature = matches
|
||||
.value_of("signature")
|
||||
.and_then(|signature| Some(signature.to_string()))
|
||||
.or(msg.headers.signature.clone());
|
||||
|
||||
let custom_headers: Option<HashMap<String, Vec<String>>> = {
|
||||
if let Some(matched_headers) = matches.values_of("header") {
|
||||
let mut custom_headers: HashMap<String, Vec<String>> = HashMap::new();
|
||||
|
||||
// collect the custom headers
|
||||
for header in matched_headers {
|
||||
let mut header = header.split(":");
|
||||
let key = header.next().unwrap_or_default();
|
||||
let val = header.next().unwrap_or_default().trim_start();
|
||||
|
||||
debug!("overriden header: {}={}", key, val);
|
||||
|
||||
custom_headers.insert(key.to_string(), vec![val.to_string()]);
|
||||
}
|
||||
|
||||
Some(custom_headers)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
let body = {
|
||||
if atty::isnt(Stream::Stdin) {
|
||||
let body = io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(|line| line.ok())
|
||||
.map(|line| line.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
debug!("overriden body from stdin: {:?}", body);
|
||||
body
|
||||
} else if let Some(body) = matches.value_of("body") {
|
||||
debug!("overriden body: {:?}", body);
|
||||
body.to_string()
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
};
|
||||
|
||||
let body = Body::new_with_text(body);
|
||||
|
||||
// -- Creating and printing --
|
||||
let headers = Headers {
|
||||
from,
|
||||
subject,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
signature,
|
||||
custom_headers,
|
||||
..msg.headers.clone()
|
||||
};
|
||||
|
||||
msg.headers = headers;
|
||||
msg.body = body;
|
||||
}
|
||||
|
||||
fn tpl_matches_new(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("new command matched");
|
||||
|
||||
let mut msg = Msg::new(&ctx);
|
||||
|
||||
override_msg_with_args(&mut msg, &matches);
|
||||
|
||||
trace!("Message: {:?}", msg);
|
||||
ctx.output.print(MsgSerialized::try_from(&msg)?);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn tpl_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("reply command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", uid);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let tpl = Tpl::mailto(&ctx, &url);
|
||||
let content = input::open_editor_with_tpl(tpl.to_string().as_bytes())?;
|
||||
let mut msg = Msg::from(content);
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
|
||||
msg.change_to_reply(&ctx, matches.is_present("reply-all"))?;
|
||||
|
||||
override_msg_with_args(&mut msg, &matches);
|
||||
trace!("Message: {:?}", msg);
|
||||
ctx.output.print(MsgSerialized::try_from(&msg)?);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn tpl_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("forward command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", uid);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let mut msg = imap_conn.get_msg(&ctx.mbox, &uid)?;
|
||||
msg.change_to_forwarding(&ctx);
|
||||
|
||||
override_msg_with_args(&mut msg, &matches);
|
||||
|
||||
trace!("Message: {:?}", msg);
|
||||
ctx.output.print(MsgSerialized::try_from(&msg)?);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// This function opens the prompt to do some actions to the msg like sending, editing it again and
|
||||
/// so on.
|
||||
fn msg_interaction(ctx: &Ctx, msg: &mut Msg, imap_conn: &mut ImapConnector) -> Result<bool> {
|
||||
// let the user change the body a little bit first, before opening the prompt
|
||||
msg.edit_body()?;
|
||||
|
||||
loop {
|
||||
match input::post_edit_choice() {
|
||||
Ok(choice) => match choice {
|
||||
input::PostEditChoice::Send => {
|
||||
debug!("sending message…");
|
||||
let msg = msg.to_sendable_msg()?;
|
||||
smtp::send(&ctx.account, &msg)?;
|
||||
imap_conn.append_msg("Sent", &msg.formatted(), vec![Flag::Seen])?;
|
||||
|
||||
// prepare the msg to be send
|
||||
let sendable = match msg.to_sendable_msg() {
|
||||
Ok(sendable) => sendable,
|
||||
// In general if an error occured, then this is normally
|
||||
// due to a missing value of a header. So let's give the
|
||||
// user another try and give him/her the chance to fix
|
||||
// that :)
|
||||
Err(err) => {
|
||||
println!("{}", err);
|
||||
println!("Please reedit your msg to make it to a sendable message!");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
smtp::send(&ctx.account, &sendable)?;
|
||||
|
||||
// TODO: Gmail sent mailboxes are called `[Gmail]/Sent`
|
||||
// which creates a conflict, fix this!
|
||||
|
||||
// let the server know, that the user sent a msg
|
||||
msg.flags.insert(Flag::Seen);
|
||||
imap_conn.append_msg("Sent", msg)?;
|
||||
|
||||
// remove the draft, since we sent it
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully sent");
|
||||
break;
|
||||
}
|
||||
// edit the body of the msg
|
||||
input::PostEditChoice::Edit => {
|
||||
let content = input::open_editor_with_draft()?;
|
||||
msg = Msg::from(content);
|
||||
// Did something goes wrong when the user changed the
|
||||
// content?
|
||||
if let Err(err) = msg.edit_body() {
|
||||
println!("[ERROR] {}", err);
|
||||
println!(concat!(
|
||||
"Please try to fix the problem by editing",
|
||||
"the msg again."
|
||||
));
|
||||
}
|
||||
}
|
||||
input::PostEditChoice::LocalDraft => break,
|
||||
input::PostEditChoice::RemoteDraft => {
|
||||
debug!("saving to draft…");
|
||||
imap_conn.append_msg("Drafts", &msg.to_vec()?, vec![Flag::Seen])?;
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully saved to Drafts");
|
||||
|
||||
msg.flags.insert(Flag::Seen);
|
||||
|
||||
match imap_conn.append_msg("Drafts", msg) {
|
||||
Ok(_) => {
|
||||
input::remove_draft()?;
|
||||
ctx.output.print("Message successfully saved to Drafts");
|
||||
}
|
||||
Err(err) => {
|
||||
ctx.output.print("Couldn't save it to the server...");
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
input::PostEditChoice::Discard => {
|
||||
|
@ -638,6 +901,6 @@ pub fn msg_matches_mailto(ctx: &Ctx, url: &Url) -> Result<()> {
|
|||
Err(err) => error!("{}", err),
|
||||
}
|
||||
}
|
||||
imap_conn.logout();
|
||||
Ok(())
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
|
641
src/msg/headers.rs
Normal file
641
src/msg/headers.rs
Normal file
|
@ -0,0 +1,641 @@
|
|||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
||||
use log::{debug, warn};
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use rfc2047_decoder;
|
||||
|
||||
use error_chain::error_chain;
|
||||
|
||||
use lettre::message::header::ContentTransferEncoding;
|
||||
|
||||
error_chain! {
|
||||
errors {
|
||||
Convertion(field: &'static str) {
|
||||
display("Couldn't get the data from the '{}:' field.", field),
|
||||
}
|
||||
}
|
||||
|
||||
foreign_links {
|
||||
StringFromUtf8(std::string::FromUtf8Error);
|
||||
Rfc2047Decoder(rfc2047_decoder::Error);
|
||||
}
|
||||
}
|
||||
|
||||
// == Structs ==
|
||||
/// This struct is a wrapper for the [Envelope struct] of the [imap_proto]
|
||||
/// crate. It's should mainly help to interact with the mails by using more
|
||||
/// common data types like `Vec` or `String` since a `[u8]` array is a little
|
||||
/// bit limited to use.
|
||||
///
|
||||
/// # Usage
|
||||
/// The general idea is, that you create a new instance like that:
|
||||
///
|
||||
/// ```
|
||||
/// use himalaya::msg::headers::Headers;
|
||||
/// # fn main() {
|
||||
///
|
||||
/// let headers = Headers {
|
||||
/// from: vec![String::from("From <address@example.com>")],
|
||||
/// to: vec![String::from("To <address@to.com>")],
|
||||
/// ..Headers::default()
|
||||
/// };
|
||||
///
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// We don't have a build-pattern here, because this is easy as well and we
|
||||
/// don't need a dozens of functions, just to set some values.
|
||||
///
|
||||
/// [Envelope struct]: https://docs.rs/imap-proto/0.14.3/imap_proto/types/struct.Headers.html
|
||||
/// [imap_proto]: https://docs.rs/imap-proto/0.14.3/imap_proto/index.html
|
||||
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Headers {
|
||||
// -- Must-Fields --
|
||||
// These fields are the mininum needed to send a msg.
|
||||
pub from: Vec<String>,
|
||||
pub to: Vec<String>,
|
||||
pub encoding: ContentTransferEncoding,
|
||||
|
||||
// -- Optional fields --
|
||||
pub bcc: Option<Vec<String>>,
|
||||
pub cc: Option<Vec<String>>,
|
||||
pub custom_headers: Option<HashMap<String, Vec<String>>>,
|
||||
pub in_reply_to: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
pub reply_to: Option<Vec<String>>,
|
||||
pub sender: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub subject: Option<String>,
|
||||
}
|
||||
|
||||
impl Headers {
|
||||
/// This method works similiar to the [`Display Trait`] but it will only
|
||||
/// convert the header into a string **without** the signature.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// <details>
|
||||
///
|
||||
/// ```
|
||||
/// # use himalaya::msg::headers::Headers;
|
||||
/// # use std::collections::HashMap;
|
||||
/// # use lettre::message::header::ContentTransferEncoding;
|
||||
/// # fn main() {
|
||||
/// // our headers
|
||||
/// let headers = Headers {
|
||||
/// from: vec!["TornaxO7 <tornax07@gmail.com>".to_string()],
|
||||
/// to: vec!["Soywod <clement.douin@posteo.net>".to_string()],
|
||||
/// encoding: ContentTransferEncoding::Base64,
|
||||
/// bcc: Some(vec!["ThirdOne <some@msg.net>".to_string()]),
|
||||
/// cc: Some(vec!["CcAccount <cc@ccmail.net>".to_string()]),
|
||||
/// custom_headers: None,
|
||||
/// in_reply_to: Some("1234@local.machine.example".to_string()),
|
||||
/// message_id: Some("123456789".to_string()),
|
||||
/// reply_to: Some(vec!["reply@msg.net".to_string()]),
|
||||
/// sender: Some("himalaya@secretary.net".to_string()),
|
||||
/// signature: Some("Signature of Headers".to_string()),
|
||||
/// subject: Some("Himalaya is cool".to_string()),
|
||||
/// };
|
||||
///
|
||||
/// // get the header
|
||||
/// let headers_string = headers.get_header_as_string();
|
||||
///
|
||||
/// // how the header part should look like
|
||||
/// let expected_output = concat![
|
||||
/// "From: TornaxO7 <tornax07@gmail.com>\n",
|
||||
/// "To: Soywod <clement.douin@posteo.net>\n",
|
||||
/// "In-Reply-To: 1234@local.machine.example\n",
|
||||
/// "Sender: himalaya@secretary.net\n",
|
||||
/// "Message-ID: 123456789\n",
|
||||
/// "Reply-To: reply@msg.net\n",
|
||||
/// "Cc: CcAccount <cc@ccmail.net>\n",
|
||||
/// "Bcc: ThirdOne <some@msg.net>\n",
|
||||
/// "Subject: Himalaya is cool\n",
|
||||
/// ];
|
||||
///
|
||||
/// assert_eq!(headers_string, expected_output,
|
||||
/// "{}, {}",
|
||||
/// headers_string, expected_output);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// </details>
|
||||
///
|
||||
/// [`Display Trait`]: https://doc.rust-lang.org/std/fmt/trait.Display.html
|
||||
pub fn get_header_as_string(&self) -> String {
|
||||
let mut header = String::new();
|
||||
|
||||
// -- Must-Have-Fields --
|
||||
// the "From: " header
|
||||
header.push_str(&merge_addresses_to_one_line("From", &self.from, ','));
|
||||
|
||||
// the "To: " header
|
||||
header.push_str(&merge_addresses_to_one_line("To", &self.to, ','));
|
||||
|
||||
// -- Optional fields --
|
||||
// Here we are adding only the header parts which have a value (are not
|
||||
// None). That's why we are always checking here with "if let Some()".
|
||||
|
||||
// in reply to
|
||||
if let Some(in_reply_to) = &self.in_reply_to {
|
||||
header.push_str(&format!("In-Reply-To: {}\n", in_reply_to));
|
||||
}
|
||||
|
||||
// Sender
|
||||
if let Some(sender) = &self.sender {
|
||||
header.push_str(&format!("Sender: {}\n", sender));
|
||||
}
|
||||
|
||||
// Message-ID
|
||||
if let Some(message_id) = &self.message_id {
|
||||
header.push_str(&format!("Message-ID: {}\n", message_id));
|
||||
}
|
||||
|
||||
// reply_to
|
||||
if let Some(reply_to) = &self.reply_to {
|
||||
header.push_str(&merge_addresses_to_one_line("Reply-To", &reply_to, ','));
|
||||
}
|
||||
|
||||
// cc
|
||||
if let Some(cc) = &self.cc {
|
||||
header.push_str(&merge_addresses_to_one_line("Cc", &cc, ','));
|
||||
}
|
||||
|
||||
// bcc
|
||||
if let Some(bcc) = &self.bcc {
|
||||
header.push_str(&merge_addresses_to_one_line("Bcc", &bcc, ','));
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if let Some(custom_headers) = &self.custom_headers {
|
||||
for (key, value) in custom_headers.iter() {
|
||||
header.push_str(&merge_addresses_to_one_line(key, &value, ','));
|
||||
}
|
||||
}
|
||||
|
||||
// Subject
|
||||
if let Some(subject) = &self.subject {
|
||||
header.push_str(&format!("Subject: {}\n", subject));
|
||||
}
|
||||
|
||||
header
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Headers with the following values:
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use himalaya::msg::headers::Headers;
|
||||
/// # use lettre::message::header::ContentTransferEncoding;
|
||||
/// Headers {
|
||||
/// from: Vec::new(),
|
||||
/// to: Vec::new(),
|
||||
/// encoding: ContentTransferEncoding::Base64,
|
||||
/// bcc: None,
|
||||
/// cc: None,
|
||||
/// custom_headers: None,
|
||||
/// in_reply_to: None,
|
||||
/// message_id: None,
|
||||
/// reply_to: None,
|
||||
/// sender: None,
|
||||
/// signature: None,
|
||||
/// subject: None,
|
||||
/// };
|
||||
/// ```
|
||||
impl Default for Headers {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
// must-fields
|
||||
from: Vec::new(),
|
||||
to: Vec::new(),
|
||||
encoding: ContentTransferEncoding::Base64,
|
||||
|
||||
// optional fields
|
||||
bcc: None,
|
||||
cc: None,
|
||||
custom_headers: None,
|
||||
in_reply_to: None,
|
||||
message_id: None,
|
||||
reply_to: None,
|
||||
sender: None,
|
||||
signature: None,
|
||||
subject: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// == From implementations ==
|
||||
impl TryFrom<Option<&imap_proto::types::Envelope<'_>>> for Headers {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(envelope: Option<&imap_proto::types::Envelope<'_>>) -> Result<Self> {
|
||||
if let Some(envelope) = envelope {
|
||||
debug!("Fetch has headers.");
|
||||
|
||||
let subject = envelope
|
||||
.subject
|
||||
.as_ref()
|
||||
.and_then(|subj| rfc2047_decoder::decode(subj).ok());
|
||||
|
||||
let from = match convert_vec_address_to_string(envelope.from.as_ref())? {
|
||||
Some(from) => from,
|
||||
None => return Err(ErrorKind::Convertion("From").into()),
|
||||
};
|
||||
|
||||
// only the first address is used, because how should multiple machines send the same
|
||||
// mail?
|
||||
let sender = convert_vec_address_to_string(envelope.sender.as_ref())?;
|
||||
let sender = match sender {
|
||||
Some(tmp_sender) => Some(
|
||||
tmp_sender
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap_or(&String::new())
|
||||
.to_string(),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let message_id = convert_cow_u8_to_string(envelope.message_id.as_ref())?;
|
||||
let reply_to = convert_vec_address_to_string(envelope.reply_to.as_ref())?;
|
||||
let to = match convert_vec_address_to_string(envelope.to.as_ref())? {
|
||||
Some(to) => to,
|
||||
None => return Err(ErrorKind::Convertion("To").into()),
|
||||
};
|
||||
let cc = convert_vec_address_to_string(envelope.cc.as_ref())?;
|
||||
let bcc = convert_vec_address_to_string(envelope.bcc.as_ref())?;
|
||||
let in_reply_to = convert_cow_u8_to_string(envelope.in_reply_to.as_ref())?;
|
||||
|
||||
Ok(Self {
|
||||
subject,
|
||||
from,
|
||||
sender,
|
||||
message_id,
|
||||
reply_to,
|
||||
to,
|
||||
cc,
|
||||
bcc,
|
||||
in_reply_to,
|
||||
custom_headers: None,
|
||||
signature: None,
|
||||
encoding: ContentTransferEncoding::Base64,
|
||||
})
|
||||
} else {
|
||||
debug!("Fetch hasn't headers.");
|
||||
Ok(Headers::default())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'from> From<&mailparse::ParsedMail<'from>> for Headers {
|
||||
fn from(parsed_mail: &mailparse::ParsedMail<'from>) -> Self {
|
||||
let mut new_headers = Headers::default();
|
||||
|
||||
let header_iter = parsed_mail.headers.iter();
|
||||
for header in header_iter {
|
||||
// get the value of the header. For example if we have this header:
|
||||
//
|
||||
// Subject: I use Arch btw
|
||||
//
|
||||
// than `value` would be like that: `let value = "I use Arch btw".to_string()`
|
||||
let value = header.get_value().replace("\r", "");
|
||||
let header_name = header.get_key().to_lowercase();
|
||||
let header_name = header_name.as_str();
|
||||
|
||||
// now go through all headers and look which values they have.
|
||||
match header_name {
|
||||
"from" => {
|
||||
new_headers.from = value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
"to" => {
|
||||
new_headers.to = value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
"bcc" => {
|
||||
new_headers.bcc = Some(
|
||||
value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
"cc" => {
|
||||
new_headers.cc = Some(
|
||||
value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
"in_reply_to" => new_headers.in_reply_to = Some(value),
|
||||
"reply_to" => {
|
||||
new_headers.reply_to = Some(
|
||||
value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
"sender" => new_headers.sender = Some(value),
|
||||
"subject" => new_headers.subject = Some(value),
|
||||
"message-id" => new_headers.message_id = Some(value),
|
||||
"content-transfer-encoding" => {
|
||||
match value.to_lowercase().as_str() {
|
||||
"8bit" => new_headers.encoding = ContentTransferEncoding::EightBit,
|
||||
"7bit" => new_headers.encoding = ContentTransferEncoding::SevenBit,
|
||||
"quoted-printable" => {
|
||||
new_headers.encoding = ContentTransferEncoding::QuotedPrintable
|
||||
}
|
||||
"base64" => new_headers.encoding = ContentTransferEncoding::Base64,
|
||||
_ => warn!("Unsupported encoding, default to QuotedPrintable"),
|
||||
};
|
||||
}
|
||||
|
||||
// it's a custom header => Add it to our
|
||||
// custom-header-hash-map
|
||||
_ => {
|
||||
let custom_header = header.get_key();
|
||||
|
||||
// If we don't have a HashMap yet => Create one! Otherwise
|
||||
// we'll keep using it, because why should we reset its
|
||||
// values again?
|
||||
if let None = new_headers.custom_headers {
|
||||
new_headers.custom_headers = Some(HashMap::new());
|
||||
}
|
||||
|
||||
let mut updated_hashmap = new_headers.custom_headers.unwrap();
|
||||
|
||||
updated_hashmap.insert(
|
||||
custom_header,
|
||||
value
|
||||
.rsplit(',')
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect(),
|
||||
);
|
||||
|
||||
new_headers.custom_headers = Some(updated_hashmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new_headers
|
||||
}
|
||||
}
|
||||
|
||||
// -- Common Traits --
|
||||
/// This trait just returns the headers but as a string. But be careful! **The
|
||||
/// signature is printed as well!!!**, so it isn't really useable to create the
|
||||
/// content of a msg! Use [get_header_as_string] instead!
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use himalaya::msg::headers::Headers;
|
||||
/// # fn main() {
|
||||
/// let headers = Headers {
|
||||
/// subject: Some(String::from("Himalaya is cool")),
|
||||
/// to: vec![String::from("Soywod <clement.douin@posteo.net>")],
|
||||
/// from: vec![String::from("TornaxO7 <tornax07@gmail.com>")],
|
||||
/// signature: Some(String::from("Signature of Headers")),
|
||||
/// ..Headers::default()
|
||||
/// };
|
||||
///
|
||||
/// // use the `fmt::Display` trait
|
||||
/// let headers_output = format!("{}", headers);
|
||||
///
|
||||
/// // How the output of the `fmt::Display` trait should look like
|
||||
/// let expected_output = concat![
|
||||
/// "From: TornaxO7 <tornax07@gmail.com>\n",
|
||||
/// "To: Soywod <clement.douin@posteo.net>\n",
|
||||
/// "Subject: Himalaya is cool\n",
|
||||
/// "\n\n\n",
|
||||
/// "Signature of Headers",
|
||||
/// ];
|
||||
///
|
||||
/// assert_eq!(headers_output, expected_output,
|
||||
/// "{:#?}, {:#?}",
|
||||
/// headers_output, expected_output);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [get_header_as_string]: struct.Headers.html#method.get_header_as_string
|
||||
impl fmt::Display for Headers {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut header = self.get_header_as_string();
|
||||
|
||||
// now add some space between the header and the signature
|
||||
header.push_str("\n\n\n");
|
||||
|
||||
// and add the signature in the end
|
||||
header.push_str(&self.signature.clone().unwrap_or(String::new()));
|
||||
|
||||
write!(formatter, "{}", header)
|
||||
}
|
||||
}
|
||||
|
||||
// -- Helper functions --
|
||||
/// This function is mainly used for the `imap_proto::types::Address` struct to
|
||||
/// convert one field into a String. Take a look into the
|
||||
/// `test_convert_cow_u8_to_string` test function to see it in action.
|
||||
fn convert_cow_u8_to_string<'val>(value: Option<&Cow<'val, [u8]>>) -> Result<Option<String>> {
|
||||
if let Some(value) = value {
|
||||
// convert the `[u8]` list into a vector and try to get a string out of
|
||||
// it. If everything worked fine, return the content of the list
|
||||
Ok(Some(rfc2047_decoder::decode(&value.to_vec())?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is mainly used for the `imap_proto::types::Address` struct as
|
||||
/// well to change the Address into an address-string like this:
|
||||
/// `TornaxO7 <tornax07@gmail.com>`.
|
||||
///
|
||||
/// If you provide two addresses as the function argument, then this functions
|
||||
/// returns their "parsed" address in the same order. Take a look into the
|
||||
/// `test_convert_vec_address_to_string` for an example.
|
||||
fn convert_vec_address_to_string<'val>(
|
||||
addresses: Option<&Vec<imap_proto::types::Address<'val>>>,
|
||||
) -> Result<Option<Vec<String>>> {
|
||||
if let Some(addresses) = addresses {
|
||||
let mut parsed_addresses: Vec<String> = Vec::new();
|
||||
|
||||
for address in addresses.iter() {
|
||||
// This variable will hold the parsed version of the Address-struct,
|
||||
// like this:
|
||||
//
|
||||
// "Name <msg@host>"
|
||||
let mut parsed_address = String::new();
|
||||
|
||||
// -- Get the fields --
|
||||
// add the name field (if it exists) like this:
|
||||
// "Name"
|
||||
if let Some(name) = convert_cow_u8_to_string(address.name.as_ref())? {
|
||||
parsed_address.push_str(&name);
|
||||
}
|
||||
|
||||
// add the mailaddress
|
||||
if let Some(mailbox) = convert_cow_u8_to_string(address.mailbox.as_ref())? {
|
||||
if let Some(host) = convert_cow_u8_to_string(address.host.as_ref())? {
|
||||
let mail_address = format!("{}@{}", mailbox, host);
|
||||
|
||||
// some mail clients add a trailing space, after the address
|
||||
let trimmed = mail_address.trim();
|
||||
|
||||
if parsed_address.is_empty() {
|
||||
// parsed_address = "msg@host"
|
||||
parsed_address.push_str(&trimmed);
|
||||
} else {
|
||||
// parsed_address = "Name <msg@host>"
|
||||
parsed_address.push_str(&format!(" <{}>", trimmed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed_addresses.push(parsed_address);
|
||||
}
|
||||
|
||||
Ok(Some(parsed_addresses))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is used, in order to merge multiple msg accounts into one
|
||||
/// line. Take a look into the `test_merge_addresses_to_one_line` test-function
|
||||
/// to see an example how to use it.
|
||||
fn merge_addresses_to_one_line(header: &str, addresses: &Vec<String>, separator: char) -> String {
|
||||
let mut output = header.to_string();
|
||||
let mut address_iter = addresses.iter();
|
||||
|
||||
// Convert the header to this (for example): `Cc: `
|
||||
output.push_str(": ");
|
||||
|
||||
// the first emsg doesn't need a comma before, so we should append the msg
|
||||
// to it
|
||||
output.push_str(address_iter.next().unwrap_or(&String::new()));
|
||||
|
||||
// add the rest of the emails. It should look like this after the for_each:
|
||||
//
|
||||
// Addr1, Addr2, Addr2, ...
|
||||
address_iter.for_each(|address| output.push_str(&format!("{}{}", separator, address)));
|
||||
|
||||
// end the header-line by using a newline character
|
||||
output.push('\n');
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
// ==========
|
||||
// Tests
|
||||
// ==========
|
||||
/// This tests only test the helper functions.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_merge_addresses_to_one_line() {
|
||||
use super::merge_addresses_to_one_line;
|
||||
// In this function, we want to create the following Cc header:
|
||||
//
|
||||
// Cc: TornaxO7 <tornax07@gmail.com>, Soywod <clement.douin@posteo.net>
|
||||
//
|
||||
// by a vector of email-addresses.
|
||||
|
||||
// our msg addresses for the "Cc" header
|
||||
let mail_addresses = vec![
|
||||
"TornaxO7 <tornax07@gmail.com>".to_string(),
|
||||
"Soywod <clement.douin@posteo.net>".to_string(),
|
||||
];
|
||||
|
||||
let cc_header = merge_addresses_to_one_line("Cc", &mail_addresses, ',');
|
||||
|
||||
let expected_output = concat![
|
||||
"Cc: TornaxO7 <tornax07@gmail.com>",
|
||||
",",
|
||||
"Soywod <clement.douin@posteo.net>\n",
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
cc_header, expected_output,
|
||||
"{:#?}, {:#?}",
|
||||
cc_header, expected_output
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_cow_u8_to_string() {
|
||||
use super::convert_cow_u8_to_string;
|
||||
use std::borrow::Cow;
|
||||
|
||||
let output1 = convert_cow_u8_to_string(None);
|
||||
let output2 = convert_cow_u8_to_string(Some(&Cow::Owned(b"Test".to_vec())));
|
||||
|
||||
// test output1
|
||||
if let Ok(output1) = output1 {
|
||||
assert!(output1.is_none());
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
|
||||
// test output2
|
||||
if let Ok(output2) = output2 {
|
||||
if let Some(string) = output2 {
|
||||
assert_eq!(String::from("Test"), string);
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convert_vec_address_to_string() {
|
||||
use super::convert_vec_address_to_string;
|
||||
use imap_proto::types::Address;
|
||||
use std::borrow::Cow;
|
||||
|
||||
let addresses = vec![
|
||||
Address {
|
||||
name: Some(Cow::Owned(b"Name1".to_vec())),
|
||||
adl: None,
|
||||
mailbox: Some(Cow::Owned(b"Mailbox1".to_vec())),
|
||||
host: Some(Cow::Owned(b"Host1".to_vec())),
|
||||
},
|
||||
Address {
|
||||
name: None,
|
||||
adl: None,
|
||||
mailbox: Some(Cow::Owned(b"Mailbox2".to_vec())),
|
||||
host: Some(Cow::Owned(b"Host2".to_vec())),
|
||||
},
|
||||
];
|
||||
|
||||
// the expected addresses
|
||||
let expected_output = vec![
|
||||
String::from("Name1 <Mailbox1@Host1>"),
|
||||
String::from("Mailbox2@Host2"),
|
||||
];
|
||||
|
||||
if let Ok(converted) = convert_vec_address_to_string(Some(&addresses)) {
|
||||
assert_eq!(converted, Some(expected_output));
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,37 @@
|
|||
//! This module holds everything which is related to a **Msg**/**Mail**. Here are
|
||||
//! structs which **represent the data** in Msgs/Mails.
|
||||
|
||||
/// Includes the following subcommands:
|
||||
/// - `list`
|
||||
/// - `search`
|
||||
/// - `write`
|
||||
/// - `send`
|
||||
/// - `save`
|
||||
/// - `read`
|
||||
/// - `attachments`
|
||||
/// - `reply`
|
||||
/// - `forward`
|
||||
/// - `copy`
|
||||
/// - `move`
|
||||
/// - `delete`
|
||||
/// - `template`
|
||||
///
|
||||
/// Execute `himalaya help <cmd>` where `<cmd>` is one entry of this list above
|
||||
/// to get more information about them.
|
||||
pub mod cli;
|
||||
|
||||
/// Here are the two **main structs** of this module: `Msg` and `Msgs` which
|
||||
/// represent a *Mail* or *multiple Mails* in this crate.
|
||||
pub mod model;
|
||||
pub mod tpl;
|
||||
|
||||
/// This module is used in the `Msg` struct, which should represent an
|
||||
/// attachment of a msg.
|
||||
pub mod attachment;
|
||||
|
||||
/// This module is used in the `Msg` struct, which should represent the headers
|
||||
/// fields like `To:` and `From:`.
|
||||
pub mod headers;
|
||||
|
||||
/// This module is used in the `Msg` struct, which should represent the body of
|
||||
/// a msg; The part where you're writing some text like `Dear Mr. LMAO`.
|
||||
pub mod body;
|
||||
|
|
2044
src/msg/model.rs
2044
src/msg/model.rs
File diff suppressed because it is too large
Load diff
|
@ -1,226 +0,0 @@
|
|||
use atty::Stream;
|
||||
use clap;
|
||||
use error_chain::error_chain;
|
||||
use log::{debug, trace};
|
||||
use mailparse;
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
use crate::{ctx::Ctx, imap::model::ImapConnector, msg::tpl::model::Tpl};
|
||||
|
||||
error_chain! {
|
||||
links {
|
||||
Imap(crate::imap::model::Error, crate::imap::model::ErrorKind);
|
||||
}
|
||||
foreign_links {
|
||||
Clap(clap::Error);
|
||||
MailParse(mailparse::MailParseError);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uid_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("uid")
|
||||
.help("Specifies the targetted message")
|
||||
.value_name("UID")
|
||||
.required(true)
|
||||
}
|
||||
|
||||
fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("reply-all")
|
||||
.help("Includes all recipients")
|
||||
.short("A")
|
||||
.long("all")
|
||||
}
|
||||
|
||||
pub fn tpl_subcommand<'a>() -> clap::App<'a, 'a> {
|
||||
clap::SubCommand::with_name("template")
|
||||
.aliases(&["tpl"])
|
||||
.about("Generates a message template")
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("new")
|
||||
.aliases(&["n"])
|
||||
.about("Generates a new message template")
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("reply")
|
||||
.aliases(&["rep", "r"])
|
||||
.about("Generates a reply message template")
|
||||
.arg(uid_arg())
|
||||
.arg(reply_all_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
.subcommand(
|
||||
clap::SubCommand::with_name("forward")
|
||||
.aliases(&["fwd", "fw", "f"])
|
||||
.about("Generates a forward message template")
|
||||
.arg(uid_arg())
|
||||
.args(&tpl_args()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn tpl_args<'a>() -> Vec<clap::Arg<'a, 'a>> {
|
||||
vec![
|
||||
clap::Arg::with_name("subject")
|
||||
.help("Overrides the Subject header")
|
||||
.short("s")
|
||||
.long("subject")
|
||||
.value_name("STRING"),
|
||||
clap::Arg::with_name("from")
|
||||
.help("Overrides the From header")
|
||||
.short("f")
|
||||
.long("from")
|
||||
.value_name("ADDR"),
|
||||
clap::Arg::with_name("to")
|
||||
.help("Overrides the To header")
|
||||
.short("t")
|
||||
.long("to")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("cc")
|
||||
.help("Overrides the Cc header")
|
||||
.short("c")
|
||||
.long("cc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("bcc")
|
||||
.help("Overrides the Bcc header")
|
||||
.short("b")
|
||||
.long("bcc")
|
||||
.value_name("ADDR")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("header")
|
||||
.help("Overrides a specific header")
|
||||
.short("h")
|
||||
.long("header")
|
||||
.value_name("KEY: VAL")
|
||||
.multiple(true),
|
||||
clap::Arg::with_name("body")
|
||||
.help("Overrides the body")
|
||||
.short("B")
|
||||
.long("body")
|
||||
.value_name("STRING"),
|
||||
clap::Arg::with_name("signature")
|
||||
.help("Overrides the signature")
|
||||
.short("S")
|
||||
.long("signature")
|
||||
.value_name("STRING"),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn tpl_matches(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
match matches.subcommand() {
|
||||
("new", Some(matches)) => tpl_matches_new(ctx, matches),
|
||||
("reply", Some(matches)) => tpl_matches_reply(ctx, matches),
|
||||
("forward", Some(matches)) => tpl_matches_forward(ctx, matches),
|
||||
|
||||
// TODO: find a way to show the help message for template subcommand
|
||||
_ => Err("Subcommand not found".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn override_tpl_with_args(ctx: &Ctx, tpl: &mut Tpl, matches: &clap::ArgMatches) {
|
||||
if let Some(from) = matches.value_of("from") {
|
||||
debug!("overriden from: {:?}", from);
|
||||
tpl.header("From", from);
|
||||
};
|
||||
|
||||
if let Some(subject) = matches.value_of("subject") {
|
||||
debug!("overriden subject: {:?}", subject);
|
||||
tpl.header("Subject", subject);
|
||||
};
|
||||
|
||||
let addrs = matches.values_of("to").unwrap_or_default();
|
||||
if addrs.len() > 0 {
|
||||
debug!("overriden to: {:?}", addrs);
|
||||
tpl.header("To", addrs.collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
|
||||
let addrs = matches.values_of("cc").unwrap_or_default();
|
||||
if addrs.len() > 0 {
|
||||
debug!("overriden cc: {:?}", addrs);
|
||||
tpl.header("Cc", addrs.collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
|
||||
let addrs = matches.values_of("bcc").unwrap_or_default();
|
||||
if addrs.len() > 0 {
|
||||
debug!("overriden bcc: {:?}", addrs);
|
||||
tpl.header("Bcc", addrs.collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
|
||||
for header in matches.values_of("header").unwrap_or_default() {
|
||||
let mut header = header.split(":");
|
||||
let key = header.next().unwrap_or_default();
|
||||
let val = header.next().unwrap_or_default().trim_start();
|
||||
debug!("overriden header: {}={}", key, val);
|
||||
tpl.header(key, val);
|
||||
}
|
||||
|
||||
if atty::isnt(Stream::Stdin) && ctx.output.is_plain() {
|
||||
let body = io::stdin()
|
||||
.lock()
|
||||
.lines()
|
||||
.filter_map(|ln| ln.ok())
|
||||
.map(|ln| ln.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
debug!("overriden body from stdin: {:?}", body);
|
||||
tpl.body(body);
|
||||
} else if let Some(body) = matches.value_of("body") {
|
||||
debug!("overriden body: {:?}", body);
|
||||
tpl.body(body);
|
||||
};
|
||||
|
||||
if let Some(signature) = matches.value_of("signature") {
|
||||
debug!("overriden signature: {:?}", signature);
|
||||
tpl.signature(signature);
|
||||
};
|
||||
}
|
||||
|
||||
fn tpl_matches_new(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("new command matched");
|
||||
|
||||
let mut tpl = Tpl::new(&ctx);
|
||||
override_tpl_with_args(&ctx, &mut tpl, &matches);
|
||||
trace!("tpl: {:?}", tpl);
|
||||
ctx.output.print(tpl);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn tpl_matches_reply(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("reply command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", uid);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = &imap_conn.read_msg(&ctx.mbox, &uid)?;
|
||||
let msg = mailparse::parse_mail(&msg)?;
|
||||
let mut tpl = if matches.is_present("reply-all") {
|
||||
Tpl::reply_all(&ctx, &msg)
|
||||
} else {
|
||||
Tpl::reply(&ctx, &msg)
|
||||
};
|
||||
override_tpl_with_args(&ctx, &mut tpl, &matches);
|
||||
trace!("tpl: {:?}", tpl);
|
||||
ctx.output.print(tpl);
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn tpl_matches_forward(ctx: &Ctx, matches: &clap::ArgMatches) -> Result<bool> {
|
||||
debug!("forward command matched");
|
||||
|
||||
let uid = matches.value_of("uid").unwrap();
|
||||
debug!("uid: {}", uid);
|
||||
|
||||
let mut imap_conn = ImapConnector::new(&ctx.account)?;
|
||||
let msg = &imap_conn.read_msg(&ctx.mbox, &uid)?;
|
||||
let msg = mailparse::parse_mail(&msg)?;
|
||||
let mut tpl = Tpl::forward(&ctx, &msg);
|
||||
override_tpl_with_args(&ctx, &mut tpl, &matches);
|
||||
trace!("tpl: {:?}", tpl);
|
||||
ctx.output.print(tpl);
|
||||
|
||||
Ok(true)
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
pub mod cli;
|
||||
pub mod model;
|
|
@ -1,533 +0,0 @@
|
|||
use error_chain::error_chain;
|
||||
use mailparse::{self, MailHeaderMap};
|
||||
use serde::Serialize;
|
||||
use std::{borrow::Cow, collections::HashMap, fmt};
|
||||
use url::Url;
|
||||
|
||||
use crate::{ctx::Ctx, msg::model::Msg};
|
||||
|
||||
error_chain! {}
|
||||
|
||||
const TPL_HEADERS: &[&str] = &["From", "To", "In-Reply-To", "Cc", "Bcc", "Subject"];
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Tpl {
|
||||
headers: HashMap<String, String>,
|
||||
body: Option<String>,
|
||||
signature: Option<String>,
|
||||
raw: String,
|
||||
}
|
||||
|
||||
impl Tpl {
|
||||
pub fn new(ctx: &Ctx) -> Self {
|
||||
let mut headers = HashMap::new();
|
||||
headers.insert("From".to_string(), ctx.config.address(ctx.account));
|
||||
headers.insert("To".to_string(), String::new());
|
||||
headers.insert("Subject".to_string(), String::new());
|
||||
|
||||
let mut tpl = Self {
|
||||
headers,
|
||||
body: None,
|
||||
signature: ctx.config.signature(ctx.account),
|
||||
raw: String::new(),
|
||||
};
|
||||
tpl.raw = tpl.to_string();
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn reply(ctx: &Ctx, msg: &mailparse::ParsedMail) -> Self {
|
||||
let parsed_headers = msg.get_headers();
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
headers.insert("From".to_string(), ctx.config.address(ctx.account));
|
||||
|
||||
let to = parsed_headers
|
||||
.get_first_value("reply-to")
|
||||
.or(parsed_headers.get_first_value("from"))
|
||||
.unwrap_or_default();
|
||||
headers.insert("To".to_string(), to);
|
||||
|
||||
if let Some(in_reply_to) = parsed_headers.get_first_value("message-id") {
|
||||
headers.insert("In-Reply-To".to_string(), in_reply_to);
|
||||
}
|
||||
|
||||
let subject = parsed_headers
|
||||
.get_first_value("subject")
|
||||
.unwrap_or_default();
|
||||
headers.insert("Subject".to_string(), format!("Re: {}", subject));
|
||||
|
||||
let mut parts = vec![];
|
||||
Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts);
|
||||
if parts.is_empty() {
|
||||
Msg::extract_text_bodies_into(&msg, "text/html", &mut parts);
|
||||
}
|
||||
|
||||
let body = parts
|
||||
.join("\r\n\r\n")
|
||||
.replace("\r", "")
|
||||
.split("\n")
|
||||
.map(|line| format!(">{}", line))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
let mut tpl = Self {
|
||||
headers,
|
||||
body: Some(body),
|
||||
signature: ctx.config.signature(&ctx.account),
|
||||
raw: String::new(),
|
||||
};
|
||||
tpl.raw = tpl.to_string();
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn reply_all(ctx: &Ctx, msg: &mailparse::ParsedMail) -> Self {
|
||||
let parsed_headers = msg.get_headers();
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
let from: lettre::message::Mailbox = ctx.config.address(ctx.account).parse().unwrap();
|
||||
headers.insert("From".to_string(), from.to_string());
|
||||
|
||||
let to = parsed_headers
|
||||
.get_all_values("to")
|
||||
.iter()
|
||||
.flat_map(|addrs| addrs.split(","))
|
||||
.fold(vec![], |mut mboxes, addr| {
|
||||
match addr.trim().parse::<lettre::message::Mailbox>() {
|
||||
Err(_) => mboxes,
|
||||
Ok(mbox) => {
|
||||
if mbox != from {
|
||||
mboxes.push(mbox.to_string());
|
||||
}
|
||||
mboxes
|
||||
}
|
||||
}
|
||||
});
|
||||
let reply_to = parsed_headers
|
||||
.get_all_values("reply-to")
|
||||
.iter()
|
||||
.flat_map(|addrs| addrs.split(","))
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
let reply_to = if reply_to.is_empty() {
|
||||
parsed_headers
|
||||
.get_all_values("from")
|
||||
.iter()
|
||||
.flat_map(|addrs| addrs.split(","))
|
||||
.map(|addr| addr.trim().to_string())
|
||||
.collect::<Vec<String>>()
|
||||
} else {
|
||||
reply_to
|
||||
};
|
||||
headers.insert("To".to_string(), [reply_to, to].concat().join(", "));
|
||||
|
||||
if let Some(in_reply_to) = parsed_headers.get_first_value("message-id") {
|
||||
headers.insert("In-Reply-To".to_string(), in_reply_to);
|
||||
}
|
||||
|
||||
let cc = parsed_headers.get_all_values("cc");
|
||||
if !cc.is_empty() {
|
||||
headers.insert("Cc".to_string(), cc.join(", "));
|
||||
}
|
||||
|
||||
let subject = parsed_headers
|
||||
.get_first_value("subject")
|
||||
.unwrap_or_default();
|
||||
headers.insert("Subject".to_string(), format!("Re: {}", subject));
|
||||
|
||||
let mut parts = vec![];
|
||||
Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts);
|
||||
if parts.is_empty() {
|
||||
Msg::extract_text_bodies_into(&msg, "text/html", &mut parts);
|
||||
}
|
||||
|
||||
let body = parts
|
||||
.join("\r\n\r\n")
|
||||
.replace("\r", "")
|
||||
.split("\n")
|
||||
.map(|line| format!(">{}", line))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n");
|
||||
|
||||
let mut tpl = Self {
|
||||
headers,
|
||||
body: Some(body),
|
||||
signature: ctx.config.signature(&ctx.account),
|
||||
raw: String::new(),
|
||||
};
|
||||
tpl.raw = tpl.to_string();
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn forward(ctx: &Ctx, msg: &mailparse::ParsedMail) -> Self {
|
||||
let parsed_headers = msg.get_headers();
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
headers.insert("From".to_string(), ctx.config.address(ctx.account));
|
||||
headers.insert("To".to_string(), String::new());
|
||||
let subject = parsed_headers
|
||||
.get_first_value("subject")
|
||||
.unwrap_or_default();
|
||||
headers.insert("Subject".to_string(), format!("Fwd: {}", subject));
|
||||
|
||||
let mut parts = vec![];
|
||||
Msg::extract_text_bodies_into(&msg, "text/plain", &mut parts);
|
||||
if parts.is_empty() {
|
||||
Msg::extract_text_bodies_into(&msg, "text/html", &mut parts);
|
||||
}
|
||||
|
||||
let mut body = String::from("-------- Forwarded Message --------\n");
|
||||
body.push_str(&parts.join("\r\n\r\n").replace("\r", ""));
|
||||
|
||||
let mut tpl = Self {
|
||||
headers,
|
||||
body: Some(body),
|
||||
signature: ctx.config.signature(&ctx.account),
|
||||
raw: String::new(),
|
||||
};
|
||||
tpl.raw = tpl.to_string();
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn mailto(ctx: &Ctx, url: &Url) -> Self {
|
||||
let mut headers = HashMap::new();
|
||||
|
||||
let mut cc = Vec::new();
|
||||
let mut bcc = Vec::new();
|
||||
let mut subject = Cow::default();
|
||||
let mut body = Cow::default();
|
||||
|
||||
for (key, val) in url.query_pairs() {
|
||||
match key.as_bytes() {
|
||||
b"cc" => {
|
||||
cc.push(val);
|
||||
}
|
||||
b"bcc" => {
|
||||
bcc.push(val);
|
||||
}
|
||||
b"subject" => {
|
||||
subject = val;
|
||||
}
|
||||
b"body" => {
|
||||
body = val;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
headers.insert(String::from("From"), ctx.config.address(ctx.account));
|
||||
headers.insert(String::from("To"), url.path().to_string());
|
||||
headers.insert(String::from("Subject"), subject.into());
|
||||
if !cc.is_empty() {
|
||||
headers.insert(String::from("Cc"), cc.join(", "));
|
||||
}
|
||||
if !bcc.is_empty() {
|
||||
headers.insert(String::from("Bcc"), cc.join(", "));
|
||||
}
|
||||
|
||||
let mut tpl = Self {
|
||||
headers,
|
||||
body: Some(body.into()),
|
||||
signature: ctx.config.signature(&ctx.account),
|
||||
raw: String::new(),
|
||||
};
|
||||
tpl.raw = tpl.to_string();
|
||||
tpl
|
||||
}
|
||||
|
||||
pub fn header<K: ToString, V: ToString>(&mut self, key: K, val: V) -> &Self {
|
||||
self.headers.insert(key.to_string(), val.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn body<T: ToString>(&mut self, body: T) -> &Self {
|
||||
self.body = Some(body.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signature<T: ToString>(&mut self, signature: T) -> &Self {
|
||||
self.signature = Some(signature.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Tpl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let mut tpl = TPL_HEADERS.iter().fold(String::new(), |mut tpl, &key| {
|
||||
if let Some(val) = self.headers.get(key) {
|
||||
tpl.push_str(&format!("{}: {}\n", key, val));
|
||||
};
|
||||
tpl
|
||||
});
|
||||
|
||||
for (key, val) in self.headers.iter() {
|
||||
if !TPL_HEADERS.contains(&key.as_str()) {
|
||||
tpl.push_str(&format!("{}: {}\n", key, val));
|
||||
}
|
||||
}
|
||||
|
||||
tpl.push_str("\n");
|
||||
|
||||
if let Some(body) = self.body.as_ref() {
|
||||
tpl.push_str(&body);
|
||||
}
|
||||
|
||||
if let Some(signature) = self.signature.as_ref() {
|
||||
tpl.push_str("\n\n");
|
||||
tpl.push_str(&signature);
|
||||
}
|
||||
|
||||
write!(f, "{}", tpl)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
config::model::{Account, Config},
|
||||
ctx::Ctx,
|
||||
msg::tpl::model::Tpl,
|
||||
output::model::Output,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn new_tpl() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let tpl = Tpl::new(&ctx);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: \nSubject: \n\n",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_tpl_with_signature() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
signature: Some(String::from("Cordialement,")),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let tpl = Tpl::new(&ctx);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: \nSubject: \n\n\n\n-- \nCordialement,",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_tpl() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Content-Type: text/plain\r\nFrom: Sender <sender@localhost>\r\nSubject: Test\r\n\r\nHello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::reply(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: Sender <sender@localhost>\nSubject: Re: Test\n\n>Hello, world!",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_tpl_with_signature() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
signature_delimiter: Some(String::from("~~\n")),
|
||||
signature: Some(String::from("Cordialement,")),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Content-Type: text/plain\r\nFrom: Sender <sender@localhost>\r\nSubject: Test\r\n\r\nHello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::reply(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: Sender <sender@localhost>\nSubject: Re: Test\n\n>Hello, world!\n\n~~\nCordialement,",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_all_tpl() {
|
||||
let account = Account {
|
||||
name: Some(String::from("To")),
|
||||
email: String::from("to@localhost"),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Message-Id: 1\r
|
||||
Content-Type: text/plain\r
|
||||
From: From <from@localhost>\r
|
||||
To: To <to@localhost>,to_bis@localhost\r
|
||||
Cc: Cc <cc@localhost>, cc_bis@localhost\r
|
||||
Subject: Test\r
|
||||
\r
|
||||
Hello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::reply_all(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: To <to@localhost>
|
||||
To: From <from@localhost>, to_bis@localhost
|
||||
In-Reply-To: 1
|
||||
Cc: Cc <cc@localhost>, cc_bis@localhost
|
||||
Subject: Re: Test
|
||||
|
||||
>Hello, world!",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reply_all_tpl_with_signature() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
signature: Some(String::from("Cordialement,")),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Content-Type: text/plain\r\nFrom: Sender <sender@localhost>\r\nSubject: Test\r\n\r\nHello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::reply(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: Sender <sender@localhost>\nSubject: Re: Test\n\n>Hello, world!\n\n-- \nCordialement,",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_tpl() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Content-Type: text/plain\r\nFrom: Sender <sender@localhost>\r\nSubject: Test\r\n\r\nHello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::forward(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: \nSubject: Fwd: Test\n\n-------- Forwarded Message --------\nHello, world!",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forward_tpl_with_signature() {
|
||||
let account = Account {
|
||||
name: Some(String::from("Test")),
|
||||
email: String::from("test@localhost"),
|
||||
signature: Some(String::from("Cordialement,")),
|
||||
..Account::default()
|
||||
};
|
||||
let config = Config {
|
||||
accounts: vec![(String::from("account"), account.clone())]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
..Config::default()
|
||||
};
|
||||
let output = Output::default();
|
||||
let mbox = String::default();
|
||||
let arg_matches = clap::ArgMatches::default();
|
||||
let ctx = Ctx::new(&config, &account, &output, &mbox, &arg_matches);
|
||||
let parsed_mail = mailparse::parse_mail(
|
||||
b"Content-Type: text/plain\r\nFrom: Sender <sender@localhost>\r\nSubject: Test\r\n\r\nHello, world!",
|
||||
)
|
||||
.unwrap();
|
||||
let tpl = Tpl::forward(&ctx, &parsed_mail);
|
||||
|
||||
assert_eq!(
|
||||
"From: Test <test@localhost>\nTo: \nSubject: Fwd: Test\n\n-------- Forwarded Message --------\nHello, world!\n\n-- \nCordialement,",
|
||||
tpl.to_string()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ use std::fmt;
|
|||
|
||||
// Output format
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub enum OutputFmt {
|
||||
Plain,
|
||||
Json,
|
||||
|
@ -30,8 +30,8 @@ impl fmt::Display for OutputFmt {
|
|||
}
|
||||
|
||||
// JSON output helper
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
/// A little struct-wrapper to provide a JSON output.
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct OutputJson<T: Serialize> {
|
||||
response: T,
|
||||
}
|
||||
|
@ -43,17 +43,20 @@ impl<T: Serialize> OutputJson<T> {
|
|||
}
|
||||
|
||||
// Output
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A simple wrapper for a general formatting.
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct Output {
|
||||
fmt: OutputFmt,
|
||||
}
|
||||
|
||||
impl Output {
|
||||
/// Create a new output-handler by setting the given formatting style.
|
||||
pub fn new(fmt: &str) -> Self {
|
||||
Self { fmt: fmt.into() }
|
||||
}
|
||||
|
||||
/// Print the provided item out according to the formatting setting when you created this
|
||||
/// struct.
|
||||
pub fn print<T: Serialize + fmt::Display>(&self, item: T) {
|
||||
match self.fmt {
|
||||
OutputFmt::Plain => {
|
||||
|
@ -65,10 +68,12 @@ impl Output {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns true, if the formatting should be plaintext.
|
||||
pub fn is_plain(&self) -> bool {
|
||||
self.fmt == OutputFmt::Plain
|
||||
}
|
||||
|
||||
/// Returns true, if the formatting should be json.
|
||||
pub fn is_json(&self) -> bool {
|
||||
self.fmt == OutputFmt::Json
|
||||
}
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
extern crate himalaya;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use himalaya::{
|
||||
config::model::Account, imap::model::ImapConnector, mbox::model::Mboxes, msg::model::Msgs, smtp,
|
||||
config::model::Account, flag::model::Flags, imap::model::ImapConnector, mbox::model::Mboxes,
|
||||
msg::model::Msgs, smtp,
|
||||
};
|
||||
|
||||
use imap::types::Flag;
|
||||
|
||||
use lettre::message::SinglePart;
|
||||
use lettre::Message;
|
||||
|
||||
fn get_account(addr: &str) -> Account {
|
||||
Account {
|
||||
name: None,
|
||||
|
@ -45,74 +51,99 @@ fn mbox() {
|
|||
|
||||
#[test]
|
||||
fn msg() {
|
||||
let account = get_account("inbox@localhost");
|
||||
// Preparations
|
||||
|
||||
// Add messages
|
||||
smtp::send(
|
||||
&account,
|
||||
&lettre::Message::builder()
|
||||
.from("sender-a@localhost".parse().unwrap())
|
||||
.to("inbox@localhost".parse().unwrap())
|
||||
.subject("Subject A")
|
||||
.singlepart(lettre::message::SinglePart::builder().body("Body A".as_bytes().to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
smtp::send(
|
||||
&account,
|
||||
&lettre::Message::builder()
|
||||
.from("\"Sender B\" <sender-b@localhost>".parse().unwrap())
|
||||
.to("inbox@localhost".parse().unwrap())
|
||||
.subject("Subject B")
|
||||
.singlepart(lettre::message::SinglePart::builder().body("Body B".as_bytes().to_vec()))
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
// Get the test-account and clean up the server.
|
||||
let account = get_account("inbox@localhost");
|
||||
|
||||
// Login
|
||||
let mut imap_conn = ImapConnector::new(&account).unwrap();
|
||||
|
||||
// List messages
|
||||
// TODO: check non-existance of \Seen flag
|
||||
let msgs = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
|
||||
let msgs = if let Some(ref fetches) = msgs {
|
||||
Msgs::from(fetches)
|
||||
// remove all previous mails first
|
||||
let fetches = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
|
||||
let msgs = if let Some(ref fetches) = fetches {
|
||||
Msgs::try_from(fetches).unwrap()
|
||||
} else {
|
||||
Msgs::new()
|
||||
};
|
||||
|
||||
// mark all mails as deleted
|
||||
for msg in msgs.0.iter() {
|
||||
imap_conn
|
||||
.add_flags(
|
||||
"INBOX",
|
||||
&msg.get_uid().unwrap().to_string(),
|
||||
Flags::from(vec![Flag::Deleted]),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
imap_conn.expunge("INBOX").unwrap();
|
||||
|
||||
// make sure, that they are *really* deleted
|
||||
assert!(imap_conn.list_msgs("INBOX", &10, &0).unwrap().is_none());
|
||||
|
||||
// == Testing ==
|
||||
// Add messages
|
||||
let message_a = Message::builder()
|
||||
.from("sender-a@localhost".parse().unwrap())
|
||||
.to("inbox@localhost".parse().unwrap())
|
||||
.subject("Subject A")
|
||||
.singlepart(SinglePart::builder().body("Body A".as_bytes().to_vec()))
|
||||
.unwrap();
|
||||
|
||||
let message_b = Message::builder()
|
||||
.from("Sender B <sender-b@localhost>".parse().unwrap())
|
||||
.to("inbox@localhost".parse().unwrap())
|
||||
.subject("Subject B")
|
||||
.singlepart(SinglePart::builder().body("Body B".as_bytes().to_vec()))
|
||||
.unwrap();
|
||||
|
||||
smtp::send(&account, &message_a).unwrap();
|
||||
smtp::send(&account, &message_b).unwrap();
|
||||
|
||||
// -- Get the messages --
|
||||
// TODO: check non-existance of \Seen flag
|
||||
let msgs = imap_conn.list_msgs("INBOX", &10, &0).unwrap();
|
||||
let msgs = if let Some(ref fetches) = msgs {
|
||||
Msgs::try_from(fetches).unwrap()
|
||||
} else {
|
||||
Msgs::new()
|
||||
};
|
||||
|
||||
// make sure that there are both mails which we sended
|
||||
assert_eq!(msgs.0.len(), 2);
|
||||
|
||||
let msg_a = msgs
|
||||
.0
|
||||
.iter()
|
||||
.find(|msg| msg.subject == "Subject A")
|
||||
.find(|msg| msg.headers.subject.clone().unwrap() == "Subject A")
|
||||
.unwrap();
|
||||
assert_eq!(msg_a.subject, "Subject A");
|
||||
assert_eq!(msg_a.sender, "sender-a@localhost");
|
||||
|
||||
let msg_b = msgs
|
||||
.0
|
||||
.iter()
|
||||
.find(|msg| msg.subject == "Subject B")
|
||||
.find(|msg| msg.headers.subject.clone().unwrap() == "Subject B")
|
||||
.unwrap();
|
||||
assert_eq!(msg_b.subject, "Subject B");
|
||||
assert_eq!(msg_b.sender, "Sender B");
|
||||
|
||||
// -- Checkup --
|
||||
// look, if we received the correct credentials of the msgs.
|
||||
assert_eq!(
|
||||
msg_a.headers.subject.clone().unwrap_or_default(),
|
||||
"Subject A"
|
||||
);
|
||||
assert_eq!(&msg_a.headers.from[0], "sender-a@localhost");
|
||||
|
||||
assert_eq!(
|
||||
msg_b.headers.subject.clone().unwrap_or_default(),
|
||||
"Subject B"
|
||||
);
|
||||
assert_eq!(&msg_b.headers.from[0], "Sender B <sender-b@localhost>");
|
||||
|
||||
// TODO: search messages
|
||||
// TODO: read message (+ \Seen flag)
|
||||
// TODO: list message attachments
|
||||
// TODO: add/set/remove flags
|
||||
|
||||
// Delete messages
|
||||
imap_conn
|
||||
.add_flags("INBOX", &msg_a.uid.to_string(), "\\Deleted")
|
||||
.unwrap();
|
||||
imap_conn
|
||||
.add_flags("INBOX", &msg_b.uid.to_string(), "\\Deleted")
|
||||
.unwrap();
|
||||
imap_conn.expunge("INBOX").unwrap();
|
||||
assert!(imap_conn.list_msgs("INBOX", &10, &1).unwrap().is_none());
|
||||
|
||||
// Logout
|
||||
imap_conn.logout();
|
||||
}
|
||||
|
|
1
wiki
1
wiki
|
@ -1 +0,0 @@
|
|||
Subproject commit 51bc2d44022ed9c4695a2d6c15f2187d203e22b7
|
Loading…
Reference in a new issue