From 0e68801a35eeafb6ca0d472f2e84c40c95cca4ae Mon Sep 17 00:00:00 2001 From: TornaxO7 <50843046+TornaxO7@users.noreply.github.com> Date: Sat, 11 Sep 2021 00:35:22 +0200 Subject: [PATCH] refactor msg model (#173) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> to HashSet>, 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 _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 { : , .. 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 Co-authored-by: Erik --- .github/workflows/tests.yaml | 5 +- Cargo.lock | 658 ++++++++--- Cargo.toml | 17 +- default.nix | 2 +- rustfmt.toml | 75 ++ shell.nix | 2 +- src/comp/cli.rs | 5 +- src/config/model.rs | 262 ++++- src/ctx.rs | 27 +- src/flag/cli.rs | 17 +- src/flag/model.rs | 213 +++- src/imap/cli.rs | 4 +- src/imap/model.rs | 135 ++- src/input.rs | 6 +- src/lib.rs | 38 + src/main.rs | 42 +- src/mbox/cli.rs | 36 +- src/mbox/model.rs | 102 +- src/msg/attachment.rs | 159 +++ src/msg/body.rs | 151 +++ src/msg/cli.rs | 773 ++++++++----- src/msg/headers.rs | 641 +++++++++++ src/msg/mod.rs | 36 +- src/msg/model.rs | 2044 ++++++++++++++++++++++++---------- src/msg/tpl/cli.rs | 226 ---- src/msg/tpl/mod.rs | 2 - src/msg/tpl/model.rs | 533 --------- src/output/model.rs | 15 +- tests/imap_test.rs | 121 +- wiki | 1 - 30 files changed, 4381 insertions(+), 1967 deletions(-) create mode 100644 rustfmt.toml create mode 100644 src/msg/attachment.rs create mode 100644 src/msg/body.rs create mode 100644 src/msg/headers.rs delete mode 100644 src/msg/tpl/cli.rs delete mode 100644 src/msg/tpl/mod.rs delete mode 100644 src/msg/tpl/model.rs delete mode 160000 wiki diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index d6a191d1..f8d6eeea 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -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: diff --git a/Cargo.lock b/Cargo.lock index 33fddf68..b59e75f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,10 +1,12 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "addr2line" -version = "0.14.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" +checksum = "e7a2e47a1fbe209ee101dd6d61285226744c6c8d3c21c8dc878ba6cb9f467f3a" dependencies = [ "gimli", ] @@ -17,11 +19,11 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" dependencies = [ - "memchr 2.4.0", + "memchr 2.3.4", ] [[package]] @@ -33,6 +35,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "ascii_utils" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" + [[package]] name = "atty" version = "0.2.14" @@ -44,6 +52,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "autocfg" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d49d90015b3c36167a20fe2810c5cd875ad504b39cff3d4eae7977e6b7c1cb2" + [[package]] name = "autocfg" version = "1.0.1" @@ -52,11 +66,12 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "backtrace" -version = "0.3.56" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d117600f438b1707d4e4ae15d3595657288f8235a0eb593e80ecc98ab34e1bc" +checksum = "4717cfcbfaa661a0fd48f8453951837ae7e8f81e481fbb136e3202d72805a744" dependencies = [ "addr2line", + "cc", "cfg-if 1.0.0", "libc", "miniz_oxide", @@ -64,6 +79,16 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643" +dependencies = [ + "byteorder", + "safemem", +] + [[package]] name = "base64" version = "0.10.1" @@ -73,12 +98,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "base64" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" - [[package]] name = "base64" version = "0.13.0" @@ -87,9 +106,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitvec" @@ -111,15 +130,15 @@ checksum = "40e38929add23cdf8a366df9b0e088953150724bcbe5fc330b0d8eb3b328eec8" [[package]] name = "byteorder" -version = "1.3.4" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cc" -version = "1.0.67" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" [[package]] name = "cfg-if" @@ -179,6 +198,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "colorful" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bca1619ff57dd7a56b58a8e25ef4199f123e78e503fe1653410350a1b98ae65" + [[package]] name = "core-foundation" version = "0.9.1" @@ -217,19 +242,98 @@ dependencies = [ ] [[package]] -name = "encoding_rs" -version = "0.8.26" +name = "email" +version = "0.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801bbab217d7f79c0062f4f7205b5d4427c6d1a7bd7aafdd1475f7c59d62b283" +checksum = "91549a51bb0241165f13d57fc4c72cef063b4088fb078b019ecbf464a45f22e4" +dependencies = [ + "base64 0.9.3", + "chrono", + "encoding", + "lazy_static", + "rand 0.4.6", + "time", + "version_check 0.1.5", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "env_logger" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" dependencies = [ "atty", "humantime", @@ -245,14 +349,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ "backtrace", - "version_check", + "version_check 0.9.3", +] + +[[package]] +name = "fast_chemail" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495a39d30d624c2caabe6312bfead73e7717692b44e0b32df168c275a2e8e9e4" +dependencies = [ + "ascii_utils", ] [[package]] name = "fastrand" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b705829d1e87f762c2df6da140b26af5839e1033aa84aa5f56bb688e4e1bdb" +checksum = "b394ed3d285a429378d3b384b9eb1285267e7df4b166df24b7a6939a04dc392e" dependencies = [ "instant", ] @@ -294,6 +407,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + [[package]] name = "funty" version = "1.1.0" @@ -301,10 +420,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] -name = "getrandom" -version = "0.2.2" +name = "futures-core" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" + +[[package]] +name = "futures-io" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" + +[[package]] +name = "futures-task" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" + +[[package]] +name = "futures-util" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +dependencies = [ + "autocfg 1.0.1", + "futures-core", + "futures-io", + "futures-task", + "memchr 2.3.4", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ "cfg-if 1.0.0", "libc", @@ -313,21 +466,21 @@ dependencies = [ [[package]] name = "gimli" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6503fe142514ca4799d4c26297c4248239fe8838d827db6bd6065c6ed29a6ce" +checksum = "0e4075386626662786ddb0ec9081e7c7eeb1ba31951f447ca780ef9f5d568189" [[package]] name = "hashbrown" -version = "0.9.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hermit-abi" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -339,10 +492,13 @@ dependencies = [ "atty", "chrono", "clap", + "colorful", "env_logger", "error-chain", "imap", - "lettre", + "imap-proto", + "lettre 0.10.0-rc.3", + "lettre_email", "log", "mailparse", "native-tls", @@ -355,7 +511,7 @@ dependencies = [ "tree_magic", "unicode-width", "url", - "uuid", + "uuid 0.8.2", ] [[package]] @@ -371,9 +527,9 @@ dependencies = [ [[package]] name = "httpdate" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05842d0d43232b23ccb7060ecb0f0626922c21f30012e97b767b30afd4a5d4b9" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "humantime" @@ -394,9 +550,9 @@ dependencies = [ [[package]] name = "imap" -version = "3.0.0-alpha.3" +version = "3.0.0-alpha.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6db7782d7160066e0293d32c31b06b443f6da58a86594629554582d8a006097" +checksum = "b26b6f9b2c28b6aa7fabf623e75a8b6bd382ee0951d0e052c1d32c887150fb67" dependencies = [ "base64 0.13.0", "bufstream", @@ -404,43 +560,43 @@ dependencies = [ "imap-proto", "lazy_static", "native-tls", - "nom 6.1.2", + "nom 6.2.1", "regex", ] [[package]] name = "imap-proto" -version = "0.14.1" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06045f4bd4770f83a263cf85c5be0187e8f646dfa31507ee302019145405f0b6" +checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb" dependencies = [ - "nom 6.1.2", + "nom 6.2.1", ] [[package]] name = "indexmap" -version = "1.6.2" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ - "autocfg", + "autocfg 1.0.1", "hashbrown", ] [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "lazy_static" @@ -450,29 +606,54 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "lettre" -version = "0.10.0-rc.1" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be4ff7e8bcb0e0c6902815554a286889b0e99b4ea6e898afb7b9f53174b1929" +checksum = "86ed8677138975b573ab4949c35613931a4addeadd0a8a6aa0327e2a979660de" +dependencies = [ + "fast_chemail", + "log", +] + +[[package]] +name = "lettre" +version = "0.10.0-rc.3" +source = "git+https://github.com/TornaxO7/lettre/?branch=master#0aee778acf12a32c25585fb6d3e9a30afdfc09b3" dependencies = [ "base64 0.13.0", "fastrand", + "futures-util", "hostname", "httpdate", "idna", "mime", "native-tls", - "nom 6.1.2", + "nom 7.0.0", "once_cell", "quoted_printable", "r2d2", "regex", + "serde", +] + +[[package]] +name = "lettre_email" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd02480f8dcf48798e62113974d6ccca2129a51d241fa20f1ea349c8a42559d5" +dependencies = [ + "base64 0.10.1", + "email", + "lettre 0.9.6", + "mime", + "time", + "uuid 0.7.4", ] [[package]] name = "libc" -version = "0.2.94" +version = "0.2.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e" +checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" [[package]] name = "lock_api" @@ -485,9 +666,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -503,11 +684,11 @@ dependencies = [ [[package]] name = "mailparse" -version = "0.13.1" +version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388a77a7f161b32d0314404306b8ed5966b34b797fc9ef6bcf6686935162da3c" +checksum = "5ee6e1ca1c8396da58f8128176f6980dd57bec84c8670a479519d3655f2d6734" dependencies = [ - "base64 0.12.3", + "base64 0.13.0", "charset", "quoted_printable", ] @@ -520,9 +701,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" @@ -535,9 +716,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.0" +version = "2.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" [[package]] name = "mime" @@ -545,6 +726,12 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "minimal-lexical" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6595bb28ed34f43c3fe088e48f6cfb2e033cab45f25a5384d5fdf564fbc8c4b2" + [[package]] name = "miniz_oxide" version = "0.4.4" @@ -552,14 +739,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" dependencies = [ "adler", - "autocfg", + "autocfg 1.0.1", ] [[package]] name = "native-tls" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8d96b2e1c8da3957d58100b09f102c6d9cfdfced01b7ec5a8974044bb09dbd4" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" dependencies = [ "lazy_static", "libc", @@ -584,14 +771,25 @@ dependencies = [ [[package]] name = "nom" -version = "6.1.2" +version = "6.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" +checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" dependencies = [ "bitvec", "funty", - "memchr 2.4.0", - "version_check", + "memchr 2.3.4", + "version_check 0.9.3", +] + +[[package]] +name = "nom" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" +dependencies = [ + "memchr 2.3.4", + "minimal-lexical", + "version_check 0.9.3", ] [[package]] @@ -600,7 +798,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ - "autocfg", + "autocfg 1.0.1", "num-traits", ] @@ -610,26 +808,26 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ - "autocfg", + "autocfg 1.0.1", ] [[package]] name = "object" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a7ab5d64814df0fe4a4b5ead45ed6c5f181ee3ff04ba344313a6c80446c5d4" +checksum = "1a5b3dd1c072ee7963717671d1ca129f1048fda25edea6b752bfc71ac8854170" [[package]] name = "once_cell" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" +checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" [[package]] name = "openssl" -version = "0.10.34" +version = "0.10.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7830286ad6a3973c0f1d9b73738f69c76b739301d0229c4b96501695cbe4c8" +checksum = "8d9facdb76fec0b73c406f125d44d86fdad818d66fef0531eec9233ca425ff4a" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -647,11 +845,11 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.63" +version = "0.9.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b0d6fb7d80f877617dfcb014e605e2b5ab2fb0afdf27935219bb6bd984cb98" +checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" dependencies = [ - "autocfg", + "autocfg 1.0.1", "cc", "libc", "pkg-config", @@ -670,13 +868,13 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api 0.4.4", - "parking_lot_core 0.8.3", + "lock_api 0.4.5", + "parking_lot_core 0.8.5", ] [[package]] @@ -695,14 +893,14 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if 1.0.0", "instant", "libc", - "redox_syscall 0.2.8", + "redox_syscall 0.2.10", "smallvec", "winapi", ] @@ -723,6 +921,18 @@ dependencies = [ "indexmap", ] +[[package]] +name = "pin-project-lite" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.19" @@ -737,18 +947,18 @@ checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] [[package]] name = "quote" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" dependencies = [ "proc-macro2", ] @@ -766,7 +976,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" dependencies = [ "log", - "parking_lot 0.11.1", + "parking_lot 0.11.2", "scheduled-thread-pool", ] @@ -778,42 +988,170 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "rand" -version = "0.8.3" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +dependencies = [ + "autocfg 0.1.7", + "libc", + "rand_chacha 0.1.1", + "rand_core 0.4.2", + "rand_hc 0.1.0", + "rand_isaac", + "rand_jitter", + "rand_os", + "rand_pcg", + "rand_xorshift", + "winapi", +] + +[[package]] +name = "rand" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "libc", - "rand_chacha", - "rand_core", - "rand_hc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "rand_hc 0.3.1", ] [[package]] name = "rand_chacha" -version = "0.3.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d" +checksum = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", ] [[package]] name = "rand_core" -version = "0.6.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] name = "rand_hc" -version = "0.3.0" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73" +checksum = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" dependencies = [ - "rand_core", + "rand_core 0.3.1", +] + +[[package]] +name = "rand_hc" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" +dependencies = [ + "rand_core 0.6.3", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +dependencies = [ + "libc", + "rand_core 0.4.2", + "winapi", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +dependencies = [ + "cloudabi", + "fuchsia-cprng", + "libc", + "rand_core 0.4.2", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +dependencies = [ + "autocfg 0.1.7", + "rand_core 0.4.2", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +dependencies = [ + "rand_core 0.3.1", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", ] [[package]] @@ -824,9 +1162,9 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -838,17 +1176,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", - "redox_syscall 0.2.8", + "redox_syscall 0.2.10", ] [[package]] name = "regex" -version = "1.5.4" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" dependencies = [ "aho-corasick", - "memchr 2.4.0", + "memchr 2.3.4", "regex-syntax", ] @@ -880,9 +1218,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.18" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "ryu" @@ -890,6 +1228,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "schannel" version = "0.1.19" @@ -906,7 +1250,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" dependencies = [ - "parking_lot 0.11.1", + "parking_lot 0.11.2", ] [[package]] @@ -917,9 +1261,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "security-framework" -version = "2.2.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3670b1d2fdf6084d192bc71ead7aabe6c06aa2ea3fbd9cc3ac111fa5c2b1bd84" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ "bitflags", "core-foundation", @@ -930,9 +1274,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.2.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3676258fd3cfe2c9a0ec99ce3038798d847ce3e4bb17746373eb9f0f1ac16339" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", @@ -940,18 +1284,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.118" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06c64263859d87aa2eb554587e2d23183398d617427327cf2b3d0ed8c69e4800" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.118" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -960,9 +1304,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.61" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a" +checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" dependencies = [ "itoa", "ryu", @@ -978,6 +1322,12 @@ dependencies = [ "dirs-next", ] +[[package]] +name = "slab" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" + [[package]] name = "smallvec" version = "1.6.1" @@ -992,9 +1342,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "syn" -version = "1.0.55" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a571a711dddd09019ccc628e1b17fe87c59b09d513c06c026877aa708334f37a" +checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" dependencies = [ "proc-macro2", "quote", @@ -1015,8 +1365,8 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ "cfg-if 1.0.0", "libc", - "rand", - "redox_syscall 0.2.8", + "rand 0.8.4", + "redox_syscall 0.2.10", "remove_dir_all", "winapi", ] @@ -1032,9 +1382,9 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.1.15" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd2d183bd3fac5f5fe38ddbeb4dc9aec4a39a9d7d59e7491d900302da01cbe1" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" dependencies = [ "libc", "winapi", @@ -1062,9 +1412,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -1099,18 +1449,15 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" [[package]] name = "unicode-normalization" -version = "0.1.17" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] @@ -1123,9 +1470,9 @@ checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" [[package]] name = "unicode-xid" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] name = "url" @@ -1139,6 +1486,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbc611eb48397705a6b0f6e917da23ae517e4d127123d2cf7674206627d32a" +dependencies = [ + "rand 0.6.5", +] + [[package]] name = "uuid" version = "0.8.2" @@ -1150,9 +1506,15 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdbff6266a24120518560b5dc983096efb98462e51d0d68169895b237be3e5d" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" [[package]] name = "version_check" diff --git a/Cargo.toml b/Cargo.toml index 6bd86e75..b623eaf2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,16 +8,21 @@ edition = "2018" [dependencies] atty = "0.2.14" chrono = "0.4.19" -clap = {version = "2.33.3", default-features = false, features = ["suggestions", "color"]} +clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } +colorful = "0.2.1" env_logger = "0.8.3" error-chain = "0.12.4" -imap = "3.0.0-alpha.3" -lettre = "0.10.0-rc.1" +imap = "3.0.0-alpha.4" +imap-proto = "0.14.3" +# This commit includes the de/serialization of the ContentType +# lettre = { version = "0.10.0-rc.1", features = ["serde"] } +lettre = {git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] } +lettre_email = "0.9.4" log = "0.4.14" -mailparse = "0.13.1" +mailparse = "0.13.4" native-tls = "0.2" rfc2047-decoder = "0.1.2" -serde = {version = "1.0.118", features = ["derive"]} +serde = { version = "1.0.118", features = ["derive"] } serde_json = "1.0.61" shellexpand = "2.1.0" terminal_size = "0.1.15" @@ -25,4 +30,4 @@ toml = "0.5.8" tree_magic = "0.2.3" unicode-width = "0.1.7" url = "2.2.2" -uuid = {version = "0.8", features = ["v4"]} +uuid = { version = "0.8", features = ["v4"] } diff --git a/default.nix b/default.nix index dfb939da..7b821b27 100644 --- a/default.nix +++ b/default.nix @@ -9,4 +9,4 @@ sha256 = lock.nodes.flake-compat.locked.narHash; } ) { src = ./.; -}).defaultNix \ No newline at end of file +}).defaultNix diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..c39d2eb1 --- /dev/null +++ b/rustfmt.toml @@ -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 diff --git a/shell.nix b/shell.nix index ff542a09..a277e589 100644 --- a/shell.nix +++ b/shell.nix @@ -9,4 +9,4 @@ sha256 = lock.nodes.flake-compat.locked.narHash; } ) { src = ./.; -}).shellNix \ No newline at end of file +}).shellNix diff --git a/src/comp/cli.rs b/src/comp/cli.rs index a36bc8ba..2aa9e786 100644 --- a/src/comp/cli.rs +++ b/src/comp/cli.rs @@ -5,7 +5,8 @@ use std::io; error_chain! {} -pub fn comp_subcmds<'s>() -> Vec> { +// == Main functions == +pub fn subcmds<'s>() -> Vec> { 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> { .required(true)])] } -pub fn comp_matches<'a>(app: fn() -> App<'a, 'a>, matches: &ArgMatches) -> Result { +pub fn matches<'a>(app: fn() -> App<'a, 'a>, matches: &ArgMatches) -> Result { if let Some(matches) = matches.subcommand_matches("completion") { debug!("completion command matched"); let shell = match matches.value_of("shell").unwrap() { diff --git a/src/config/model.rs b/src/config/model.rs index f8db75b9..b7944fd7 100644 --- a/src/config/model.rs +++ b/src/config/model.rs @@ -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 { 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(name: Option, 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 + ToString>( + name: Option, + email_addr: S, + signature: Option, + ) -> 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, pub notify_cmd: Option, + /// Option to override the default signature delimiter "`--\n `". pub signature_delimiter: Option, pub signature: Option, pub default_page_size: Option, @@ -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) -> Result { 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 "); + /// assert_eq!(config.address(&special_account), "\"TL;DR\" "); + /// } + /// ``` 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>(&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 { 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 "); + assert_eq!(&config.address(&account2), "Two "); + assert_eq!(&config.address(&account3), "\"TL;DR\" "); + assert_eq!(&config.address(&account4), "\"TL,DR\" "); + assert_eq!(&config.address(&account5), "\"TL:DR\" "); + assert_eq!(&config.address(&account6), "\"TL.DR\" "); } } } diff --git a/src/ctx.rs b/src/ctx.rs index 6ca41a0e..ad01d0bb 100644 --- a/src/ctx.rs +++ b/src/ctx.rs @@ -5,22 +5,27 @@ use crate::{ output::model::Output, }; +/// `Ctx` stands for `Context` and includes the most "important" structs which are used quite often +/// in this crate. +#[derive(Debug, Default, Clone)] pub struct Ctx<'a> { - pub config: &'a Config, - pub account: &'a Account, - pub output: &'a Output, - pub mbox: &'a str, - pub arg_matches: &'a clap::ArgMatches<'a>, + pub config: Config, + pub account: Account, + pub output: Output, + pub mbox: String, + pub arg_matches: clap::ArgMatches<'a>, } impl<'a> Ctx<'a> { - pub fn new( - config: &'a Config, - account: &'a Account, - output: &'a Output, - mbox: &'a str, - arg_matches: &'a clap::ArgMatches<'a>, + pub fn new( + config: Config, + account: Account, + output: Output, + mbox: S, + arg_matches: clap::ArgMatches<'a>, ) -> Self { + let mbox = mbox.to_string(); + Self { config, account, diff --git a/src/flag/cli.rs b/src/flag/cli.rs index 4a94c31b..75da1b02 100644 --- a/src/flag/cli.rs +++ b/src/flag/cli.rs @@ -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> { +pub fn subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("flags") .about("Handles flags") .subcommand( @@ -42,7 +42,7 @@ pub fn flag_subcmds<'a>() -> Vec> { )] } -pub fn flag_matches(ctx: &Ctx) -> Result { +pub fn matches(ctx: &Ctx) -> Result { 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 { 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 { 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 { 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); diff --git a/src/flag/model.rs b/src/flag/model.rs index 4b1efb7e..51393f53 100644 --- a/src/flag/model.rs +++ b/src/flag/model.rs @@ -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(&self, serializer: S) -> Result 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>); -#[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::>>(), + ) + } +} + +impl<'a> From>> for Flags { + fn from(flags: Vec>) -> Self { + Self( + flags + .iter() + .map(|flag| convert_to_static(flag).unwrap()) + .collect::>>(), + ) + } +} + +/// 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> = 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>; 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(&self, serializer: S) -> Result 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, ()> { + 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 = 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); + } +} diff --git a/src/imap/cli.rs b/src/imap/cli.rs index 72b45888..4f14bd88 100644 --- a/src/imap/cli.rs +++ b/src/imap/cli.rs @@ -11,7 +11,7 @@ error_chain! { } } -pub fn imap_subcmds<'a>() -> Vec> { +pub fn subcmds<'a>() -> Vec> { 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> { ] } -pub fn imap_matches(ctx: &Ctx) -> Result { +pub fn matches(ctx: &Ctx) -> Result { if let Some(matches) = ctx.arg_matches.subcommand_matches("notify") { debug!("notify command matched"); diff --git a/src/imap/model.rs b/src/imap/model.rs index 5e50c7ac..442fb27d 100644 --- a/src/imap/model.rs +++ b/src/imap/model.rs @@ -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 { 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> { + /// Get the message according to the given `mbox` and `uid`. + pub fn get_msg(&mut self, mbox: &str, uid: &str) -> Result { 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) -> 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> = (*msg.flags).clone(); + self.sess - .append(mbox, msg) + .append(mbox, &body) .flags(flags) .finish() .chain_err(|| format!("Could not append message to `{}`", mbox))?; diff --git a/src/input.rs b/src/input.rs index 9ddbc048..bd7df3c4 100644 --- a/src/input.rs +++ b/src/input.rs @@ -49,19 +49,19 @@ pub fn open_editor_with_tpl(tpl: &[u8]) -> Result { } } - 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))? diff --git a/src/lib.rs b/src/lib.rs index 672508f8..7635949c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,49 @@ +//! # Welcome to Himalaya! +//! Here's a little summary of how to read the code of himalaya: +//! Each module includes three "main" files: +//! - `model.rs`: **The "main" file** of each module which includes the main implementation of the given +//! module. +//! - `cli.rs`: Includes the subcommands and arguments which are related to the module. +//! +//! For example the `read` subcommand is in the `msg/cli.rs` file because it's related to the +//! msg you want to read. +//! +//! - `mod.rs`: Includes all other files in the module. Click [here] for more information. +//! +//! [here]: https://doc.rust-lang.org/book/ch07-02-defining-modules-to-control-scope-and-privacy.html + +/// `comp` stands for `completion`. This module makes it possible to create autocompletion-settings +/// for himalaya for your shell :) pub mod comp; + +/// Everything which is related to the config files. For example the structure of your config file. pub mod config; + +/// A often used-struct to help us to access the most often used structs. pub mod ctx; + +/// A wrapper for representing a flag of a message or mailbox. For example the delete-flag or +/// read-flag. pub mod flag; + +/// A wrapper for creating connections easier to the IMAP-Servers. pub mod imap; + +/// Handles the input-interaction with the user. For example if you want to edit the body of your +/// message, his module takes care of the draft and calls your ~(neo)vim~ your favourite editor. pub mod input; + +/// Everything which is related to mboxes, for example creating or deleting some. pub mod mbox; + +/// Includes everything related to a message. This means: Body, Headers, Attachments, etc. pub mod msg; + +/// Handles the output. For example the JSON and HTML output. pub mod output; + +/// This module takes care for sending your mails! pub mod smtp; + +/// The TUI for listing the mails for example. pub mod table; diff --git a/src/main.rs b/src/main.rs index 8cb12521..faf41e70 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,13 +6,11 @@ use std::{env, path::PathBuf, process::exit}; use url::{self, Url}; use himalaya::{ - comp::cli::{comp_matches, comp_subcmds}, + comp, config::{cli::config_args, model::Config}, ctx::Ctx, - flag::cli::{flag_matches, flag_subcmds}, - imap::cli::{imap_matches, imap_subcmds}, - mbox::cli::{mbox_matches, mbox_source_arg, mbox_subcmds}, - msg::cli::{msg_matches, msg_matches_mailto, msg_subcmds}, + flag, imap, mbox, + msg::{self, cli::msg_matches_mailto}, output::{cli::output_args, model::Output}, }; @@ -38,12 +36,12 @@ fn parse_args<'a>() -> clap::App<'a, 'a> { .setting(clap::AppSettings::InferSubcommands) .args(&output_args()) .args(&config_args()) - .arg(mbox_source_arg()) - .subcommands(flag_subcmds()) - .subcommands(imap_subcmds()) - .subcommands(mbox_subcmds()) - .subcommands(msg_subcmds()) - .subcommands(comp_subcmds()) + .arg(mbox::cli::source_arg()) + .subcommands(flag::cli::subcmds()) + .subcommands(imap::cli::subcmds()) + .subcommands(mbox::cli::subcmds()) + .subcommands(msg::cli::subcmds()) + .subcommands(comp::cli::subcmds()) } fn run() -> Result<()> { @@ -52,13 +50,15 @@ fn run() -> Result<()> { ); let raw_args: Vec = 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 = 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(()) } diff --git a/src/mbox/cli.rs b/src/mbox/cli.rs index 930a19e2..bbbc8136 100644 --- a/src/mbox/cli.rs +++ b/src/mbox/cli.rs @@ -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> { +// == Main functions == +pub fn subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("mailboxes") .aliases(&["mailbox", "mboxes", "mbox", "m"]) .about("Lists all mailboxes")] } -pub fn mbox_matches(ctx: &Ctx) -> Result { +pub fn matches(ctx: &Ctx) -> Result { if let Some(_) = ctx.arg_matches.subcommand_matches("mailboxes") { debug!("mailboxes command matched"); @@ -49,3 +35,19 @@ pub fn mbox_matches(ctx: &Ctx) -> Result { 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") +} diff --git a/src/mbox/model.rs b/src/mbox/model.rs index a14e3413..bee203cb 100644 --- a/src/mbox/model.rs +++ b/src/mbox/model.rs @@ -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>); -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::>>(), + ) } } -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(&self, serializer: T) -> Result 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>); +pub struct Mboxes(pub Vec); -impl<'a> From<&'a imap::types::ZeroCopy>> for Mboxes<'a> { +impl<'a> From<&'a imap::types::ZeroCopy>> for Mboxes { fn from(names: &'a imap::types::ZeroCopy>) -> Self { Self(names.iter().map(Mbox::from).collect::>()) } } -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, ()> { + 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()))), + } +} diff --git a/src/msg/attachment.rs b/src/msg/attachment.rs new file mode 100644 index 00000000..1744397d --- /dev/null +++ b/src/msg/attachment.rs @@ -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, +} + +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) -> 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 { + 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 { + 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, + }) + } +} diff --git a/src/msg/body.rs b/src/msg/body.rs new file mode 100644 index 00000000..185335ce --- /dev/null +++ b/src/msg/body.rs @@ -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, + + /// The html version of a body (if available) + pub html: Option, +} + +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(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(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(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) + } +} diff --git a/src/msg/cli.rs b/src/msg/cli.rs index af4e7b31..300268a9 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -1,27 +1,26 @@ +use super::body::Body; +use super::headers::Headers; +use super::model::{Msg, MsgSerialized, Msgs}; +use url::Url; + use atty::Stream; use clap; use error_chain::error_chain; +use lettre::message::header::ContentTransferEncoding; use log::{debug, error, trace}; + use std::{ + borrow::Cow, + collections::HashMap, + convert::TryFrom, fs, io::{self, BufRead}, - ops::Deref, }; -use url::Url; + +use imap::types::Flag; use crate::{ - ctx::Ctx, - flag::model::Flag, - imap::model::ImapConnector, - input, - mbox::cli::mbox_target_arg, - msg::{ - model::{Attachments, Msg, Msgs, ReadableMsg}, - tpl::{ - cli::{tpl_matches, tpl_subcommand}, - model::Tpl, - }, - }, + ctx::Ctx, flag::model::Flags, imap::model::ImapConnector, input, mbox::cli::mbox_target_arg, smtp, }; @@ -29,8 +28,7 @@ error_chain! { links { Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); Input(crate::input::Error, crate::input::ErrorKind); - MsgModel(crate::msg::model::Error, crate::msg::model::ErrorKind); - TplCli(crate::msg::tpl::cli::Error, crate::msg::tpl::cli::ErrorKind); + MsgModel(super::model::Error, super::model::ErrorKind); Smtp(crate::smtp::Error, crate::smtp::ErrorKind); } foreign_links { @@ -38,47 +36,7 @@ error_chain! { } } -pub fn uid_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("uid") - .help("Specifies the targetted message") - .value_name("UID") - .required(true) -} - -fn reply_all_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("reply-all") - .help("Includes all recipients") - .short("A") - .long("all") -} - -fn page_size_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("page-size") - .help("Page size") - .short("s") - .long("size") - .value_name("INT") -} - -fn page_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("page") - .help("Page number") - .short("p") - .long("page") - .value_name("INT") - .default_value("1") -} - -fn attachment_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("attachments") - .help("Adds attachment to the message") - .short("a") - .long("attachment") - .value_name("PATH") - .multiple(true) -} - -pub fn msg_subcmds<'a>() -> Vec> { +pub fn subcmds<'a>() -> Vec> { vec![ clap::SubCommand::with_name("list") .aliases(&["lst"]) @@ -151,11 +109,34 @@ pub fn msg_subcmds<'a>() -> Vec> { .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 { +pub fn matches(ctx: &Ctx) -> Result { 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 { ("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 `` 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> { + 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 { 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 { 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 { 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 Result { 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::>(); - 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 { 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::>(); + .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 { - 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::>(); - 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 { + 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 { 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 { fn msg_matches_move(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { 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 { fn msg_matches_delete(ctx: &Ctx, matches: &clap::ArgMatches) -> Result { 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 { .lines() .filter_map(|ln| ln.ok()) .map(|ln| ln.to_string()) - .collect::>() + .collect::>() .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 { 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 { + 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 = match matches.values_of("from") { + Some(from) => from.map(|arg| arg.to_string()).collect(), + None => msg.headers.from.clone(), + }; + + let to: Vec = 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> = matches + .values_of("cc") + .and_then(|cc| Some(cc.map(|arg| arg.to_string()).collect())); + + let bcc: Option> = 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>> = { + if let Some(matched_headers) = matches.values_of("header") { + let mut custom_headers: HashMap> = 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::>() + .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 { + 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 { + 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 { + 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 { + // 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) } diff --git a/src/msg/headers.rs b/src/msg/headers.rs new file mode 100644 index 00000000..13456cfe --- /dev/null +++ b/src/msg/headers.rs @@ -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 ")], +/// to: vec![String::from("To ")], +/// ..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, + pub to: Vec, + pub encoding: ContentTransferEncoding, + + // -- Optional fields -- + pub bcc: Option>, + pub cc: Option>, + pub custom_headers: Option>>, + pub in_reply_to: Option, + pub message_id: Option, + pub reply_to: Option>, + pub sender: Option, + pub signature: Option, + pub subject: Option, +} + +impl Headers { + /// This method works similiar to the [`Display Trait`] but it will only + /// convert the header into a string **without** the signature. + /// + /// # Example + /// + ///
+ /// + /// ``` + /// # use himalaya::msg::headers::Headers; + /// # use std::collections::HashMap; + /// # use lettre::message::header::ContentTransferEncoding; + /// # fn main() { + /// // our headers + /// let headers = Headers { + /// from: vec!["TornaxO7 ".to_string()], + /// to: vec!["Soywod ".to_string()], + /// encoding: ContentTransferEncoding::Base64, + /// bcc: Some(vec!["ThirdOne ".to_string()]), + /// cc: Some(vec!["CcAccount ".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 \n", + /// "To: Soywod \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 \n", + /// "Bcc: ThirdOne \n", + /// "Subject: Himalaya is cool\n", + /// ]; + /// + /// assert_eq!(headers_string, expected_output, + /// "{}, {}", + /// headers_string, expected_output); + /// # } + /// ``` + /// + ///
+ /// + /// [`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>> for Headers { + type Error = Error; + + fn try_from(envelope: Option<&imap_proto::types::Envelope<'_>>) -> Result { + 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 ")], +/// from: vec![String::from("TornaxO7 ")], +/// 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 \n", +/// "To: Soywod \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> { + 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 `. +/// +/// 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>>, +) -> Result>> { + if let Some(addresses) = addresses { + let mut parsed_addresses: Vec = Vec::new(); + + for address in addresses.iter() { + // This variable will hold the parsed version of the Address-struct, + // like this: + // + // "Name " + 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 " + 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, 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 , Soywod + // + // by a vector of email-addresses. + + // our msg addresses for the "Cc" header + let mail_addresses = vec![ + "TornaxO7 ".to_string(), + "Soywod ".to_string(), + ]; + + let cc_header = merge_addresses_to_one_line("Cc", &mail_addresses, ','); + + let expected_output = concat![ + "Cc: TornaxO7 ", + ",", + "Soywod \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 "), + String::from("Mailbox2@Host2"), + ]; + + if let Ok(converted) = convert_vec_address_to_string(Some(&addresses)) { + assert_eq!(converted, Some(expected_output)); + } else { + assert!(false); + } + } +} diff --git a/src/msg/mod.rs b/src/msg/mod.rs index 807eee09..7c77f6f2 100644 --- a/src/msg/mod.rs +++ b/src/msg/mod.rs @@ -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 ` where `` 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; diff --git a/src/msg/model.rs b/src/msg/model.rs index 8eb1fd7a..179f6319 100644 --- a/src/msg/model.rs +++ b/src/msg/model.rs @@ -1,672 +1,1556 @@ -use error_chain::error_chain; -use lettre; -use log::warn; -use mailparse::{self, MailHeaderMap}; -use rfc2047_decoder; -use serde::{ - ser::{self, SerializeStruct, Serializer}, - Serialize, -}; -use std::{borrow::Cow, fmt, fs, path::PathBuf, result}; -use tree_magic; -use unicode_width::UnicodeWidthStr; -use uuid::Uuid; +use super::attachment::Attachment; +use super::body::Body; +use super::headers::Headers; + +use log::debug; + +use imap::types::{Fetch, Flag, ZeroCopy}; + +use mailparse; use crate::{ - config::model::{Account, Config}, - flag::model::{Flag, Flags}, + ctx::Ctx, + flag::model::Flags, table::{Cell, Row, Table}, }; -error_chain! { +#[cfg(not(test))] +use crate::input; + +use serde::Serialize; + +use lettre::message::{ + header::ContentTransferEncoding, header::ContentType, Attachment as lettre_Attachment, Mailbox, + Message, MultiPart, SinglePart, +}; + +use std::{ + convert::{From, TryFrom}, + fmt, +}; + +use colorful::Colorful; + +// == Macros == +error_chain::error_chain! { + errors { + ParseBody (err: String) { + description("An error appeared, when trying to parse the body of the msg!"), + display("Couldn't get the body of the parsed msg: {}", err), + } + + /// Is mainly used in the "to_sendable_msg" function + Header(error_msg: String, header_name: &'static str, header_input: String) { + + description("An error happened, when trying to parse a header-field."), + display(concat![ + "[{}] {}\n", + "Header-Field-Name: '{}'\n", + "The word which let this error occur: '{}'"], + "Error".red(), + error_msg.clone().light_red(), + header_name.light_blue(), + header_input.clone().light_cyan()), + } + } + + links { + Attachment(super::attachment::Error, super::attachment::ErrorKind); + Headers(super::headers::Error, super::headers::ErrorKind); + Input(crate::input::Error, crate::input::ErrorKind); + } + foreign_links { - Mailparse(mailparse::MailParseError); + MailParse(mailparse::MailParseError); Lettre(lettre::error::Error); + LettreAddress(lettre::address::AddressError); + FromUtf8Error(std::string::FromUtf8Error); } } -// Template +// == Msg == +/// Represents the msg in a serializeable form with additional values. +/// This struct-type makes it also possible to print the msg in a serialized form or in a normal +/// form. +#[derive(Serialize, Clone, Debug, Eq, PartialEq)] +pub struct MsgSerialized { + /// First of all, the messge in general + #[serde(flatten)] + pub msg: Msg, -#[derive(Debug)] -pub struct Tpl(String); - -impl fmt::Display for Tpl { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Serialize for Tpl { - fn serialize(&self, serializer: S) -> result::Result - where - S: ser::Serializer, - { - let mut state = serializer.serialize_struct("Tpl", 1)?; - state.serialize_field("template", &self.0)?; - state.end() - } -} - -// Attachments - -#[derive(Debug)] -pub struct Attachment { - pub filename: String, - pub raw: Vec, -} - -impl<'a> Attachment { - // TODO: put in common with ReadableMsg - pub fn from_part(part: &'a mailparse::ParsedMail) -> Self { - Self { - filename: part - .get_content_disposition() - .params - .get("filename") - .unwrap_or(&Uuid::new_v4().to_simple().to_string()) - .to_owned(), - raw: part.get_body_raw().unwrap_or_default(), - } - } -} - -#[derive(Debug)] -pub struct Attachments(pub Vec); - -impl<'a> Attachments { - fn extract_from_part(&'a mut self, part: &'a mailparse::ParsedMail) { - if part.subparts.is_empty() { - let ctype = part - .get_headers() - .get_first_value("content-type") - .unwrap_or_default(); - if !ctype.starts_with("text") { - self.0.push(Attachment::from_part(part)); - } - } else { - part.subparts - .iter() - .for_each(|part| self.extract_from_part(part)); - } - } - - pub fn from_bytes(bytes: &[u8]) -> Result { - let msg = mailparse::parse_mail(bytes)?; - let mut attachments = Self(vec![]); - attachments.extract_from_part(&msg); - Ok(attachments) - } -} - -// Readable message - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ReadableMsg { - pub content: String, - #[serde(serialize_with = "bool_to_int")] + /// A bool which indicates if the current msg includes attachments or not. pub has_attachment: bool, + + /// The raw mail as a string + pub raw: String, } -impl fmt::Display for ReadableMsg { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "{}", self.content) - } -} +impl TryFrom<&Msg> for MsgSerialized { + type Error = Error; -impl<'a> ReadableMsg { - fn flatten_parts(part: &'a mailparse::ParsedMail) -> Vec<&'a mailparse::ParsedMail<'a>> { - if part.subparts.is_empty() { - vec![part] - } else { - part.subparts - .iter() - .flat_map(Self::flatten_parts) - .collect::>() - } - } - - pub fn from_bytes(mime: &str, bytes: &[u8]) -> Result { - let msg = mailparse::parse_mail(bytes)?; - let (text_part, html_part, has_attachment) = Self::flatten_parts(&msg).into_iter().fold( - (None, None, false), - |(mut text_part, mut html_part, mut has_attachment), part| { - let ctype = part - .get_headers() - .get_first_value("content-type") - .unwrap_or_default(); - - if text_part.is_none() && ctype.starts_with("text/plain") { - text_part = part.get_body().ok(); - } else { - if html_part.is_none() && ctype.starts_with("text/html") { - html_part = part.get_body().ok(); - } else { - has_attachment = true - }; - }; - - (text_part, html_part, has_attachment) - }, - ); - - let content = if mime == "text/plain" { - text_part.or(html_part).unwrap_or_default() - } else { - html_part.or(text_part).unwrap_or_default() - }; + fn try_from(msg: &Msg) -> Result { + let has_attachment = msg.attachments.is_empty(); + let raw = msg.get_raw_as_string()?; Ok(Self { - content, + msg: msg.clone(), has_attachment, + raw, }) } } -// Message - -#[derive(Debug)] -pub struct Msg<'m> { - pub uid: u32, - pub flags: Flags<'m>, - pub subject: String, - pub sender: String, - pub date: String, - pub attachments: Vec, - pub raw: Vec, -} - -impl<'a> Serialize for Msg<'a> { - fn serialize(&self, serializer: T) -> result::Result - where - T: ser::Serializer, - { - let mut state = serializer.serialize_struct("Msg", 7)?; - state.serialize_field("uid", &self.uid)?; - state.serialize_field("flags", &self.flags)?; - state.serialize_field("subject", &self.subject)?; - state.serialize_field( - "subject_len", - &UnicodeWidthStr::width(self.subject.as_str()), - )?; - state.serialize_field("sender", &self.sender)?; - state.serialize_field("sender_len", &UnicodeWidthStr::width(self.sender.as_str()))?; - state.serialize_field("date", &self.date)?; - state.end() +impl fmt::Display for MsgSerialized { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "{}", self.msg) } } -impl<'m> From> for Msg<'m> { - fn from(raw: Vec) -> Self { - Self { - uid: 0, - flags: Flags::new(&[]), - subject: String::from(""), - sender: String::from(""), - date: String::from(""), - attachments: vec![], - raw, +/// This struct represents a whole msg with its attachments, body-content +/// and its headers. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct Msg { + /// All added attachments are listed in this vector. + pub attachments: Vec, + + /// The flags of this msg. + pub flags: Flags, + + /// All information of the headers (sender, from, to and so on) + // headers: HashMap>, + pub headers: Headers, + + /// This variable stores the body of the msg. + /// This includes the general content text and the signature. + pub body: Body, + + /// The UID of the msg. In general, a message should already have one, unless you're writing a + /// new message, then we're generating it. + uid: Option, + + /// The origination date field. Read [the RFC here] here for more + /// information. + /// + /// [the RFC here]: + /// https://www.rfc-editor.org/rfc/rfc5322.html#section-3.6.1 + date: Option, + + /// The msg but in raw. + #[serde(skip_serializing)] + raw: Vec, +} + +impl Msg { + /// Creates a completely new msg where two header fields are set: + /// - [`from`] + /// - and [`signature`] + /// + /// [`from`]: struct.Headers.html#structfield.from + /// [`signature`]: struct.Headers.html#structfield.signature + /// + /// # Example + /// + ///
+ /// + /// ``` + /// # use himalaya::msg::model::Msg; + /// # use himalaya::msg::headers::Headers; + /// # use himalaya::config::model::Account; + /// # use himalaya::ctx::Ctx; + /// + /// # fn main() { + /// // -- Accounts -- + /// let ctx1 = Ctx { + /// account: Account::new_with_signature(Some("Soywod"), "clement.douin@posteo.net", + /// Some("Account Signature") + /// ), + /// .. Ctx::default() + /// }; + /// let ctx2 = Ctx { + /// account: Account::new(None, "tornax07@gmail.com"), + /// .. Ctx::default() + /// }; + /// + /// // Creating messages + /// let msg1 = Msg::new(&ctx1); + /// let msg2 = Msg::new(&ctx2); + /// + /// let expected_headers1 = Headers { + /// from: vec![String::from("Soywod ")], + /// // the signature of the account is stored as well + /// signature: Some(String::from("\n-- \nAccount Signature")), + /// ..Headers::default() + /// }; + /// + /// let expected_headers2 = Headers { + /// from: vec![String::from("tornax07@gmail.com")], + /// ..Headers::default() + /// }; + /// + /// assert_eq!(msg1.headers, expected_headers1, + /// "{:#?}, {:#?}", + /// msg1.headers, expected_headers1); + /// assert_eq!(msg2.headers, expected_headers2, + /// "{:#?}, {:#?}", + /// msg2.headers, expected_headers2); + /// # } + /// ``` + /// + ///
+ pub fn new(ctx: &Ctx) -> Self { + Self::new_with_headers(&ctx, Headers::default()) + } + + /// This function does the same as [`Msg::new`] but you can apply a custom + /// [`headers`] when calling the function instead of using the default one + /// from the [`Msg::new`] function. + /// + /// [`Msg::new`]: struct.Msg.html#method.new + /// [`headers`]: struct.Headers.html + pub fn new_with_headers(ctx: &Ctx, mut headers: Headers) -> Self { + if headers.from.is_empty() { + headers.from = vec![ctx.config.address(&ctx.account)]; } - } -} -impl<'m> From for Msg<'m> { - fn from(raw: String) -> Self { - Self::from(raw.as_bytes().to_vec()) - } -} - -impl<'m> From<&'m imap::types::Fetch> for Msg<'m> { - fn from(fetch: &'m imap::types::Fetch) -> Self { - match fetch.envelope() { - None => Self::from(fetch.body().unwrap_or_default().to_vec()), - Some(envelope) => Self { - uid: fetch.uid.unwrap_or_default(), - flags: Flags::new(fetch.flags()), - subject: envelope - .subject - .as_ref() - .and_then(|subj| rfc2047_decoder::decode(subj).ok()) - .unwrap_or_default(), - sender: envelope - .from - .as_ref() - .and_then(|addrs| addrs.first()) - .and_then(|addr| { - addr.name - .as_ref() - .and_then(|name| rfc2047_decoder::decode(name).ok()) - .or_else(|| { - let mbox = addr - .mailbox - .as_ref() - .and_then(|mbox| String::from_utf8(mbox.to_vec()).ok()) - .unwrap_or(String::from("unknown")); - let host = addr - .host - .as_ref() - .and_then(|host| String::from_utf8(host.to_vec()).ok()) - .unwrap_or(String::from("unknown")); - Some(format!("{}@{}", mbox, host)) - }) - }) - .unwrap_or(String::from("unknown")), - date: fetch - .internal_date() - .map(|date| date.naive_local().to_string()) - .unwrap_or_default(), - attachments: vec![], - raw: fetch.body().unwrap_or_default().to_vec(), - }, + if let None = headers.signature { + headers.signature = ctx.config.signature(&ctx.account); } - } -} -impl<'m> Msg<'m> { - pub fn parse(&'m self) -> Result> { - Ok(mailparse::parse_mail(&self.raw)?) - } - - pub fn to_vec(&self) -> Result> { - let parsed = self.parse()?; - let headers = parsed.get_headers().get_raw_bytes().to_vec(); - let sep = "\r\n".as_bytes().to_vec(); - let body = parsed.get_body()?.as_bytes().to_vec(); - - Ok(vec![headers, sep, body].concat()) - } - - pub fn to_sendable_msg(&self) -> Result { - use lettre::message::{ - header::*, - {Body, Message, MultiPart, SinglePart}, - }; - - let mut encoding = ContentTransferEncoding::Base64; - let parsed = self.parse()?; - let msg_builder = parsed.headers.iter().fold(Message::builder(), |msg, h| { - let value = String::from_utf8(h.get_value_raw().to_vec()) - .unwrap() - .replace("\r", ""); - - match h.get_key().to_lowercase().as_str() { - "in-reply-to" => msg.in_reply_to(value.parse().unwrap()), - "from" => match value.parse::() { - Ok(addr) => { - let msg_id = - format!("{}@{}", Uuid::new_v4().to_string(), addr.email.domain()); - msg.from(addr).message_id(Some(msg_id)) - } - Err(_) => msg, - }, - "to" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.to(addr), - Err(_) => msg, - }), - "cc" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.cc(addr), - Err(_) => msg, - }), - "bcc" => value - .split(",") - .fold(msg, |msg, addr| match addr.trim().parse() { - Ok(addr) => msg.bcc(addr), - Err(_) => msg, - }), - "subject" => msg.subject(value), - "content-transfer-encoding" => { - match value.to_lowercase().as_str() { - "8bit" => encoding = ContentTransferEncoding::EightBit, - "7bit" => encoding = ContentTransferEncoding::SevenBit, - "quoted-printable" => encoding = ContentTransferEncoding::QuotedPrintable, - "base64" => encoding = ContentTransferEncoding::Base64, - _ => warn!("unsupported encoding, default to base64"), - } - msg - } - _ => msg, - } + let body = Body::new_with_text(if let Some(sig) = headers.signature.as_ref() { + format!("\n{}", sig) + } else { + String::from("\n") }); - let text_part = SinglePart::builder() - .header(ContentType::TEXT_PLAIN) - .header(encoding) - .body(parsed.get_body_raw()?); - - let msg = if self.attachments.is_empty() { - msg_builder.singlepart(text_part) - } else { - let mut parts = MultiPart::mixed().singlepart(text_part); - - for attachment in &self.attachments { - let attachment_name = PathBuf::from(attachment); - let attachment_name = attachment_name - .file_name() - .map(|fname| fname.to_string_lossy()) - .unwrap_or(Cow::from(Uuid::new_v4().to_string())); - let attachment_content = fs::read(attachment) - .chain_err(|| format!("Could not read attachment `{}`", attachment))?; - let attachment_ctype = tree_magic::from_u8(&attachment_content); - - parts = parts.singlepart( - SinglePart::builder() - .content_type(attachment_ctype.parse().chain_err(|| { - format!("Could not parse content type `{}`", attachment_ctype) - })?) - .header(ContentDisposition::attachment(&attachment_name)) - .body(Body::new(attachment_content)), - ); - } - - msg_builder.multipart(parts) - }?; - - Ok(msg) - } - - pub fn extract_text_bodies_into( - part: &mailparse::ParsedMail, - mime: &str, - parts: &mut Vec, - ) { - match part.subparts.len() { - 0 => { - let content_type = part - .get_headers() - .get_first_value("content-type") - .unwrap_or_default(); - - if content_type.starts_with(mime) { - parts.push(part.get_body().unwrap_or_default()) - } - } - _ => { - part.subparts - .iter() - .for_each(|part| Self::extract_text_bodies_into(part, mime, parts)); - } + Self { + headers, + body, + ..Self::default() } } - fn extract_text_bodies(&self, mime: &str) -> Result> { - let mut parts = vec![]; - Self::extract_text_bodies_into(&self.parse()?, mime, &mut parts); - Ok(parts) - } + /// Converts the message into a Reply message. + /// An [`Account`] struct is needed to set the `From:` field. + /// + /// # Changes + /// The value on the left side, represents the header *after* the function + /// call, while the value on the right side shows the data *before* the + /// function call. So if we pick up the first example of `reply_all = + /// false`, then we can see, that the value of `ReplyTo:` is moved into the + /// `To:` header field in this function call. + /// + /// - `reply_all = false`: + /// - `To:` = `ReplyTo:` otherwise from `From:` + /// - attachments => cleared + /// - `From:` = Emailaddress of the current user account + /// - `Subject:` = "Re:" + `Subject` + /// - `in_reply_to` = Old Message ID + /// - `Cc:` = cleared + /// + /// - `reply_all = true`: + /// - `To:` = `ReplyTo:` + Addresses in `To:` + /// - `Cc:` = All CC-Addresses + /// - The rest: Same as in `reply_all = false` + /// + /// It'll add for each line in the body the `>` character in the beginning + /// of each line. + /// + /// # Example + /// [Here] you can see an example how a discussion with replies could look + /// like. + /// + /// [Here]: https://www.rfc-editor.org/rfc/rfc5322.html#page-46 + /// [`Account`]: struct.Account.html + /// + // TODO: References field is missing, but the imap-crate can't implement it + // currently. + pub fn change_to_reply(&mut self, ctx: &Ctx, reply_all: bool) -> Result<()> { + let subject = self + .headers + .subject + .as_ref() + .map(|sub| { + if sub.starts_with("Re:") { + sub.to_owned() + } else { + format!("Re: {}", sub) + } + }) + .unwrap_or_default(); - pub fn text_bodies(&self, mime: &str) -> Result { - let text_bodies = self.extract_text_bodies(mime)?; - Ok(text_bodies.join("\r\n")) - } + // The new fields + let mut to: Vec = Vec::new(); + let mut cc = None; - pub fn build_new_tpl(config: &Config, account: &Account) -> Result { - let msg_spec = MsgSpec { - in_reply_to: None, - to: None, - cc: None, - subject: None, - default_content: None, - }; - Msg::build_tpl(config, account, msg_spec) - } + if reply_all { + let email_addr: lettre::Address = ctx.account.email.parse()?; - pub fn build_reply_tpl(&self, config: &Config, account: &Account) -> Result { - let msg = &self.parse()?; - let headers = msg.get_headers(); - let to = headers - .get_first_value("reply-to") - .or(headers.get_first_value("from")); - let to = match to { - Some(t) => Some(vec![t]), - None => None, + for addr in self.headers.to.iter() { + let addr_parsed: Mailbox = addr.parse()?; + + // we don't want to receive the msg which we have just sent, + // don't we? + if addr_parsed.email != email_addr { + to.push(addr.to_string()); + } + } + + // Also use the addresses in the "Cc:" field + cc = self.headers.cc.clone(); + } + + // Now add the addresses in the `Reply-To:` Field or from the `From:` + // field. + if let Some(reply_to) = &self.headers.reply_to { + to.append(&mut reply_to.clone()); + } else { + // if the "Reply-To" wasn't set from the sender, then we're just + // replying to the addresses in the "From:" field + to.append(&mut self.headers.from.clone()); }; - let thread = self // Original msg prepend with ">" - .text_bodies("text/plain")? - .replace("\r", "") - .split("\n") - .map(|line| format!(">{}", line)) - .collect::>(); - - let msg_spec = MsgSpec { - in_reply_to: headers.get_first_value("message-id"), + let new_headers = Headers { + from: vec![ctx.config.address(&ctx.account)], to, - cc: None, - subject: headers - .get_first_value("subject") - .map(|s| format!("Re: {}", s)), - default_content: Some(thread), + cc, + subject: Some(subject), + in_reply_to: self.headers.message_id.clone(), + signature: ctx.config.signature(&ctx.account), + // and clear the rest of the fields + ..Headers::default() }; - Msg::build_tpl(config, account, msg_spec) + + // comment "out" the body of the msg, by adding the `>` characters to + // each line which includes a string. + let mut new_body = self + .body + .text + .clone() + .unwrap_or_default() + .lines() + .map(|line| { + let space = if line.starts_with(">") { "" } else { " " }; + format!(">{}{}", space, line) + }) + .collect::>() + .join("\n"); + + // also add the the signature in the end + if let Some(sig) = new_headers.signature.as_ref() { + new_body.push('\n'); + new_body.push_str(&sig) + } + + self.body = Body::new_with_text(new_body); + self.headers = new_headers; + self.attachments.clear(); + + Ok(()) } - pub fn build_reply_all_tpl(&self, config: &Config, account: &Account) -> Result { - let msg = &self.parse()?; - let headers = msg.get_headers(); + /// Changes the msg/msg to a forwarding msg/msg. + /// + /// # Changes + /// Calling this function will change apply the following to the current + /// message: + /// + /// - `Subject:`: `"Fwd: "` will be added in front of the "old" subject + /// - `"---------- Forwarded Message ----------"` will be added on top of + /// the body. + /// + /// # Example + /// ```text + /// Subject: Test subject + /// ... + /// + /// Hi, + /// I use Himalaya + /// + /// Sincerely + /// ``` + /// + /// will be changed to + /// + /// ```text + /// Subject: Fwd: Test subject + /// Sender: + /// ... + /// + /// > Hi, + /// > I use Himalaya + /// > + /// > Sincerely + /// ``` + pub fn change_to_forwarding(&mut self, ctx: &Ctx) { + // -- Header -- + let old_subject = self.headers.subject.clone().unwrap_or(String::new()); - // "To" header - // All addresses coming from original "To" … - let email: lettre::Address = account.email.parse().unwrap(); - let to = headers - .get_all_values("to") - .iter() - .flat_map(|addrs| addrs.split(",")) - .fold(vec![], |mut mboxes, addr| { - match addr.trim().parse::() { - Err(_) => mboxes, - Ok(mbox) => { - // … except current user's one (from config) … - if mbox.email != email { - mboxes.push(mbox.to_string()); - } - mboxes - } + self.headers = Headers { + subject: Some(format!("Fwd: {}", old_subject)), + sender: Some(ctx.config.address(&ctx.account)), + // and use the rest of the headers + ..self.headers.clone() + }; + + let mut body = String::new(); + + // -- Body -- + // apply a line which should indicate where the forwarded message begins + body.push_str(&format!( + "\n---------- Forwarded Message ----------\n{}", + self.body.text.clone().unwrap_or_default().replace("\r", ""), + )); + + if let Some(signature) = ctx.config.signature(&ctx.account) { + body.push('\n'); + body.push_str(&signature); + } + + self.body = Body::new_with_text(body); + } + + /// Returns the bytes of the *sendable message* of the struct! + pub fn into_bytes(&mut self) -> Result> { + // parse the whole msg first + let parsed = self.to_sendable_msg()?; + + return Ok(parsed.formatted()); + } + + /// Let the user edit the body of the msg. + /// + /// It'll enter the headers of the headers into the draft-file *if they're + /// not [`None`]!*. + /// + /// # Example + /// ```no_run + /// use himalaya::config::model::Account; + /// use himalaya::msg::model::Msg; + /// use himalaya::ctx::Ctx; + /// + /// fn main() { + /// let ctx = Ctx { + /// account: Account::new(Some("Name"), "some@msg.asdf"), + /// .. Ctx::default() + /// }; + /// let mut msg = Msg::new(&ctx); + /// + /// // In this case, only the header fields "From:" and "To:" are gonna + /// // be editable, because the other headers fields are set to "None" + /// // per default! + /// msg.edit_body().unwrap(); + /// } + /// ``` + /// + /// Now enable some headers: + /// + /// ```no_run + /// use himalaya::config::model::Account; + /// use himalaya::msg::{headers::Headers, model::Msg}; + /// use himalaya::ctx::Ctx; + /// + /// fn main() { + /// let ctx = Ctx { + /// account: Account::new(Some("Name"), "some@msg.asdf"), + /// .. Ctx::default() + /// }; + /// + /// let mut msg = Msg::new_with_headers( + /// &ctx, + /// Headers { + /// bcc: Some(Vec::new()), + /// cc: Some(Vec::new()), + /// ..Headers::default() + /// }, + /// ); + /// + /// // The "Bcc:" and "Cc:" header fields are gonna be editable as well + /// msg.edit_body().unwrap(); + /// } + /// ``` + /// + /// # Errors + /// In generel an error should appear if + /// - The draft or changes couldn't be saved + /// - The changed msg can't be parsed! (You wrote some things wrong...) + pub fn edit_body(&mut self) -> Result<()> { + // First of all, we need to create our template for the user. This + // means, that the header needs to be added as well! + let msg = self.to_string(); + + // We don't let this line compile, if we're doing + // tests, because we just need to look, if the headers are set + // correctly + #[cfg(not(test))] + let msg = input::open_editor_with_tpl(msg.as_bytes())?; + + // refresh the state of the msg + self.parse_from_str(&msg)?; + + Ok(()) + } + + /// Read the string of the argument `content` and store it's values into the + /// struct. It stores the headers-fields and the body of the msg. + /// + /// **Hint: The signature can't be fetched of the content at the moment!** + /// + /// # Example + /// ``` + /// use himalaya::config::model::Account; + /// use himalaya::msg::model::Msg; + /// use himalaya::ctx::Ctx; + /// + /// fn main() { + /// let content = concat![ + /// "Subject: Himalaya is nice\n", + /// "To: Soywod \n", + /// "From: TornaxO7 \n", + /// "Bcc: third_person@msg.com,rofl@yeet.com\n", + /// "\n", + /// "You should use himalaya, it's a nice program :D\n", + /// "\n", + /// "Sincerely\n", + /// ]; + /// + /// let ctx = Ctx { + /// account: Account::new(Some("Username"), "some@msg.com"), + /// .. Ctx::default() + /// }; + /// + /// // create the message + /// let mut msg = Msg::new(&ctx); + /// + /// // store the information given by the `content` variable which + /// // represents our current msg + /// msg.parse_from_str(content); + /// } + /// ``` + pub fn parse_from_str(&mut self, content: &str) -> Result<()> { + let parsed = mailparse::parse_mail(content.as_bytes()) + .chain_err(|| format!("How the message looks like currently:\n{}", self))?; + + self.headers = Headers::from(&parsed); + + match parsed.get_body() { + Ok(body) => self.body = Body::new_with_text(body), + Err(err) => return Err(ErrorKind::ParseBody(err.to_string()).into()), + }; + + Ok(()) + } + + /// Add an attachment to the msg from the local machine by the given path. + /// + /// # Example + /// ``` + /// use himalaya::config::model::Account; + /// use himalaya::msg::headers::Headers; + /// use himalaya::msg::model::Msg; + /// use himalaya::ctx::Ctx; + /// + /// fn main() { + /// let ctx = Ctx { + /// account: Account::new(Some("Name"), "address@msg.com"), + /// .. Ctx::default() + /// }; + /// let mut msg = Msg::new(&ctx); + /// + /// // suppose we have a Screenshot saved in our home directory + /// // Remember: Currently himalaya can't expand tilde ('~') and shell variables + /// msg.add_attachment("/home/bruh/Screenshot.png"); + /// } + /// ``` + /// + // THOUGHT: Error handling? + pub fn add_attachment(&mut self, path: &str) { + if let Ok(new_attachment) = Attachment::try_from(path) { + self.attachments.push(new_attachment); + } + } + + /// This function will use the information of the `Msg` struct and creates + /// a sendable msg with it. It uses the `Msg.headers` and + /// `Msg.attachments` fields for that. + /// + /// # Example + /// ```no_run + /// use himalaya::config::model::Account; + /// use himalaya::smtp; + /// + /// use himalaya::msg::{body::Body, headers::Headers, model::Msg}; + /// + /// use himalaya::imap::model::ImapConnector; + /// + /// use himalaya::ctx::Ctx; + /// + /// use imap::types::Flag; + /// + /// fn main() { + /// let ctx = Ctx { + /// account: Account::new(Some("Name"), "name@msg.net"), + /// .. Ctx::default() + /// }; + /// + /// let mut imap_conn = ImapConnector::new(&ctx.account).unwrap(); + /// let mut msg = Msg::new_with_headers( + /// &ctx, + /// Headers { + /// to: vec!["someone ".to_string()], + /// ..Headers::default() + /// }, + /// ); + /// + /// msg.body = Body::new_with_text("A little text."); + /// let sendable_msg = msg.to_sendable_msg().unwrap(); + /// + /// // now send the msg. Hint: Do the appropriate error handling here! + /// smtp::send(&ctx.account, &sendable_msg).unwrap(); + /// + /// // also say to the server of the account user, that we've just sent + /// // new message + /// msg.flags.insert(Flag::Seen); + /// imap_conn.append_msg("Sent", &mut msg).unwrap(); + /// + /// imap_conn.logout(); + /// } + /// ``` + pub fn to_sendable_msg(&mut self) -> Result { + // == Header of Msg == + // This variable will hold all information of our msg + let mut msg = Message::builder(); + + // -- Must-have-fields -- + // add "from" + for mailaddress in &self.headers.from { + msg = msg.from(match mailaddress.parse() { + Ok(from) => from, + Err(err) => { + return Err( + ErrorKind::Header(err.to_string(), "From", mailaddress.to_string()).into(), + ) } }); - // … and the ones coming from either "Reply-To" or "From" - let reply_to = headers - .get_all_values("reply-to") - .iter() - .flat_map(|addrs| addrs.split(",")) - .map(|addr| addr.trim().to_string()) - .collect::>(); - let reply_to = if reply_to.is_empty() { - headers - .get_all_values("from") - .iter() - .flat_map(|addrs| addrs.split(",")) - .map(|addr| addr.trim().to_string()) - .collect::>() + } + + // add "to" + for mailaddress in &self.headers.to { + msg = msg.to(match mailaddress.parse() { + Ok(to) => to, + Err(err) => { + return Err( + ErrorKind::Header(err.to_string(), "To", mailaddress.to_string()).into(), + ) + } + }); + } + + // -- Optional fields -- + // add "bcc" + if let Some(bcc) = &self.headers.bcc { + for mailaddress in bcc { + msg = msg.bcc(match mailaddress.parse() { + Ok(bcc) => bcc, + Err(err) => { + return Err(ErrorKind::Header( + err.to_string(), + "Bcc", + mailaddress.to_string(), + ) + .into()) + } + }); + } + } + + // add "cc" + if let Some(cc) = &self.headers.cc { + for mailaddress in cc { + msg = msg.cc(match mailaddress.parse() { + Ok(cc) => cc, + Err(err) => { + return Err(ErrorKind::Header( + err.to_string(), + "Cc", + mailaddress.to_string(), + ) + .into()) + } + }); + } + } + + // add "in_reply_to" + if let Some(in_reply_to) = &self.headers.in_reply_to { + msg = msg.in_reply_to(match in_reply_to.parse() { + Ok(in_reply_to) => in_reply_to, + Err(err) => { + return Err(ErrorKind::Header( + err.to_string(), + "In-Reply-To", + in_reply_to.to_string(), + ) + .into()) + } + }); + } + + // add message-id if it exists + msg = match self.headers.message_id.clone() { + Some(message_id) => msg.message_id(Some(message_id)), + None => { + // extract the domain like "gmail.com" + let mailbox: lettre::message::Mailbox = self.headers.from[0].parse()?; + let domain = mailbox.email.domain(); + + // generate a new UUID + let new_msg_id = format!("{}@{}", uuid::Uuid::new_v4().to_string(), domain); + + msg.message_id(Some(new_msg_id)) + } + }; + + // add "reply-to" + if let Some(reply_to) = &self.headers.reply_to { + for mailaddress in reply_to { + msg = msg.reply_to(match mailaddress.parse() { + Ok(reply_to) => reply_to, + Err(err) => { + return Err(ErrorKind::Header( + err.to_string(), + "Reply-to", + mailaddress.to_string(), + ) + .into()) + } + }); + } + } + + // add "sender" + if let Some(sender) = &self.headers.sender { + msg = msg.sender(match sender.parse() { + Ok(sender) => sender, + Err(err) => { + return Err( + ErrorKind::Header(err.to_string(), "Sender", sender.to_string()).into(), + ) + } + }); + } + + // add subject + if let Some(subject) = &self.headers.subject { + msg = msg.subject(subject); + } + + // -- Body + Attachments -- + // In this part, we'll add the content of the msg. This means the body + // and the attachments of the msg. + + // this variable will store all "sections" or attachments of the msg + let mut msg_parts = MultiPart::mixed().build(); + + // -- Body -- + if self.body.text.is_some() && self.body.html.is_some() { + msg_parts = msg_parts.multipart(MultiPart::alternative_plain_html( + self.body.text.clone().unwrap(), + self.body.html.clone().unwrap(), + )); } else { - reply_to - }; + let msg_body = SinglePart::builder() + .header(ContentType::TEXT_PLAIN) + .header(self.headers.encoding) + .body(self.body.text.clone().unwrap_or_default()); - // "Cc" header - let cc = Some( - headers - .get_all_values("cc") - .iter() - .flat_map(|addrs| addrs.split(",")) - .map(|addr| addr.trim().to_string()) - .collect::>(), - ); - - // Original msg prepend with ">" - let thread = self - .text_bodies("text/plain")? - .split("\r\n") - .map(|line| format!(">{}", line)) - .collect::>(); - - let msg_spec = MsgSpec { - in_reply_to: headers.get_first_value("message-id"), - cc, - to: Some(vec![reply_to, to].concat()), - subject: headers - .get_first_value("subject") - .map(|s| format!("Re: {}", s)), - default_content: Some(thread), - }; - Msg::build_tpl(config, account, msg_spec) - } - - pub fn build_forward_tpl(&self, config: &Config, account: &Account) -> Result { - let msg = &self.parse()?; - let headers = msg.get_headers(); - - let subject = format!( - "Fwd: {}", - headers - .get_first_value("subject") - .unwrap_or_else(String::new) - ); - let original_msg = vec![ - "-------- Forwarded Message --------".to_string(), - self.text_bodies("text/plain")?, - ]; - - let msg_spec = MsgSpec { - in_reply_to: None, - cc: None, - to: None, - subject: Some(subject), - default_content: Some(original_msg), - }; - Msg::build_tpl(config, account, msg_spec) - } - - fn add_from_header(tpl: &mut Vec, from: Option) { - tpl.push(format!("From: {}", from.unwrap_or_else(String::new))); - } - - fn add_in_reply_to_header(tpl: &mut Vec, in_reply_to: Option) { - if let Some(r) = in_reply_to { - tpl.push(format!("In-Reply-To: {}", r)); + msg_parts = msg_parts.singlepart(msg_body); } - } - fn add_cc_header(tpl: &mut Vec, cc: Option>) { - if let Some(c) = cc { - tpl.push(format!("Cc: {}", c.join(", "))); + // -- Attachments -- + for attachment in self.attachments.iter() { + let msg_attachment = lettre_Attachment::new(attachment.filename.clone()); + let msg_attachment = + msg_attachment.body(attachment.body_raw.clone(), attachment.content_type.clone()); + + msg_parts = msg_parts.singlepart(msg_attachment); } + + Ok(msg + .multipart(msg_parts) + // whenever an error appears, print out the messge as well to see what might be the + // error + .chain_err(|| format!("-- Current Message --\n{}", self))?) } - fn add_to_header(tpl: &mut Vec, to: Option>) { - tpl.push(format!( - "To: {}", - match to { - Some(t) => { - t.join(", ") - } - None => { - String::new() - } - } - )); + /// Returns the uid of the msg. + /// + /// # Hint + /// The uid is set if you *send* a *new* message or if you receive a message of the server. So + /// in general you can only get a `Some(...)` from this function, if it's a fetched msg + /// otherwise you'll get `None`. + pub fn get_uid(&self) -> Option { + self.uid } - fn add_subject_header(tpl: &mut Vec, subject: Option) { - tpl.push(format!("Subject: {}", subject.unwrap_or_else(String::new))); + /// It returns the raw version of the Message. In general it's the structure + /// how you get it if you get the data from the fetch. It's the output if + /// you read a message with the `--raw` flag like this: `himalaya read + /// --raw `. + pub fn get_raw(&self) -> Vec { + self.raw.clone() } - fn add_content(tpl: &mut Vec, content: Option>) { - if let Some(c) = content { - tpl.push(String::new()); // Separator between headers and body - tpl.extend(c); - } + /// Returns the raw mail as a string instead of a Vector of bytes. + pub fn get_raw_as_string(&self) -> Result { + let raw_message = String::from_utf8(self.raw.clone()).chain_err(|| { + format!( + "[{}]: Couldn't parse the raw message as string.", + "Error".red() + ) + })?; + + Ok(raw_message) } - fn add_signature(tpl: &mut Vec, config: &Config, account: &Account) { - if let Some(sig) = config.signature(&account) { - tpl.push(String::new()); - for line in sig.split("\n") { - tpl.push(line.to_string()); - } - } + /// Returns the [`ContentTransferEncoding`] of the body. + pub fn get_encoding(&self) -> ContentTransferEncoding { + self.headers.encoding } - fn build_tpl(config: &Config, account: &Account, msg_spec: MsgSpec) -> Result { - let mut tpl = vec![]; - Msg::add_from_header(&mut tpl, Some(config.address(account))); - Msg::add_in_reply_to_header(&mut tpl, msg_spec.in_reply_to); - Msg::add_cc_header(&mut tpl, msg_spec.cc); - Msg::add_to_header(&mut tpl, msg_spec.to); - Msg::add_subject_header(&mut tpl, msg_spec.subject); - Msg::add_content(&mut tpl, msg_spec.default_content); - Msg::add_signature(&mut tpl, config, account); - Ok(Tpl(tpl.join("\r\n"))) + /// Returns the whole message: Header + Body as a String + pub fn get_full_message(&self) -> String { + format!("{}\n{}", self.headers.get_header_as_string(), self.body) } } -struct MsgSpec { - in_reply_to: Option, - to: Option>, - cc: Option>, - subject: Option, - default_content: Option>, +// -- Traits -- +impl Default for Msg { + fn default() -> Self { + Self { + attachments: Vec::new(), + flags: Flags::default(), + headers: Headers::default(), + body: Body::default(), + // the uid is generated in the "to_sendable_msg" function if the server didn't apply a + // message id to it. + uid: None, + date: None, + raw: Vec::new(), + } + } } -impl<'m> Table for Msg<'m> { +impl fmt::Display for Msg { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!( + formatter, + "{}\n{}", + self.headers.get_header_as_string(), + self.body + ) + } +} + +impl Table for Msg { fn head() -> Row { Row::new() .cell(Cell::new("UID").bold().underline().white()) .cell(Cell::new("FLAGS").bold().underline().white()) .cell(Cell::new("SUBJECT").shrinkable().bold().underline().white()) - .cell(Cell::new("SENDER").bold().underline().white()) + .cell(Cell::new("FROM").bold().underline().white()) .cell(Cell::new("DATE").bold().underline().white()) } fn row(&self) -> Row { let is_seen = !self.flags.contains(&Flag::Seen); + + // The data which will be shown in the row + let uid = self.get_uid().unwrap_or(0); + let flags = self.flags.get_signs(); + let subject = self.headers.subject.clone().unwrap_or_default(); + let mut from = String::new(); + let date = self.date.clone().unwrap_or(String::new()); + + for from_addr in self.headers.from.iter() { + let mut address_iter = from_addr.split_ascii_whitespace(); + + if let Some(name) = address_iter.next() { + from.push_str(&format!("{}, ", name)); + } else if let Some(address) = address_iter.next() { + from.push_str(&format!("{}, ", address)); + } else { + from.push_str("UNKNWON"); + } + } + + // remove trailing whitespace + the ',' + let mut from = from.trim_end().to_string(); + from.pop(); + Row::new() - .cell(Cell::new(&self.uid.to_string()).bold_if(is_seen).red()) - .cell(Cell::new(&self.flags.to_string()).bold_if(is_seen).white()) - .cell( - Cell::new(&self.subject) - .shrinkable() - .bold_if(is_seen) - .green(), - ) - .cell(Cell::new(&self.sender).bold_if(is_seen).blue()) - .cell(Cell::new(&self.date).bold_if(is_seen).yellow()) + .cell(Cell::new(&uid.to_string()).bold_if(is_seen).red()) + .cell(Cell::new(&flags).bold_if(is_seen).white()) + .cell(Cell::new(&subject).shrinkable().bold_if(is_seen).green()) + .cell(Cell::new(&from).bold_if(is_seen).blue()) + .cell(Cell::new(&date).bold_if(is_seen).yellow()) } } -// Msgs +// -- From's -- +/// Load the data from a fetched msg and store them in the msg-struct. +/// Please make sure that the fetch includes the following query: +/// +/// - UID (optional) +/// - FLAGS (optional) +/// - ENVELOPE (optional) +/// - INTERNALDATE +/// - BODY[] (optional) +impl TryFrom<&Fetch> for Msg { + type Error = Error; + fn try_from(fetch: &Fetch) -> Result { + // -- Preparations -- + // We're preparing the variables first, which will hold the data of the + // fetched msg. + + let mut attachments = Vec::new(); + let flags = Flags::from(fetch.flags()); + let headers = Headers::try_from(fetch.envelope())?; + let uid = fetch.uid; + + let date = fetch + .internal_date() + .map(|date| date.naive_local().to_string()); + + let raw = match fetch.body() { + Some(body) => body.to_vec(), + None => Vec::new(), + }; + + // Get the content of the msg. Here we have to look (important!) if + // the fetch even includes a body or not, since the `BODY[]` query is + // only *optional*! + let parsed = + // the empty array represents an invalid body, so we can enter the + // `Err` arm if the body-query wasn't applied + match mailparse::parse_mail(raw.as_slice()) { + Ok(parsed) => { + debug!("Fetch has a body to parse."); + Some(parsed) + }, + Err(_) => { + debug!("Fetch hasn't a body to parse."); + None + }, + }; + + // -- Storing the information (body) -- + let mut body = Body::new(); + if let Some(parsed) = parsed { + // Ok, so some mails have their mody wrapped in a multipart, some + // don't. This condition hits, if the body isn't in a multipart, so we can + // immediately fetch the body from the first part of the mail. + match parsed.ctype.mimetype.as_ref() { + "text/plain" => body.text = parsed.get_body().ok(), + "text/html" => body.html = parsed.get_body().ok(), + _ => (), + }; + + for subpart in &parsed.subparts { + // now it might happen, that the body is *in* a multipart, if + // that's the case, look, if we've already applied a body + // (body.is_empty()) and set it, if needed + if body.text.is_none() && subpart.ctype.mimetype == "text/plain" { + body.text = subpart.get_body().ok(); + } else if body.html.is_none() && subpart.ctype.mimetype == "text/html" { + body.html = subpart.get_body().ok(); + } + // otherise it's a normal attachment, like a PNG file or + // something like that + else if let Some(attachment) = Attachment::from_parsed_mail(subpart) { + attachments.push(attachment); + } + // this shouldn't happen, since this would mean, that's neither an attachment nor + // the body of the mail but something else. Log that! + else { + println!( + "[{}] Unknown attachment with the following mime-type: {}\n", + "Warning".yellow(), + subpart.ctype.mimetype, + ); + } + } + } + + Ok(Self { + attachments, + flags, + headers, + body: Body::new_with_text(body), + uid, + date, + raw, + }) + } +} + +impl TryFrom<&str> for Msg { + type Error = Error; + + fn try_from(content: &str) -> Result { + let mut msg = Msg::default(); + msg.parse_from_str(content)?; + + Ok(msg) + } +} + +// == Msgs == +/// A Type-Safety struct which stores a vector of Messages. #[derive(Debug, Serialize)] -pub struct Msgs<'a>(pub Vec>); +pub struct Msgs(pub Vec); -impl<'a> From<&'a imap::types::ZeroCopy>> for Msgs<'a> { - fn from(fetches: &'a imap::types::ZeroCopy>) -> Self { - Self(fetches.iter().rev().map(Msg::from).collect::>()) - } -} - -impl Msgs<'_> { +impl Msgs { pub fn new() -> Self { - Self(vec![]) + Self(Vec::new()) } } -impl fmt::Display for Msgs<'_> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "\n{}", Table::render(&self.0)) +// -- From's -- +impl<'mails> TryFrom<&'mails ZeroCopy>> for Msgs { + type Error = Error; + + fn try_from(fetches: &'mails ZeroCopy>) -> Result { + // the content of the Msgs-struct + let mut mails = Vec::new(); + + for fetch in fetches.iter().rev() { + mails.push(Msg::try_from(fetch)?); + } + + Ok(Self(mails)) } } -// Custom bool to int serializer - -fn bool_to_int(t: &bool, s: S) -> std::result::Result -where - S: Serializer, -{ - match t { - true => s.serialize_u8(1), - false => s.serialize_u8(0), +// -- Traits -- +impl fmt::Display for Msgs { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + writeln!(formatter, "\n{}", Table::render(&self.0)) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + config::model::{Account, Config}, + ctx::Ctx, + msg::{body::Body, headers::Headers, model::Msg}, + }; + + #[test] + fn test_new() { + let ctx = Ctx { + account: Account::new_with_signature(None, "test@mail.com", None), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + ..Ctx::default() + }; + + let msg = Msg::new(&ctx); + let expected_headers = Headers { + from: vec![String::from("Config Name ")], + ..Headers::default() + }; + + assert_eq!( + msg.headers, expected_headers, + "{:#?}, {:#?}", + msg.headers, expected_headers + ); + assert!(msg.get_raw_as_string().unwrap().is_empty()); + } + + #[test] + fn test_new_with_account_name() { + let ctx = Ctx { + account: Account::new_with_signature(Some("Account Name"), "test@mail.com", None), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let msg = Msg::new(&ctx); + let expected_headers = Headers { + from: vec![String::from("Account Name ")], + ..Headers::default() + }; + + assert_eq!( + msg.headers, expected_headers, + "{:#?}, {:#?}", + msg.headers, expected_headers + ); + assert!(msg.get_raw_as_string().unwrap().is_empty()); + } + + #[test] + fn test_new_with_headers() { + let ctx = Ctx { + account: Account::new(Some("Account Name"), "test@mail.com"), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let msg_with_custom_from = Msg::new_with_headers( + &ctx, + Headers { + from: vec![String::from("Account Name ")], + ..Headers::default() + }, + ); + let expected_with_custom_from = Msg { + headers: Headers { + // the Msg::new_with_headers function should use the from + // address in the headers struct, not the from address of the + // account + from: vec![String::from("Account Name ")], + ..Headers::default() + }, + // The signature should be added automatically + body: Body::new_with_text("\n"), + ..Msg::default() + }; + + assert_eq!( + msg_with_custom_from, expected_with_custom_from, + "Left: {:#?}, Right: {:#?}", + msg_with_custom_from, expected_with_custom_from + ); + } + + #[test] + fn test_new_with_headers_and_signature() { + let ctx = Ctx { + account: Account::new_with_signature( + Some("Account Name"), + "test@mail.com", + Some("Signature"), + ), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let msg_with_custom_signature = Msg::new_with_headers(&ctx, Headers::default()); + + let expected_with_custom_signature = Msg { + headers: Headers { + from: vec![String::from("Account Name ")], + signature: Some(String::from("\n-- \nSignature")), + ..Headers::default() + }, + body: Body::new_with_text("\n\n-- \nSignature"), + ..Msg::default() + }; + + assert_eq!( + msg_with_custom_signature, + expected_with_custom_signature, + "Left: {:?}, Right: {:?}", + dbg!(&msg_with_custom_signature), + dbg!(&expected_with_custom_signature) + ); + } + + #[test] + fn test_change_to_reply() { + // in this test, we are gonna reproduce the same situation as shown + // here: https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2 + + // == Preparations == + // -- rfc test -- + // accounts for the rfc test + let config = Config { + name: String::from("Config Name"), + ..Config::default() + }; + + let john_doe = Ctx { + account: Account::new(Some("John Doe"), "jdoe@machine.example"), + config: config.clone(), + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let mary_smith = Ctx { + account: Account::new(Some("Mary Smith"), "mary@example.net"), + config: config.clone(), + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let msg_rfc_test = Msg { + headers: Headers { + from: vec!["John Doe ".to_string()], + to: vec!["Mary Smith ".to_string()], + subject: Some("Saying Hello".to_string()), + message_id: Some("<1234@local.machine.example>".to_string()), + ..Headers::default() + }, + body: Body::new_with_text(concat![ + "This is a message just to say hello.\n", + "So, \"Hello\".", + ]), + ..Msg::default() + }; + + // -- for general tests -- + let ctx = Ctx { + account: Account::new(Some("Name"), "some@address.asdf"), + config: config, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + // -- for reply_all -- + // a custom test to look what happens, if we want to reply to all addresses. + // Take a look into the doc of the "change_to_reply" what should happen, if we + // set "reply_all" to "true". + let mut msg_reply_all = Msg { + headers: Headers { + from: vec!["Boss ".to_string()], + to: vec![ + "msg@1.asdf".to_string(), + "msg@2.asdf".to_string(), + "Name ".to_string(), + ], + cc: Some(vec![ + "test@testing".to_string(), + "test2@testing".to_string(), + ]), + message_id: Some("RandomID123".to_string()), + reply_to: Some(vec!["Reply@Mail.rofl".to_string()]), + subject: Some("Have you heard of himalaya?".to_string()), + ..Headers::default() + }, + body: Body::new_with_text(concat!["A body test\n", "\n", "Sincerely",]), + ..Msg::default() + }; + + // == Expected output(s) == + // -- rfc test -- + // the first step + let expected_rfc1 = Msg { + headers: Headers { + from: vec!["Mary Smith ".to_string()], + to: vec!["John Doe ".to_string()], + reply_to: Some(vec![ + "\"Mary Smith: Personal Account\" ".to_string(), + ]), + subject: Some("Re: Saying Hello".to_string()), + message_id: Some("<3456@example.net>".to_string()), + in_reply_to: Some("<1234@local.machine.example>".to_string()), + ..Headers::default() + }, + body: Body::new_with_text(concat![ + "> This is a message just to say hello.\n", + "> So, \"Hello\".", + ]), + ..Msg::default() + }; + + // then the response the the first respone above + let expected_rfc2 = Msg { + headers: Headers { + to: vec!["\"Mary Smith: Personal Account\" ".to_string()], + from: vec!["John Doe ".to_string()], + subject: Some("Re: Saying Hello".to_string()), + message_id: Some("".to_string()), + in_reply_to: Some("<3456@example.net>".to_string()), + ..Headers::default() + }, + body: Body::new_with_text(concat![ + ">> This is a message just to say hello.\n", + ">> So, \"Hello\".", + ]), + ..Msg::default() + }; + + // -- reply all -- + let expected_reply_all = Msg { + headers: Headers { + from: vec!["Name ".to_string()], + to: vec![ + "msg@1.asdf".to_string(), + "msg@2.asdf".to_string(), + "Reply@Mail.rofl".to_string(), + ], + cc: Some(vec![ + "test@testing".to_string(), + "test2@testing".to_string(), + ]), + in_reply_to: Some("RandomID123".to_string()), + subject: Some("Re: Have you heard of himalaya?".to_string()), + ..Headers::default() + }, + body: Body::new_with_text(concat!["> A body test\n", "> \n", "> Sincerely"]), + ..Msg::default() + }; + + // == Testing == + // -- rfc test -- + // represents the message for the first reply + let mut rfc_reply_1 = msg_rfc_test.clone(); + rfc_reply_1.change_to_reply(&mary_smith, false).unwrap(); + + // the user would enter this normally + rfc_reply_1.headers = Headers { + message_id: Some("<3456@example.net>".to_string()), + reply_to: Some(vec![ + "\"Mary Smith: Personal Account\" ".to_string(), + ]), + ..rfc_reply_1.headers.clone() + }; + + // represents the message for the reply to the reply + let mut rfc_reply_2 = rfc_reply_1.clone(); + rfc_reply_2.change_to_reply(&john_doe, false).unwrap(); + rfc_reply_2.headers = Headers { + message_id: Some("".to_string()), + ..rfc_reply_2.headers.clone() + }; + + assert_eq!( + rfc_reply_1, + expected_rfc1, + "Left: {:?}, Right: {:?}", + dbg!(&rfc_reply_1), + dbg!(&expected_rfc1) + ); + + assert_eq!( + rfc_reply_2, + expected_rfc2, + "Left: {:?}, Right: {:?}", + dbg!(&rfc_reply_2), + dbg!(&expected_rfc2) + ); + + // -- custom tests -— + msg_reply_all.change_to_reply(&ctx, true).unwrap(); + assert_eq!( + msg_reply_all, + expected_reply_all, + "Left: {:?}, Right: {:?}", + dbg!(&msg_reply_all), + dbg!(&expected_reply_all) + ); + } + + #[test] + fn test_change_to_forwarding() { + // == Preparations == + let ctx = Ctx { + account: Account::new_with_signature(Some("Name"), "some@address.asdf", Some("lol")), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let mut msg = Msg::new_with_headers( + &ctx, + Headers { + from: vec![String::from("ThirdPerson ")], + subject: Some(String::from("Test subject")), + ..Headers::default() + }, + ); + + msg.body = Body::new_with_text(concat!["The body text, nice!\n", "Himalaya is nice!",]); + + // == Expected Results == + let expected_msg = Msg { + headers: Headers { + from: vec![String::from("ThirdPerson ")], + sender: Some(String::from("Name ")), + signature: Some(String::from("\n-- \nlol")), + subject: Some(String::from("Fwd: Test subject")), + ..Headers::default() + }, + body: Body::new_with_text(concat![ + "\n", + "---------- Forwarded Message ----------\n", + "The body text, nice!\n", + "Himalaya is nice!\n", + "\n-- \nlol" + ]), + ..Msg::default() + }; + + // == Tests == + msg.change_to_forwarding(&ctx); + assert_eq!( + msg, + expected_msg, + "Left: {:?}, Right: {:?}", + dbg!(&msg), + dbg!(&expected_msg) + ); + } + + #[test] + fn test_edit_body() { + // == Preparations == + let ctx = Ctx { + account: Account::new_with_signature(Some("Name"), "some@address.asdf", None), + ..Ctx::default() + }; + + let mut msg = Msg::new_with_headers( + &ctx, + Headers { + bcc: Some(vec![String::from("bcc ")]), + cc: Some(vec![String::from("cc ")]), + subject: Some(String::from("Subject")), + ..Headers::default() + }, + ); + + // == Expected Results == + let expected_msg = Msg { + headers: Headers { + from: vec![String::from("Name ")], + to: vec![String::new()], + // these fields should exist now + subject: Some(String::from("Subject")), + bcc: Some(vec![String::from("bcc ")]), + cc: Some(vec![String::from("cc ")]), + ..Headers::default() + }, + body: Body::new_with_text("\n"), + ..Msg::default() + }; + + // == Tests == + msg.edit_body().unwrap(); + + assert_eq!( + msg, expected_msg, + "Left: {:#?}, Right: {:#?}", + msg, expected_msg + ); + } + + #[test] + fn test_parse_from_str() { + use std::collections::HashMap; + + // == Preparations == + let ctx = Ctx { + account: Account::new_with_signature(Some("Name"), "some@address.asdf", None), + config: Config { + name: String::from("Config Name"), + ..Config::default() + }, + mbox: String::from("INBOX"), + ..Ctx::default() + }; + + let msg_template = Msg::new(&ctx); + + let normal_content = concat![ + "From: Some \n", + "Subject: Awesome Subject\n", + "Bcc: mail1@rofl.lol,name \n", + "To: To \n", + "\n", + "Account Signature\n", + ]; + + let content_with_custom_headers = concat![ + "From: Some \n", + "Subject: Awesome Subject\n", + "Bcc: mail1@rofl.lol,name \n", + "To: To \n", + "CustomHeader1: Value1\n", + "CustomHeader2: Value2\n", + "\n", + "Account Signature\n", + ]; + + // == Expected outputs == + let expect = Msg { + headers: Headers { + from: vec![String::from("Some ")], + subject: Some(String::from("Awesome Subject")), + bcc: Some(vec![ + String::from("name "), + String::from("mail1@rofl.lol"), + ]), + to: vec![String::from("To ")], + ..Headers::default() + }, + body: Body::new_with_text("Account Signature\n"), + ..Msg::default() + }; + + // -- with custom headers -- + let mut custom_headers: HashMap> = HashMap::new(); + custom_headers.insert("CustomHeader1".to_string(), vec!["Value1".to_string()]); + custom_headers.insert("CustomHeader2".to_string(), vec!["Value2".to_string()]); + + let expect_custom_header = Msg { + headers: Headers { + from: vec![String::from("Some ")], + subject: Some(String::from("Awesome Subject")), + bcc: Some(vec![ + String::from("name "), + String::from("mail1@rofl.lol"), + ]), + to: vec![String::from("To ")], + custom_headers: Some(custom_headers), + ..Headers::default() + }, + body: Body::new_with_text("Account Signature\n"), + ..Msg::default() + }; + + // == Testing == + let mut msg1 = msg_template.clone(); + let mut msg2 = msg_template.clone(); + + msg1.parse_from_str(normal_content).unwrap(); + msg2.parse_from_str(content_with_custom_headers).unwrap(); + + assert_eq!( + msg1, + expect, + "Left: {:?}, Right: {:?}", + dbg!(&msg1), + dbg!(&expect) + ); + + assert_eq!( + msg2, + expect_custom_header, + "Left: {:?}, Right: {:?}", + dbg!(&msg2), + dbg!(&expect_custom_header) + ); } } diff --git a/src/msg/tpl/cli.rs b/src/msg/tpl/cli.rs deleted file mode 100644 index 780613dc..00000000 --- a/src/msg/tpl/cli.rs +++ /dev/null @@ -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> { - 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 { - 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::>().join(", ")); - } - - let addrs = matches.values_of("cc").unwrap_or_default(); - if addrs.len() > 0 { - debug!("overriden cc: {:?}", addrs); - tpl.header("Cc", addrs.collect::>().join(", ")); - } - - let addrs = matches.values_of("bcc").unwrap_or_default(); - if addrs.len() > 0 { - debug!("overriden bcc: {:?}", addrs); - tpl.header("Bcc", addrs.collect::>().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::>() - .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 { - 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 { - 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 { - 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) -} diff --git a/src/msg/tpl/mod.rs b/src/msg/tpl/mod.rs deleted file mode 100644 index 92256774..00000000 --- a/src/msg/tpl/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod cli; -pub mod model; diff --git a/src/msg/tpl/model.rs b/src/msg/tpl/model.rs deleted file mode 100644 index ea1ba7c8..00000000 --- a/src/msg/tpl/model.rs +++ /dev/null @@ -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, - body: Option, - signature: Option, - 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::>() - .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::() { - 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::>(); - 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::>() - } 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::>() - .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(&mut self, key: K, val: V) -> &Self { - self.headers.insert(key.to_string(), val.to_string()); - self - } - - pub fn body(&mut self, body: T) -> &Self { - self.body = Some(body.to_string()); - self - } - - pub fn signature(&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 \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 \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 \r\nSubject: Test\r\n\r\nHello, world!", - ) - .unwrap(); - let tpl = Tpl::reply(&ctx, &parsed_mail); - - assert_eq!( - "From: Test \nTo: Sender \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 \r\nSubject: Test\r\n\r\nHello, world!", - ) - .unwrap(); - let tpl = Tpl::reply(&ctx, &parsed_mail); - - assert_eq!( - "From: Test \nTo: Sender \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 \r -To: To ,to_bis@localhost\r -Cc: Cc , cc_bis@localhost\r -Subject: Test\r -\r -Hello, world!", - ) - .unwrap(); - let tpl = Tpl::reply_all(&ctx, &parsed_mail); - - assert_eq!( - "From: To -To: From , to_bis@localhost -In-Reply-To: 1 -Cc: Cc , 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 \r\nSubject: Test\r\n\r\nHello, world!", - ) - .unwrap(); - let tpl = Tpl::reply(&ctx, &parsed_mail); - - assert_eq!( - "From: Test \nTo: Sender \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 \r\nSubject: Test\r\n\r\nHello, world!", - ) - .unwrap(); - let tpl = Tpl::forward(&ctx, &parsed_mail); - - assert_eq!( - "From: Test \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 \r\nSubject: Test\r\n\r\nHello, world!", - ) - .unwrap(); - let tpl = Tpl::forward(&ctx, &parsed_mail); - - assert_eq!( - "From: Test \nTo: \nSubject: Fwd: Test\n\n-------- Forwarded Message --------\nHello, world!\n\n-- \nCordialement,", - tpl.to_string() - ); - } -} diff --git a/src/output/model.rs b/src/output/model.rs index 13b9f6c7..50cae8b7 100644 --- a/src/output/model.rs +++ b/src/output/model.rs @@ -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 { response: T, } @@ -43,17 +43,20 @@ impl OutputJson { } // 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(&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 } diff --git a/tests/imap_test.rs b/tests/imap_test.rs index ce822931..b54fcbb5 100644 --- a/tests/imap_test.rs +++ b/tests/imap_test.rs @@ -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\" ".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 ".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 "); // 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(); } diff --git a/wiki b/wiki deleted file mode 160000 index 51bc2d44..00000000 --- a/wiki +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 51bc2d44022ed9c4695a2d6c15f2187d203e22b7