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:
TornaxO7 2021-09-11 00:35:22 +02:00 committed by GitHub
parent 2ac2f53f31
commit 0e68801a35
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 4381 additions and 1967 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -9,4 +9,4 @@
sha256 = lock.nodes.flake-compat.locked.narHash; }
) {
src = ./.;
}).defaultNix
}).defaultNix

75
rustfmt.toml Normal file
View 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

View File

@ -9,4 +9,4 @@
sha256 = lock.nodes.flake-compat.locked.narHash; }
) {
src = ./.;
}).shellNix
}).shellNix

View File

@ -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() {

View File

@ -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>");
}
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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))?;

View File

@ -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))?

View File

@ -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;

View File

@ -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(())
}

View File

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

View File

@ -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
View 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
View 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)
}
}

View File

@ -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
View 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);
}
}
}

View File

@ -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;

File diff suppressed because it is too large Load Diff

View File

@ -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)
}

View File

@ -1,2 +0,0 @@
pub mod cli;
pub mod model;

View File

@ -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()
);
}
}

View File

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

View File

@ -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 +0,0 @@
Subproject commit 51bc2d44022ed9c4695a2d6c15f2187d203e22b7