improve folder struct + msg management (#217)

* make imap list and search return msg instead of fetch

* move imap logouts to main fn

* improve list command

* improve search command

* improve flags command

* improve template reply

* improve tpl forward command

* refactor tpl and msg reply/forward

* refactor copy, move and write commands

* refactor attachment command

* fix attachment part of copy and move commands

* fix send, save, read and mbox

* put back notify and watch commands

* fix msg encoding

* refactor edit choices, clean dead code

* fix attachment for write, reply and forward commands

* refactor config mod struct

* refactor project folder struct

* fix vim plugin (#215)
This commit is contained in:
Clément DOUIN 2021-10-10 22:58:57 +02:00 committed by GitHub
parent 794860befe
commit b7d068c729
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 3100 additions and 4260 deletions

368
Cargo.lock generated
View file

@ -4,11 +4,26 @@ version = 3
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [ dependencies = [
"memchr 2.3.4", "memchr 2.4.1",
]
[[package]]
name = "ammonia"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e445c26125ff80316eaea16e812d717b147b82a68682bd4730f74d4845c8b35"
dependencies = [
"html5ever",
"lazy_static",
"maplit",
"markup5ever_rcdom",
"matches",
"tendril",
"url",
] ]
[[package]] [[package]]
@ -90,9 +105,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.69" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -263,6 +278,16 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
[[package]]
name = "futf"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
dependencies = [
"mac",
"new_debug_unreachable",
]
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.17" version = "0.3.17"
@ -291,12 +316,23 @@ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-task", "futures-task",
"memchr 2.3.4", "memchr 2.4.1",
"pin-project-lite", "pin-project-lite",
"pin-utils", "pin-utils",
"slab", "slab",
] ]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]] [[package]]
name = "getrandom" name = "getrandom"
version = "0.2.3" version = "0.2.3"
@ -305,7 +341,7 @@ checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"wasi", "wasi 0.10.0+wasi-snapshot-preview1",
] ]
[[package]] [[package]]
@ -327,17 +363,20 @@ dependencies = [
name = "himalaya" name = "himalaya"
version = "0.4.0" version = "0.4.0"
dependencies = [ dependencies = [
"ammonia",
"anyhow", "anyhow",
"atty", "atty",
"chrono", "chrono",
"clap", "clap",
"env_logger", "env_logger",
"htmlescape",
"imap", "imap",
"imap-proto", "imap-proto",
"lettre", "lettre",
"log", "log",
"mailparse", "mailparse",
"native-tls", "native-tls",
"regex",
"rfc2047-decoder", "rfc2047-decoder",
"serde", "serde",
"serde_json", "serde_json",
@ -361,6 +400,26 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "html5ever"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b"
dependencies = [
"log",
"mac",
"markup5ever",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "htmlescape"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163"
[[package]] [[package]]
name = "httpdate" name = "httpdate"
version = "1.0.1" version = "1.0.1"
@ -396,7 +455,7 @@ dependencies = [
"imap-proto", "imap-proto",
"lazy_static", "lazy_static",
"native-tls", "native-tls",
"nom 6.2.1", "nom 6.1.2",
"regex", "regex",
] ]
@ -406,7 +465,7 @@ version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb" checksum = "3ad9b46a79efb6078e578ae04e51463d7c3e8767864687f7e63095b3cbefafbb"
dependencies = [ dependencies = [
"nom 6.2.1", "nom 6.1.2",
] ]
[[package]] [[package]]
@ -421,9 +480,9 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.10" version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
@ -463,9 +522,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.101" version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -494,6 +553,12 @@ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
] ]
[[package]]
name = "mac"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "mailparse" name = "mailparse"
version = "0.13.6" version = "0.13.6"
@ -505,6 +570,38 @@ dependencies = [
"quoted_printable", "quoted_printable",
] ]
[[package]]
name = "maplit"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
[[package]]
name = "markup5ever"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd"
dependencies = [
"log",
"phf",
"phf_codegen",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b"
dependencies = [
"html5ever",
"markup5ever",
"tendril",
"xml5ever",
]
[[package]] [[package]]
name = "match_cfg" name = "match_cfg"
version = "0.1.0" version = "0.1.0"
@ -528,9 +625,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.3.4" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]] [[package]]
name = "mime" name = "mime"
@ -540,9 +637,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]] [[package]]
name = "minimal-lexical" name = "minimal-lexical"
version = "0.1.2" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6595bb28ed34f43c3fe088e48f6cfb2e033cab45f25a5384d5fdf564fbc8c4b2" checksum = "9c64630dcdd71f1a64c435f54885086a0de5d6a12d104d69b165fb7d5286d677"
[[package]] [[package]]
name = "native-tls" name = "native-tls"
@ -562,6 +659,12 @@ dependencies = [
"tempfile", "tempfile",
] ]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]] [[package]]
name = "nom" name = "nom"
version = "3.2.1" version = "3.2.1"
@ -573,13 +676,13 @@ dependencies = [
[[package]] [[package]]
name = "nom" name = "nom"
version = "6.2.1" version = "6.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c5c51b9083a3c620fa67a2a635d1ce7d95b897e957d6b28ff9a5da960a103a6" checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2"
dependencies = [ dependencies = [
"bitvec", "bitvec",
"funty", "funty",
"memchr 2.3.4", "memchr 2.4.1",
"version_check", "version_check",
] ]
@ -589,7 +692,7 @@ version = "7.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1" checksum = "7ffd9d26838a953b4af82cbeb9f1592c6798916983959be223a7124e992742c1"
dependencies = [ dependencies = [
"memchr 2.3.4", "memchr 2.4.1",
"minimal-lexical", "minimal-lexical",
"version_check", "version_check",
] ]
@ -641,9 +744,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a"
[[package]] [[package]]
name = "openssl-sys" name = "openssl-sys"
version = "0.9.66" version = "0.9.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"cc", "cc",
@ -717,6 +820,44 @@ dependencies = [
"indexmap", "indexmap",
] ]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
dependencies = [
"phf_generator",
"phf_shared",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
dependencies = [
"phf_shared",
"rand 0.7.3",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.7" version = "0.2.7"
@ -731,9 +872,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.19" version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb"
[[package]] [[package]]
name = "ppv-lite86" name = "ppv-lite86"
@ -741,6 +882,12 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.29" version = "1.0.29"
@ -752,9 +899,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -782,6 +929,20 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc 0.2.0",
"rand_pcg",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.4" version = "0.8.4"
@ -789,9 +950,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [ dependencies = [
"libc", "libc",
"rand_chacha", "rand_chacha 0.3.1",
"rand_core", "rand_core 0.6.3",
"rand_hc", "rand_hc 0.3.1",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
] ]
[[package]] [[package]]
@ -801,7 +972,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [ dependencies = [
"ppv-lite86", "ppv-lite86",
"rand_core", "rand_core 0.6.3",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
] ]
[[package]] [[package]]
@ -810,7 +990,16 @@ version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.3",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
] ]
[[package]] [[package]]
@ -819,7 +1008,16 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7" checksum = "d51e9f596de227fda2ea6c84607f5558e196eeaf43c986b724ba4fb8fdf497e7"
dependencies = [ dependencies = [
"rand_core", "rand_core 0.6.3",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
] ]
[[package]] [[package]]
@ -843,18 +1041,18 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.3",
"redox_syscall 0.2.10", "redox_syscall 0.2.10",
] ]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.4.6" version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a26af418b574bd56588335b3a3659a65725d4e636eb1016c2f9e3b38c7cc759" checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr 2.3.4", "memchr 2.4.1",
"regex-syntax", "regex-syntax",
] ]
@ -960,9 +1158,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.67" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f9e390c27c3c0ce8bc5d725f6e4d30a29d26659494aa4b17535f7522c5c950" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -978,6 +1176,12 @@ dependencies = [
"dirs-next", "dirs-next",
] ]
[[package]]
name = "siphasher"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "533494a8f9b724d33625ab53c6c4800f7cc445895924a8ef649222dcb76e938b"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.4" version = "0.4.4"
@ -986,9 +1190,34 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.6.1" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "string_cache"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ddb1139b5353f96e429e1a5e19fbaf663bddedaa06d1dbd49f82e352601209a"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"phf_shared",
"precomputed-hash",
"serde",
]
[[package]]
name = "string_cache_codegen"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97"
dependencies = [
"phf_generator",
"phf_shared",
"proc-macro2",
"quote",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
@ -998,9 +1227,9 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.75" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f58f7e8eaa0009c5fec437aabf511bd9933e4b2d7407bd05273c01a8906ea7" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1021,12 +1250,23 @@ checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"rand", "rand 0.8.4",
"redox_syscall 0.2.10", "redox_syscall 0.2.10",
"remove_dir_all", "remove_dir_all",
"winapi", "winapi",
] ]
[[package]]
name = "tendril"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
dependencies = [
"futf",
"mac",
"utf-8",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.2" version = "1.1.2"
@ -1062,15 +1302,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi 0.10.0+wasi-snapshot-preview1",
"winapi", "winapi",
] ]
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.3.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@ -1105,9 +1345,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.6" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
@ -1120,9 +1360,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
@ -1142,13 +1382,19 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "0.8.2" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [ dependencies = [
"getrandom", "getrandom 0.2.3",
] ]
[[package]] [[package]]
@ -1163,6 +1409,12 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe" checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.10.0+wasi-snapshot-preview1" version = "0.10.0+wasi-snapshot-preview1"
@ -1205,3 +1457,15 @@ name = "wyz"
version = "0.2.0" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
[[package]]
name = "xml5ever"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b1b52e6e8614d4a58b8e70cf51ec0cc21b256ad8206708bcff8139b5bbd6a59"
dependencies = [
"log",
"mac",
"markup5ever",
"time",
]

View file

@ -6,19 +6,22 @@ authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
ammonia = "3.1.2"
anyhow = "1.0.44" anyhow = "1.0.44"
atty = "0.2.14" atty = "0.2.14"
chrono = "0.4.19" 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"] }
env_logger = "0.8.3" env_logger = "0.8.3"
htmlescape = "0.3.1"
imap = "3.0.0-alpha.4" imap = "3.0.0-alpha.4"
imap-proto = "0.14.3" imap-proto = "0.14.3"
# This commit includes the de/serialization of the ContentType # This commit includes the de/serialization of the ContentType
# lettre = { version = "0.10.0-rc.1", features = ["serde"] } # lettre = { version = "0.10.0-rc.1", features = ["serde"] }
lettre = {git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] } lettre = {git = "https://github.com/TornaxO7/lettre/", branch = "master", features = ["serde"] }
log = "0.4.14" log = "0.4.14"
mailparse = "0.13.4" mailparse = "0.13.6"
native-tls = "0.2" native-tls = "0.2"
regex = "1.5.4"
rfc2047-decoder = "0.1.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" serde_json = "1.0.61"

View file

@ -5,5 +5,5 @@
//! //!
//! [clap's docs.rs website]: https://docs.rs/clap/2.33.3/clap/enum.Shell.html //! [clap's docs.rs website]: https://docs.rs/clap/2.33.3/clap/enum.Shell.html
pub mod arg; pub mod compl_arg;
pub mod handler; pub mod compl_handler;

View file

@ -0,0 +1,162 @@
use anyhow::{anyhow, Context, Error, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, trace};
use std::{convert::TryFrom, env, fs, path::PathBuf};
use crate::{
config::{Config, DEFAULT_PAGE_SIZE, DEFAULT_SIG_DELIM},
output::run_cmd,
};
/// Represent a user account.
#[derive(Debug, Default)]
pub struct Account {
pub name: String,
pub from: String,
pub downloads_dir: PathBuf,
pub sig: Option<String>,
pub default_page_size: usize,
pub watch_cmds: Vec<String>,
pub default: bool,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: bool,
pub imap_insecure: bool,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: bool,
pub smtp_insecure: bool,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
}
impl Account {
pub fn address(&self) -> String {
let name = &self.from;
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() {
format!("{}", self.email)
} else if has_special_chars {
// so the name has special characters => Wrap it with '"'
format!("\"{}\" <{}>", name, self.email)
} else {
format!("{} <{}>", name, self.email)
}
}
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
}
impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
type Error = Error;
fn try_from((config, account_name): (&'a Config, Option<&str>)) -> Result<Self, Self::Error> {
debug!("init account `{}`", account_name.unwrap_or("default"));
let (name, account) = match account_name.map(|name| name.trim()) {
Some("default") | Some("") | None => config
.accounts
.iter()
.find(|(_, account)| account.default.unwrap_or(false))
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!(r#"cannot find account "{}""#, name)),
}?;
let downloads_dir = account
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(|| {
config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
})
.unwrap_or_else(|| env::temp_dir());
let default_page_size = account
.default_page_size
.as_ref()
.or_else(|| config.default_page_size.as_ref())
.unwrap_or(&DEFAULT_PAGE_SIZE)
.to_owned();
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
let sig_delim = account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let sig = sig
.and_then(|sig| shellexpand::full(sig).ok())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| sig.map(|sig| sig.to_owned()))
.map(|sig| format!("{}{}", sig_delim, sig.trim_end()));
let account = Account {
name,
from: account.name.as_ref().unwrap_or(&config.name).to_owned(),
downloads_dir,
sig,
default_page_size,
watch_cmds: account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
default: account.default.unwrap_or(false),
email: account.email.to_owned(),
imap_host: account.imap_host.to_owned(),
imap_port: account.imap_port,
imap_starttls: account.imap_starttls.unwrap_or_default(),
imap_insecure: account.imap_insecure.unwrap_or_default(),
imap_login: account.imap_login.to_owned(),
imap_passwd_cmd: account.imap_passwd_cmd.to_owned(),
smtp_host: account.smtp_host.to_owned(),
smtp_port: account.smtp_port,
smtp_starttls: account.smtp_starttls.unwrap_or_default(),
smtp_insecure: account.smtp_insecure.unwrap_or_default(),
smtp_login: account.smtp_login.to_owned(),
smtp_passwd_cmd: account.smtp_passwd_cmd.to_owned(),
};
trace!("{:#?}", account);
Ok(account)
}
}

158
src/config/config_entity.rs Normal file
View file

@ -0,0 +1,158 @@
use anyhow::{Context, Error, Result};
use log::{debug, trace};
use serde::Deserialize;
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf, thread};
use toml;
use crate::output::run_cmd;
pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
/// Represent the user config.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
/// Define the full display name of the user.
pub name: String,
/// Define the downloads directory (eg. for attachments).
pub downloads_dir: Option<PathBuf>,
/// Override the default signature delimiter "`--\n `".
pub signature_delimiter: Option<String>,
/// Define the signature.
pub signature: Option<String>,
/// Define the default page size for listings.
pub default_page_size: Option<usize>,
pub notify_cmd: Option<String>,
pub watch_cmds: Option<Vec<String>>,
#[serde(flatten)]
pub accounts: ConfigAccountsMap,
}
/// Represent the accounts section of the config.
pub type ConfigAccountsMap = HashMap<String, ConfigAccountEntry>;
/// Represent an account in the accounts section.
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigAccountEntry {
pub name: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub signature_delimiter: Option<String>,
pub signature: Option<String>,
pub default_page_size: Option<usize>,
pub watch_cmds: Option<Vec<String>>,
pub default: Option<bool>,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: Option<bool>,
pub imap_insecure: Option<bool>,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: Option<bool>,
pub smtp_insecure: Option<bool>,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
}
impl Config {
fn path_from_xdg() -> Result<PathBuf> {
let path = env::var("XDG_CONFIG_HOME").context("cannot find `XDG_CONFIG_HOME` env var")?;
let mut path = PathBuf::from(path);
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".config");
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".himalayarc");
Ok(path)
}
pub fn path() -> Result<PathBuf> {
let path = Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")?;
Ok(path)
}
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
let subject = subject.as_ref();
let sender = sender.as_ref();
let default_cmd = format!(r#"notify-send "📫 {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
pub fn _exec_watch_cmds(&self, account: &ConfigAccountEntry) -> Result<()> {
let cmds = account
.watch_cmds
.as_ref()
.or_else(|| self.watch_cmds.as_ref())
.map(|cmds| cmds.to_owned())
.unwrap_or_default();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
Ok(())
}
}
impl TryFrom<Option<&str>> for Config {
type Error = Error;
fn try_from(path: Option<&str>) -> Result<Self, Self::Error> {
debug!("init config from `{:?}`", path);
let path = path.map(|s| s.into()).unwrap_or(Config::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
trace!("{:#?}", config);
Ok(config)
}
}

View file

@ -1,398 +0,0 @@
use anyhow::{anyhow, Context, Error, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, trace};
use serde::Deserialize;
use shellexpand;
use std::{collections::HashMap, convert::TryFrom, env, fs, path::PathBuf, thread};
use toml;
use crate::output::utils::run_cmd;
const DEFAULT_PAGE_SIZE: usize = 10;
const DEFAULT_SIG_DELIM: &str = "-- \n";
/// Represents the whole config file.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
// TODO: rename with `from`
pub name: String,
pub downloads_dir: Option<PathBuf>,
pub notify_cmd: Option<String>,
/// Option to override the default signature delimiter "`--\n `".
pub signature_delimiter: Option<String>,
pub signature: Option<String>,
pub default_page_size: Option<usize>,
pub watch_cmds: Option<Vec<String>>,
#[serde(flatten)]
pub accounts: ConfigAccountsMap,
}
impl Config {
fn path_from_xdg() -> Result<PathBuf> {
let path = env::var("XDG_CONFIG_HOME").context("cannot find `XDG_CONFIG_HOME` env var")?;
let mut path = PathBuf::from(path);
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".config");
path.push("himalaya");
path.push("config.toml");
Ok(path)
}
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let mut path: PathBuf = env::var(home_var)
.context(format!("cannot find `{}` env var", home_var))?
.into();
path.push(".himalayarc");
Ok(path)
}
pub fn path() -> Result<PathBuf> {
let path = Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")?;
Ok(path)
}
pub fn run_notify_cmd<S: AsRef<str>>(&self, subject: S, sender: S) -> Result<()> {
let subject = subject.as_ref();
let sender = sender.as_ref();
let default_cmd = format!(r#"notify-send "📫 {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
pub fn _exec_watch_cmds(&self, account: &ConfigAccountEntry) -> Result<()> {
let cmds = account
.watch_cmds
.as_ref()
.or_else(|| self.watch_cmds.as_ref())
.map(|cmds| cmds.to_owned())
.unwrap_or_default();
thread::spawn(move || {
debug!("batch execution of {} cmd(s)", cmds.len());
cmds.iter().for_each(|cmd| {
debug!("running command {:?}…", cmd);
let res = run_cmd(cmd);
debug!("{:?}", res);
})
});
Ok(())
}
}
impl TryFrom<Option<&str>> for Config {
type Error = Error;
fn try_from(path: Option<&str>) -> Result<Self, Self::Error> {
debug!("init config from `{:?}`", path);
let path = path.map(|s| s.into()).unwrap_or(Config::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
trace!("{:#?}", config);
Ok(config)
}
}
pub type ConfigAccountsMap = HashMap<String, ConfigAccountEntry>;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct ConfigAccountEntry {
// TODO: rename with `from`
pub name: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub signature_delimiter: Option<String>,
pub signature: Option<String>,
pub default_page_size: Option<usize>,
pub watch_cmds: Option<Vec<String>>,
pub default: Option<bool>,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: Option<bool>,
pub imap_insecure: Option<bool>,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: Option<bool>,
pub smtp_insecure: Option<bool>,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
}
/// Representation of a user account.
#[derive(Debug, Default)]
pub struct Account {
pub name: String,
pub from: String,
pub downloads_dir: PathBuf,
pub signature: String,
pub default_page_size: usize,
pub watch_cmds: Vec<String>,
pub default: bool,
pub email: String,
pub imap_host: String,
pub imap_port: u16,
pub imap_starttls: bool,
pub imap_insecure: bool,
pub imap_login: String,
pub imap_passwd_cmd: String,
pub smtp_host: String,
pub smtp_port: u16,
pub smtp_starttls: bool,
pub smtp_insecure: bool,
pub smtp_login: String,
pub smtp_passwd_cmd: String,
}
impl Account {
/// This is a little helper-function like which uses the the name and email
/// of the account to create a valid address for the header of the headers
/// of a msg.
///
/// # Hint
/// If the name includes some special characters like a whitespace, comma or semicolon, then
/// the name will be automatically wrapped between two `"`.
///
/// # Exapmle
/// ```
/// use himalaya::config::model::{Account, Config};
///
/// fn main() {
/// let config = Config::default();
///
/// let normal_account = Account::new(Some("Acc1"), "acc1@mail.com");
/// // notice the semicolon in the name!
/// let special_account = Account::new(Some("TL;DR"), "acc2@mail.com");
///
/// // -- Expeced outputs --
/// let expected_normal = Account {
/// name: Some("Acc1".to_string()),
/// email: "acc1@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// let expected_special = Account {
/// name: Some("\"TL;DR\"".to_string()),
/// email: "acc2@mail.com".to_string(),
/// .. Account::default()
/// };
///
/// assert_eq!(config.address(&normal_account), "Acc1 <acc1@mail.com>");
/// assert_eq!(config.address(&special_account), "\"TL;DR\" <acc2@mail.com>");
/// }
/// ```
pub fn address(&self) -> String {
let name = &self.from;
let has_special_chars = "()<>[]:;@.,".contains(|special_char| name.contains(special_char));
if name.is_empty() {
format!("{}", self.email)
} else if has_special_chars {
// so the name has special characters => Wrap it with '"'
format!("\"{}\" <{}>", name, self.email)
} else {
format!("{} <{}>", name, self.email)
}
}
/// Runs the given command in your password string and returns it.
pub fn imap_passwd(&self) -> Result<String> {
let passwd = run_cmd(&self.imap_passwd_cmd).context("cannot run IMAP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(passwd)
}
pub fn smtp_creds(&self) -> Result<SmtpCredentials> {
let passwd = run_cmd(&self.smtp_passwd_cmd).context("cannot run SMTP passwd cmd")?;
let passwd = passwd
.trim_end_matches(|c| c == '\r' || c == '\n')
.to_owned();
Ok(SmtpCredentials::new(self.smtp_login.to_owned(), passwd))
}
}
impl<'a> TryFrom<(&'a Config, Option<&str>)> for Account {
type Error = Error;
fn try_from((config, account_name): (&'a Config, Option<&str>)) -> Result<Self, Self::Error> {
debug!("init account `{}`", account_name.unwrap_or("default"));
let (name, account) = match account_name {
Some("") | None => config
.accounts
.iter()
.find(|(_, account)| account.default.unwrap_or(false))
.map(|(name, account)| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => config
.accounts
.get(name)
.map(|account| (name.to_owned(), account))
.ok_or_else(|| anyhow!("cannot find account `{}`", name)),
}?;
let downloads_dir = account
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
.or_else(|| {
config
.downloads_dir
.as_ref()
.and_then(|dir| dir.to_str())
.and_then(|dir| shellexpand::full(dir).ok())
.map(|dir| PathBuf::from(dir.to_string()))
})
.unwrap_or_else(|| env::temp_dir());
let default_page_size = account
.default_page_size
.as_ref()
.or_else(|| config.default_page_size.as_ref())
.unwrap_or(&DEFAULT_PAGE_SIZE)
.to_owned();
let default_sig_delim = DEFAULT_SIG_DELIM.to_string();
let signature_delim = account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let signature = account
.signature
.as_ref()
.or_else(|| config.signature.as_ref());
let signature = signature
.and_then(|sig| shellexpand::full(sig).ok())
.map(String::from)
.and_then(|sig| fs::read_to_string(sig).ok())
.or_else(|| signature.map(|sig| sig.to_owned()))
.map(|sig| format!("\n\n{}{}", signature_delim, sig.trim_end()))
.unwrap_or_default();
let account = Account {
name,
from: account.name.as_ref().unwrap_or(&config.name).to_owned(),
downloads_dir,
signature,
default_page_size,
watch_cmds: account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
default: account.default.unwrap_or(false),
email: account.email.to_owned(),
imap_host: account.imap_host.to_owned(),
imap_port: account.imap_port,
imap_starttls: account.imap_starttls.unwrap_or_default(),
imap_insecure: account.imap_insecure.unwrap_or_default(),
imap_login: account.imap_login.to_owned(),
imap_passwd_cmd: account.imap_passwd_cmd.to_owned(),
smtp_host: account.smtp_host.to_owned(),
smtp_port: account.smtp_port,
smtp_starttls: account.smtp_starttls.unwrap_or_default(),
smtp_insecure: account.smtp_insecure.unwrap_or_default(),
smtp_login: account.smtp_login.to_owned(),
smtp_passwd_cmd: account.smtp_passwd_cmd.to_owned(),
};
trace!("{:#?}", account);
Ok(account)
}
}
// FIXME: tests
// #[cfg(test)]
// mod tests {
// use crate::domain::{account::entity::Account, config::entity::Config};
// // a quick way to get a config instance for testing
// fn get_config() -> Config {
// Config {
// name: String::from("Config Name"),
// ..Config::default()
// }
// }
// #[test]
// fn test_find_account_by_name() {
// let mut config = get_config();
// let account1 = Account::new(None, "one@mail.com");
// let account2 = Account::new(Some("Two"), "two@mail.com");
// // add some accounts
// config.accounts.insert("One".to_string(), account1.clone());
// config.accounts.insert("Two".to_string(), account2.clone());
// let ret1 = config.find_account_by_name(Some("One")).unwrap();
// let ret2 = config.find_account_by_name(Some("Two")).unwrap();
// assert_eq!(*ret1, account1);
// assert_eq!(*ret2, account2);
// }
// #[test]
// fn test_address() {
// let config = get_config();
// let account1 = Account::new(None, "one@mail.com");
// let account2 = Account::new(Some("Two"), "two@mail.com");
// let account3 = Account::new(Some("TL;DR"), "three@mail.com");
// let account4 = Account::new(Some("TL,DR"), "lol@mail.com");
// let account5 = Account::new(Some("TL:DR"), "rofl@mail.com");
// let account6 = Account::new(Some("TL.DR"), "rust@mail.com");
// assert_eq!(&config.address(&account1), "Config Name <one@mail.com>");
// assert_eq!(&config.address(&account2), "Two <two@mail.com>");
// assert_eq!(&config.address(&account3), "\"TL;DR\" <three@mail.com>");
// assert_eq!(&config.address(&account4), "\"TL,DR\" <lol@mail.com>");
// assert_eq!(&config.address(&account5), "\"TL:DR\" <rofl@mail.com>");
// assert_eq!(&config.address(&account6), "\"TL.DR\" <rust@mail.com>");
// }
// }

View file

@ -1,4 +1,9 @@
//! Module related to the user's configuration. //! Module related to the user's configuration.
pub mod arg; pub mod config_arg;
pub mod entity;
pub mod account_entity;
pub use account_entity::*;
pub mod config_entity;
pub use config_entity::*;

View file

@ -4,7 +4,7 @@
use anyhow::Result; use anyhow::Result;
use crate::{config::entity::Config, domain::imap::service::ImapServiceInterface}; use crate::{config::Config, domain::imap::ImapServiceInterface};
/// Notify handler. /// Notify handler.
pub fn notify<ImapService: ImapServiceInterface>( pub fn notify<ImapService: ImapServiceInterface>(
@ -12,9 +12,7 @@ pub fn notify<ImapService: ImapServiceInterface>(
config: &Config, config: &Config,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
imap.notify(&config, keepalive)?; imap.notify(&config, keepalive)
imap.logout()?;
Ok(())
} }
/// Watch handler. /// Watch handler.
@ -22,7 +20,5 @@ pub fn watch<ImapService: ImapServiceInterface>(
keepalive: u64, keepalive: u64,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
imap.watch(keepalive)?; imap.watch(keepalive)
imap.logout()?;
Ok(())
} }

View file

@ -3,47 +3,45 @@
//! This module exposes a service that can interact with IMAP servers. //! This module exposes a service that can interact with IMAP servers.
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use imap;
use log::{debug, trace}; use log::{debug, trace};
use native_tls::{self, TlsConnector, TlsStream}; use native_tls::{TlsConnector, TlsStream};
use std::{collections::HashSet, convert::TryFrom, iter::FromIterator, net::TcpStream}; use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
iter::FromIterator,
net::TcpStream,
};
use crate::{ use crate::{
config::entity::{Account, Config}, config::{Account, Config},
domain::{ domain::{
mbox::entity::Mbox, mbox::Mbox,
msg::{entity::Msg, flag::entity::Flags}, msg::{Envelopes, Flags, Msg},
}, },
}; };
type ImapSession = imap::Session<TlsStream<TcpStream>>; type ImapSession = imap::Session<TlsStream<TcpStream>>;
type ImapMsgs = imap::types::ZeroCopy<Vec<imap::types::Fetch>>;
type ImapMboxes = imap::types::ZeroCopy<Vec<imap::types::Name>>; type ImapMboxes = imap::types::ZeroCopy<Vec<imap::types::Name>>;
pub trait ImapServiceInterface { pub trait ImapServiceInterface {
fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>; fn notify(&mut self, config: &Config, keepalive: u64) -> Result<()>;
fn watch(&mut self, keepalive: u64) -> Result<()>; fn watch(&mut self, keepalive: u64) -> Result<()>;
fn list_mboxes(&mut self) -> Result<ImapMboxes>; fn get_mboxes(&mut self) -> Result<ImapMboxes>;
fn list_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Option<ImapMsgs>>; fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes>;
fn search_msgs( fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result<Envelopes>;
&mut self, fn find_msg(&mut self, seq: &str) -> Result<Msg>;
query: &str, fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>>;
page_size: &usize, fn append_msg(&mut self, mbox: &Mbox, msg: Msg) -> Result<()>;
page: &usize, fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()>;
) -> Result<Option<ImapMsgs>>;
fn get_msg(&mut self, uid: &str) -> Result<Msg>;
fn append_msg(&mut self, mbox: &Mbox, msg: &mut Msg) -> Result<()>;
/// Add flags to the given message UID sequence.
///
/// ```ignore
/// let flags = Flags::from(vec![Flag::Seen, Flag::Deleted]);
/// add_flags("5:10", flags)
/// ```
fn add_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()>;
fn set_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()>;
fn remove_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()>;
fn expunge(&mut self) -> Result<()>; fn expunge(&mut self) -> Result<()>;
fn logout(&mut self) -> Result<()>; fn logout(&mut self) -> Result<()>;
/// Add flags to all messages within the given sequence range.
fn add_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
/// Replace flags of all messages within the given sequence range.
fn set_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
/// Remove flags from all messages within the given sequence range.
fn remove_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()>;
} }
pub struct ImapService<'a> { pub struct ImapService<'a> {
@ -108,7 +106,7 @@ impl<'a> ImapService<'a> {
} }
impl<'a> ImapServiceInterface for ImapService<'a> { impl<'a> ImapServiceInterface for ImapService<'a> {
fn list_mboxes(&mut self) -> Result<ImapMboxes> { fn get_mboxes(&mut self) -> Result<ImapMboxes> {
let mboxes = self let mboxes = self
.sess()? .sess()?
.list(Some(""), Some("*")) .list(Some(""), Some("*"))
@ -116,20 +114,20 @@ impl<'a> ImapServiceInterface for ImapService<'a> {
Ok(mboxes) Ok(mboxes)
} }
fn list_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Option<ImapMsgs>> { fn get_msgs(&mut self, page_size: &usize, page: &usize) -> Result<Envelopes> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
let last_seq = self let last_seq = self
.sess()? .sess()?
.select(&mbox.name) .select(&mbox.name)
.context(format!("cannot select mailbox `{}`", self.mbox.name))? .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?
.exists as i64; .exists as i64;
if last_seq == 0 { if last_seq == 0 {
return Ok(None); return Ok(Envelopes::default());
} }
// TODO: add tests, improve error management when empty page // TODO: add tests, improve error management when empty page
let range = if page_size > &0 { let range = if *page_size > 0 {
let cursor = (page * page_size) as i64; let cursor = (page * page_size) as i64;
let begin = 1.max(last_seq - cursor); let begin = 1.max(last_seq - cursor);
let end = begin - begin.min(*page_size as i64) + 1; let end = begin - begin.min(*page_size as i64) + 1;
@ -140,138 +138,94 @@ impl<'a> ImapServiceInterface for ImapService<'a> {
let fetches = self let fetches = self
.sess()? .sess()?
.fetch(range, "(UID FLAGS ENVELOPE INTERNALDATE)") .fetch(range, "(ENVELOPE FLAGS INTERNALDATE)")
.context("cannot fetch messages")?; .context(r#"cannot fetch messages within range "{}""#)?;
Ok(Some(fetches)) Ok(Envelopes::try_from(fetches)?)
} }
fn search_msgs( fn find_msgs(&mut self, query: &str, page_size: &usize, page: &usize) -> Result<Envelopes> {
&mut self,
query: &str,
page_size: &usize,
page: &usize,
) -> Result<Option<ImapMsgs>> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
self.sess()? self.sess()?
.select(&mbox.name) .select(&mbox.name)
.context(format!("cannot select mailbox `{}`", self.mbox.name))?; .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
let begin = page * page_size; let begin = page * page_size;
let end = begin + (page_size - 1); let end = begin + (page_size - 1);
let uids: Vec<String> = self let seqs: Vec<String> = self
.sess()? .sess()?
.search(query) .search(query)
.context(format!( .context(format!(
"cannot search in `{}` with query `{}`", r#"cannot search in "{}" with query: "{}""#,
self.mbox.name, query self.mbox.name, query
))? ))?
.iter() .iter()
.map(|seq| seq.to_string()) .map(|seq| seq.to_string())
.collect(); .collect();
if uids.is_empty() { if seqs.is_empty() {
return Ok(None); return Ok(Envelopes::default());
} }
let range = uids[begin..end.min(uids.len())].join(","); // FIXME: panic if begin > end
let range = seqs[begin..end.min(seqs.len())].join(",");
let fetches = self let fetches = self
.sess()? .sess()?
.fetch(&range, "(UID FLAGS ENVELOPE INTERNALDATE)") .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!("cannot fetch range `{}`", &range))?; .context(r#"cannot fetch messages within range "{}""#)?;
Ok(Some(fetches)) Ok(Envelopes::try_from(fetches)?)
} }
/// Get the message according to the given `mbox` and `uid`.
fn get_msg(&mut self, uid: &str) -> Result<Msg> { /// Find a message by sequence number.
fn find_msg(&mut self, seq: &str) -> Result<Msg> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
self.sess()?
.select(&mbox.name)
.context(format!("cannot select mbox `{}`", self.mbox.name))?;
match self
.sess()?
.uid_fetch(uid, "(FLAGS BODY[] ENVELOPE INTERNALDATE)")
.context("cannot fetch bodies")?
.first()
{
None => Err(anyhow!("cannot find message `{}`", uid)),
Some(fetch) => Ok(Msg::try_from(fetch)?),
}
}
fn append_msg(&mut self, mbox: &Mbox, msg: &mut Msg) -> Result<()> {
let body = msg.into_bytes()?;
let flags: HashSet<imap::types::Flag<'static>> = (*msg.flags).clone();
self.sess()?
.append(&mbox.name, &body)
.flags(flags)
.finish()
.context(format!("cannot append message to `{}`", mbox.name))?;
Ok(())
}
fn add_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned();
let flags: String = flags.to_string();
self.sess()? self.sess()?
.select(&mbox.name) .select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?; .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()? let fetches = self
.uid_store(uid_seq, format!("+FLAGS ({})", flags)) .sess()?
.context(format!(r#"cannot add flags "{}""#, &flags))?; .fetch(seq, "(ENVELOPE FLAGS INTERNALDATE BODY[])")
Ok(()) .context(r#"cannot fetch messages "{}""#)?;
let fetch = fetches
.first()
.ok_or(anyhow!(r#"cannot find message "{}"#, seq))?;
Ok(Msg::try_from(fetch)?)
} }
/// Applies the given flags to the msg. fn find_raw_msg(&mut self, seq: &str) -> Result<Vec<u8>> {
///
/// # 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();
/// }
/// ```
fn set_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned(); let mbox = self.mbox.to_owned();
self.sess()? self.sess()?
.select(&mbox.name) .select(&mbox.name)
.context(format!("cannot select mailbox `{}`", self.mbox.name))?; .context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
let fetches = self
.sess()?
.fetch(seq, "BODY[]")
.context(r#"cannot fetch raw messages "{}""#)?;
let fetch = fetches
.first()
.ok_or(anyhow!(r#"cannot find raw message "{}"#, seq))?;
Ok(fetch.body().map(Vec::from).unwrap_or_default())
}
fn append_raw_msg_with_flags(&mut self, mbox: &Mbox, msg: &[u8], flags: Flags) -> Result<()> {
self.sess()? self.sess()?
.uid_store(uid_seq, format!("FLAGS ({})", flags)) .append(&mbox.name, &msg)
.context(format!("cannot set flags `{}`", &flags))?; .flags(flags.0)
.finish()
.context(format!(r#"cannot append message to "{}""#, mbox.name))?;
Ok(()) Ok(())
} }
/// Remove the flags to the message by the given information. Take a look on the example above. fn append_msg(&mut self, mbox: &Mbox, msg: Msg) -> Result<()> {
/// It's pretty similar. let msg_raw: Vec<u8> = (&msg).try_into()?;
fn remove_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned();
let flags = flags.to_string();
self.sess()? self.sess()?
.select(&mbox.name) .append(&mbox.name, &msg_raw)
.context(format!("cannot select mailbox `{}`", self.mbox.name))?; .flags(msg.flags.0)
self.sess()? .finish()
.uid_store(uid_seq, format!("-FLAGS ({})", flags)) .context(format!(r#"cannot append message to "{}""#, mbox.name))?;
.context(format!("cannot remove flags `{}`", &flags))?;
Ok(())
}
fn expunge(&mut self) -> Result<()> {
self.sess()?
.expunge()
.context(format!("cannot expunge `{}`", self.mbox.name))?;
Ok(()) Ok(())
} }
@ -327,8 +281,13 @@ impl<'a> ImapServiceInterface for ImapService<'a> {
anyhow!("cannot retrieve message {}'s UID", fetch.message) anyhow!("cannot retrieve message {}'s UID", fetch.message)
})?; })?;
let subject = msg.headers.subject.clone().unwrap_or_default(); let from = msg
config.run_notify_cmd(&subject, &msg.headers.from[0])?; .from
.as_ref()
.and_then(|addrs| addrs.iter().next())
.map(|addr| addr.to_string())
.unwrap_or(String::from("unknown"));
config.run_notify_cmd(&msg.subject, &from)?;
debug!("notify message: {}", uid); debug!("notify message: {}", uid);
trace!("message: {:?}", msg); trace!("message: {:?}", msg);
@ -377,6 +336,48 @@ impl<'a> ImapServiceInterface for ImapService<'a> {
} }
Ok(()) Ok(())
} }
fn add_flags(&mut self, seq_range: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned();
let flags: String = flags.to_string();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(seq_range, format!("+FLAGS ({})", flags))
.context(format!(r#"cannot add flags "{}""#, &flags))?;
Ok(())
}
fn set_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(uid_seq, format!("FLAGS ({})", flags))
.context(format!(r#"cannot set flags "{}""#, &flags))?;
Ok(())
}
fn remove_flags(&mut self, uid_seq: &str, flags: &Flags) -> Result<()> {
let mbox = self.mbox.to_owned();
let flags = flags.to_string();
self.sess()?
.select(&mbox.name)
.context(format!(r#"cannot select mailbox "{}""#, self.mbox.name))?;
self.sess()?
.store(uid_seq, format!("-FLAGS ({})", flags))
.context(format!(r#"cannot remove flags "{}""#, &flags))?;
Ok(())
}
fn expunge(&mut self) -> Result<()> {
self.sess()?
.expunge()
.context(format!(r#"cannot expunge mailbox "{}""#, self.mbox.name))?;
Ok(())
}
} }
impl<'a> From<(&'a Account, &'a Mbox)> for ImapService<'a> { impl<'a> From<(&'a Account, &'a Mbox)> for ImapService<'a> {

View file

@ -1,5 +1,7 @@
//! Module related to IMAP. //! Module related to IMAP.
pub mod arg; pub mod imap_arg;
pub mod handler; pub mod imap_handler;
pub mod service;
pub mod imap_service;
pub use imap_service::*;

View file

@ -5,9 +5,7 @@ use serde::{
ser::{self, SerializeSeq}, ser::{self, SerializeSeq},
Serialize, Serialize,
}; };
use std::collections::HashSet; use std::{borrow::Cow, collections::HashSet, convert::TryFrom, fmt};
use std::fmt;
use std::{borrow::Cow, convert::TryFrom};
use crate::ui::table::{Cell, Row, Table}; use crate::ui::table::{Cell, Row, Table};

View file

@ -6,8 +6,8 @@ use anyhow::Result;
use log::{debug, trace}; use log::{debug, trace};
use crate::{ use crate::{
domain::{imap::service::ImapServiceInterface, mbox::entity::Mboxes}, domain::{imap::ImapServiceInterface, mbox::Mboxes},
output::service::{OutputService, OutputServiceInterface}, output::{OutputService, OutputServiceInterface},
}; };
/// List all mailboxes. /// List all mailboxes.
@ -15,11 +15,10 @@ pub fn list<ImapService: ImapServiceInterface>(
output: &OutputService, output: &OutputService,
imap: &mut ImapService, imap: &mut ImapService,
) -> Result<()> { ) -> Result<()> {
let names = imap.list_mboxes()?; let names = imap.get_mboxes()?;
let mboxes = Mboxes::from(&names); let mboxes = Mboxes::from(&names);
debug!("mailboxes len: {}", mboxes.0.len()); debug!("mailboxes len: {}", mboxes.0.len());
trace!("mailboxes: {:#?}", mboxes); trace!("mailboxes: {:#?}", mboxes);
output.print(mboxes)?; output.print(mboxes)?;
imap.logout()?;
Ok(()) Ok(())
} }

View file

@ -1,5 +1,7 @@
//! Module related to mailbox. //! Module related to mailbox.
pub mod arg; pub mod mbox_arg;
pub mod entity; pub mod mbox_handler;
pub mod handler;
pub mod mbox_entity;
pub use mbox_entity::*;

View file

@ -1,6 +1,13 @@
//! Domain-specific modules. //! Domain-specific modules.
pub mod imap; pub mod imap;
pub use self::imap::*;
pub mod mbox; pub mod mbox;
pub use mbox::*;
pub mod msg; pub mod msg;
pub use msg::*;
pub mod smtp; pub mod smtp;
pub use smtp::*;

View file

@ -1,25 +0,0 @@
//! Module related to message attachment CLI.
//!
//! This module provides arguments related to message attachment.
use clap::{App, Arg, SubCommand};
use crate::domain::msg;
/// Message attachment subcommands.
pub(crate) fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("attachments")
.aliases(&["attachment", "att", "a"])
.about("Downloads all message attachments")
.arg(msg::arg::uid_arg())]
}
/// Message attachment path argument.
pub(crate) fn path_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("attachments")
.help("Adds attachment to the message")
.short("a")
.long("attachment")
.value_name("PATH")
.multiple(true)
}

View file

@ -1,120 +0,0 @@
use anyhow::{Error, Result};
use lettre::message::header::ContentType;
use mailparse::{DispositionType, ParsedMail};
use serde::Serialize;
use std::{convert::TryFrom, fs, path::Path};
/// This struct represents an attachment.
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Attachment {
/// Holds the filename of an attachment.
pub filename: String,
/// Holds the mime-type of the attachment. For example `text/plain`.
pub content_type: ContentType,
/// Holds the data of the attachment.
#[serde(skip_serializing)]
pub body_raw: Vec<u8>,
}
impl Attachment {
/// This from function extracts one attachment of a parsed msg.
/// If it couldn't create an attachment with the given parsed msg, than it will
/// return `None`.
///
/// # Example
/// ```
/// use himalaya::msg::attachment::Attachment;
///
/// let parsed = mailparse::parse_mail(concat![
/// "Content-Type: text/plain; charset=utf-8\n",
/// "Content-Transfer-Encoding: quoted-printable\n",
/// "\n",
/// "A plaintext attachment.",
/// ].as_bytes()).unwrap();
///
/// let attachment = Attachment::from_parsed_mail(&parsed);
/// ```
pub fn from_parsed_mail(parsed_mail: &ParsedMail) -> Option<Self> {
if parsed_mail.get_content_disposition().disposition == DispositionType::Attachment {
let disposition = parsed_mail.get_content_disposition();
let filename = disposition.params.get("filename").unwrap().to_string();
let body_raw = parsed_mail.get_body_raw().unwrap_or(Vec::new());
let content_type: ContentType = tree_magic::from_u8(&body_raw).parse().unwrap();
return Some(Self {
filename,
content_type,
body_raw,
});
}
None
}
}
// == Traits ==
/// Creates an Attachment with the follwing values:
///
/// ```no_run
/// # use himalaya::msg::attachment::Attachment;
/// use lettre::message::header::ContentType;
///
/// let attachment = Attachment {
/// filename: String::new(),
/// content_type: ContentType::TEXT_PLAIN,
/// body_raw: Vec::new(),
/// };
/// ```
impl Default for Attachment {
fn default() -> Self {
Self {
filename: String::new(),
content_type: ContentType::TEXT_PLAIN,
body_raw: Vec::new(),
}
}
}
// -- From Implementations --
/// Tries to convert the given file (by the given path) into an attachment.
/// It'll try to detect the mime-type/data-type automatically.
///
/// # Example
/// ```no_run
/// use himalaya::msg::attachment::Attachment;
/// use std::convert::TryFrom;
///
/// let attachment = Attachment::try_from("/some/path.png");
/// ```
impl<'from> TryFrom<&'from str> for Attachment {
type Error = Error;
fn try_from(path: &'from str) -> Result<Self> {
let path = Path::new(path);
// -- Get attachment information --
let filename = if let Some(filename) = path.file_name() {
filename
// `&OsStr` -> `Option<&str>`
.to_str()
// get rid of the `Option` wrapper
.unwrap_or(&String::new())
.to_string()
} else {
// use an empty string
String::new()
};
let file_content = fs::read(&path)?;
let content_type: ContentType = tree_magic::from_filepath(&path).parse()?;
Ok(Self {
filename,
content_type,
body_raw: file_content,
})
}
}

View file

@ -1,4 +0,0 @@
//! Module related to message attachment.
pub mod arg;
pub mod entity;

View file

@ -1,92 +0,0 @@
use serde::Serialize;
use std::fmt;
/// 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 plain version of a body (if available)
pub plain: Option<String>,
/// The html version of a body (if available)
pub html: Option<String>,
}
impl Body {
/// Returns a new instance of `Body` without any attributes set. (Same as `Body::default()`)
///
/// # Example
/// ```rust
/// use himalaya::msg::body::Body;
///
/// fn main() {
/// let body = Body::new();
///
/// let expected_body = Body {
/// text: None,
/// html: None,
/// };
///
/// assert_eq!(body, expected_body);
/// }
/// ```
pub fn new() -> Self {
Self::default()
}
/// Returns a new instance of `Body` with `text` set.
///
/// # Example
/// ```rust
/// use himalaya::msg::body::Body;
///
/// fn main() {
/// let body = Body::new_with_text("Text body");
///
/// let expected_body = Body {
/// text: Some("Text body".to_string()),
/// html: None,
/// };
///
/// assert_eq!(body, expected_body);
/// }
/// ```
pub fn new_with_text<S: ToString>(text: S) -> Self {
Self {
plain: Some(text.to_string()),
html: None,
}
}
}
// == Traits ==
impl Default for Body {
fn default() -> Self {
Self {
plain: None,
html: None,
}
}
}
impl fmt::Display for Body {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
let content = if let Some(text) = self.plain.clone() {
text
} else if let Some(html) = self.html.clone() {
html
} else {
String::new()
};
write!(formatter, "{}", content)
}
}

View file

@ -1 +0,0 @@
pub mod entity;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,145 @@
use anyhow::{anyhow, Context, Error, Result};
use serde::Serialize;
use std::convert::TryFrom;
use crate::{
domain::msg::{Flag, Flags},
ui::table::{Cell, Row, Table},
};
/// Representation of an envelope. An envelope gathers basic information related to a message. It
/// is mostly used for listings.
#[derive(Debug, Default, Serialize)]
pub struct Envelope {
/// The sequence number of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
pub id: u32,
/// The flags attached to the message.
pub flags: Flags,
/// The subject of the message.
pub subject: String,
/// The sender of the message.
pub sender: String,
/// The internal date of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
pub date: Option<String>,
}
impl<'a> TryFrom<&'a imap::types::Fetch> for Envelope {
type Error = Error;
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Envelope> {
let envelope = fetch
.envelope()
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number
let id = fetch.message;
// Get the flags
let flags = Flags::try_from(fetch.flags())?;
// Get the subject
let subject = envelope
.subject
.as_ref()
.ok_or(anyhow!("cannot get subject of message {}", fetch.message))
.and_then(|subj| {
rfc2047_decoder::decode(subj).context(format!(
"cannot decode subject of message {}",
fetch.message
))
})?;
// Get the sender
let sender = envelope
.sender
.as_ref()
.and_then(|addrs| addrs.get(0))
.or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0)))
.ok_or(anyhow!("cannot get sender of message {}", fetch.message))?;
let sender = if let Some(ref name) = sender.name {
rfc2047_decoder::decode(&name.to_vec()).context(format!(
"cannot decode sender's name of message {}",
fetch.message,
))?
} else {
let mbox = sender
.mailbox
.as_ref()
.ok_or(anyhow!(
"cannot get sender's mailbox of message {}",
fetch.message
))
.and_then(|mbox| {
rfc2047_decoder::decode(&mbox.to_vec()).context(format!(
"cannot decode sender's mailbox of message {}",
fetch.message,
))
})?;
let host = sender
.host
.as_ref()
.ok_or(anyhow!(
"cannot get sender's host of message {}",
fetch.message
))
.and_then(|host| {
rfc2047_decoder::decode(&host.to_vec()).context(format!(
"cannot decode sender's host of message {}",
fetch.message,
))
})?;
format!("{}@{}", mbox, host)
};
// Get the internal date
let date = fetch
.internal_date()
.map(|date| date.naive_local().to_string());
Ok(Self {
id,
flags,
subject,
sender,
date,
})
}
}
impl Table for Envelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").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("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen);
let subject = &self.subject;
let sender = &self.sender;
let date = self
.date
.as_ref()
.map(|date| date.as_str())
.unwrap_or_default();
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}

View file

@ -0,0 +1,42 @@
use anyhow::{Error, Result};
use imap::types::{Fetch, ZeroCopy};
use serde::Serialize;
use std::{
convert::TryFrom,
fmt::{self, Display},
ops::Deref,
};
use crate::{domain::msg::Envelope, ui::Table};
/// Representation of a list of envelopes.
#[derive(Debug, Default, Serialize)]
pub struct Envelopes(pub Vec<Envelope>);
impl Deref for Envelopes {
type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<ZeroCopy<Vec<Fetch>>> for Envelopes {
type Error = Error;
fn try_from(fetches: ZeroCopy<Vec<Fetch>>) -> Result<Self> {
let mut envelopes = vec![];
for fetch in fetches.iter().rev() {
envelopes.push(Envelope::try_from(fetch)?);
}
Ok(Self(envelopes))
}
}
impl Display for Envelopes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "\n{}", Table::render(&self))
}
}

View file

@ -1,280 +0,0 @@
pub(crate) use imap::types::Flag;
use serde::ser::{Serialize, SerializeSeq, Serializer};
use std::borrow::Cow;
use std::collections::HashSet;
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::convert::From;
/// Serializable wrapper for `imap::types::Flag`
#[derive(Debug, PartialEq, Eq, Clone)]
struct SerializableFlag<'flag>(&'flag imap::types::Flag<'flag>);
impl<'flag> Serialize for SerializableFlag<'flag> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match self.0 {
Flag::Seen => "Seen",
Flag::Answered => "Answered",
Flag::Flagged => "Flagged",
Flag::Deleted => "Deleted",
Flag::Draft => "Draft",
Flag::Recent => "Recent",
Flag::MayCreate => "MayCreate",
Flag::Custom(cow) => cow,
_ => "Unknown",
})
}
}
/// This struct type includes all flags which belong to a given mail.
/// It's used in the [`Msg.flags`] attribute field of the `Msg` struct. To be more clear: It's just
/// a wrapper for the [`imap::types::Flag`] but without a lifetime.
///
/// [`Msg.flags`]: struct.Msg.html#structfield.flags
/// [`imap::types::Flag`]: https://docs.rs/imap/2.4.1/imap/types/enum.Flag.html
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct Flags(pub HashSet<Flag<'static>>);
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) {
" "
} else {
""
});
flags.push_str(if self.0.contains(&Flag::Answered) {
""
} else {
" "
});
flags.push_str(if self.0.contains(&Flag::Flagged) {
"!"
} else {
" "
});
flags
}
}
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut glue = "";
for flag in &self.0 {
write!(f, "{}", glue)?;
match flag {
Flag::Seen => write!(f, "\\Seen")?,
Flag::Answered => write!(f, "\\Answered")?,
Flag::Flagged => write!(f, "\\Flagged")?,
Flag::Deleted => write!(f, "\\Deleted")?,
Flag::Draft => write!(f, "\\Draft")?,
Flag::Recent => write!(f, "\\Recent")?,
Flag::MayCreate => write!(f, "\\MayCreate")?,
Flag::Custom(cow) => write!(f, "{}", cow)?,
_ => (),
}
glue = " ";
}
Ok(())
}
}
impl<'a> From<&[imap::types::Flag<'a>]> for Flags {
fn from(flags: &[imap::types::Flag<'a>]) -> Self {
Self(
flags
.iter()
.map(|flag| convert_to_static(flag).unwrap())
.collect::<HashSet<Flag<'static>>>(),
)
}
}
impl<'a> From<Vec<imap::types::Flag<'a>>> for Flags {
fn from(flags: Vec<imap::types::Flag<'a>>) -> Self {
Self(
flags
.iter()
.map(|flag| convert_to_static(flag).unwrap())
.collect::<HashSet<Flag<'static>>>(),
)
}
}
/// Converst a string of flags into their appropriate flag representation. For example `"Seen"` is
/// gonna be convertred to `Flag::Seen`.
///
/// # Example
/// ```rust
/// use himalaya::flag::model::Flags;
/// use imap::types::Flag;
/// use std::collections::HashSet;
///
/// fn main() {
/// let flags = "Seen Answered";
///
/// let mut expected = HashSet::new();
/// expected.insert(Flag::Seen);
/// expected.insert(Flag::Answered);
///
/// let output = Flags::from(flags);
///
/// assert_eq!(output.0, expected);
/// }
/// ```
impl From<&str> for Flags {
fn from(flags: &str) -> Self {
let mut content: HashSet<Flag<'static>> = HashSet::new();
for flag in flags.split_ascii_whitespace() {
match flag {
"Answered" => content.insert(Flag::Answered),
"Deleted" => content.insert(Flag::Deleted),
"Draft" => content.insert(Flag::Draft),
"Flagged" => content.insert(Flag::Flagged),
"MayCreate" => content.insert(Flag::MayCreate),
"Recent" => content.insert(Flag::Recent),
"Seen" => content.insert(Flag::Seen),
custom => content.insert(Flag::Custom(Cow::Owned(custom.to_string()))),
};
}
Self(content)
}
}
impl<'a> From<Vec<&'a str>> for Flags {
fn from(flags: Vec<&'a str>) -> Self {
let mut map: HashSet<Flag<'static>> = HashSet::new();
for f in flags {
match f {
"Answered" | _ if f.eq_ignore_ascii_case("answered") => map.insert(Flag::Answered),
"Deleted" | _ if f.eq_ignore_ascii_case("deleted") => map.insert(Flag::Deleted),
"Draft" | _ if f.eq_ignore_ascii_case("draft") => map.insert(Flag::Draft),
"Flagged" | _ if f.eq_ignore_ascii_case("flagged") => map.insert(Flag::Flagged),
"MayCreate" | _ if f.eq_ignore_ascii_case("maycreate") => {
map.insert(Flag::MayCreate)
}
"Recent" | _ if f.eq_ignore_ascii_case("recent") => map.insert(Flag::Recent),
"Seen" | _ if f.eq_ignore_ascii_case("seen") => map.insert(Flag::Seen),
custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))),
};
}
Self(map)
}
}
impl Deref for Flags {
type Target = HashSet<Flag<'static>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Flags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Serialize for Flags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for flag in &self.0 {
seq.serialize_element(&SerializableFlag(flag))?;
}
seq.end()
}
}
// == Helper Functions ==
/// HINT: This function is only needed as long this pull request hasn't been
/// merged yet: https://github.com/jonhoo/rust-imap/pull/206
fn convert_to_static<'func>(flag: &'func Flag) -> Result<Flag<'static>, ()> {
match flag {
Flag::Seen => Ok(Flag::Seen),
Flag::Answered => Ok(Flag::Answered),
Flag::Flagged => Ok(Flag::Flagged),
Flag::Deleted => Ok(Flag::Deleted),
Flag::Draft => Ok(Flag::Draft),
Flag::Recent => Ok(Flag::Recent),
Flag::MayCreate => Ok(Flag::MayCreate),
Flag::Custom(cow) => Ok(Flag::Custom(Cow::Owned(cow.to_string()))),
&_ => Err(()),
}
}
#[cfg(test)]
mod tests {
use crate::domain::msg::flag::entity::Flags;
use imap::types::Flag;
use std::collections::HashSet;
#[test]
fn test_get_signs() {
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
assert_eq!(flags.get_signs(), "".to_string());
}
#[test]
fn test_from_string() {
let flags = Flags::from("Seen Answered");
let expected = Flags::from(vec![Flag::Seen, Flag::Answered]);
assert_eq!(flags, expected);
}
#[test]
fn test_to_string() {
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// since we can't influence the order in the HashSet, we're gonna convert it into a vec,
// sort it according to the names and compare it aftwards.
let flag_string = flags.to_string();
let mut flag_vec: Vec<String> = flag_string
.split_ascii_whitespace()
.map(|word| word.to_string())
.collect();
flag_vec.sort();
assert_eq!(
flag_vec,
vec!["\\Answered".to_string(), "\\Seen".to_string()]
);
}
#[test]
fn test_from_vec() {
let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
let mut expected = HashSet::new();
expected.insert(Flag::Seen);
expected.insert(Flag::Answered);
assert_eq!(flags.0, expected);
}
}

View file

@ -1,82 +0,0 @@
//! Module related to message flag handling.
//!
//! This module gathers all message flag commands.
use anyhow::Result;
use crate::{
domain::{imap::service::ImapServiceInterface, msg::flag::entity::Flags},
output::service::OutputServiceInterface,
};
/// Add flags from the given message UID sequence.
/// Flags do not need to be prefixed with `\` and they are not case-sensitive.
///
/// ```ignore
/// add("21", "\\Seen", &output, &mut imap)?;
/// add("42", "recent", &output, &mut imap)?;
/// add("1:10", "Answered custom", &output, &mut imap)?;
/// ```
pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.add_flags(uid, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully added to message {}"#,
flags, uid
))?;
imap.logout()?;
Ok(())
}
/// Remove flags from the given message UID sequence.
/// Flags do not need to be prefixed with `\` and they are not case-sensitive.
///
/// ```ignore
/// remove("21", "\\Seen", &output, &mut imap)?;
/// remove("42", "recent", &output, &mut imap)?;
/// remove("1:10", "Answered custom", &output, &mut imap)?;
/// ```
pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.remove_flags(uid, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully removed from message {}"#,
flags, uid
))?;
imap.logout()?;
Ok(())
}
/// Replace flags from the given message UID sequence.
/// Flags do not need to be prefixed with `\` and they are not case-sensitive.
///
/// ```ignore
/// set("21", "\\Seen", &output, &mut imap)?;
/// set("42", "recent", &output, &mut imap)?;
/// set("1:10", "Answered custom", &output, &mut imap)?;
/// ```
pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.set_flags(uid, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully set for message {}"#,
flags, uid
))?;
imap.logout()?;
Ok(())
}

View file

@ -1,5 +0,0 @@
//! Module related to messages flag.
pub mod arg;
pub mod entity;
pub mod handler;

View file

@ -4,47 +4,47 @@
use anyhow::Result; use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::debug; use log::{debug, trace};
use crate::domain::msg; use crate::domain::msg::msg_arg;
type Uid<'a> = &'a str; type SeqRange<'a> = &'a str;
type Flags<'a> = Vec<&'a str>; type Flags<'a> = Vec<&'a str>;
/// Message flag commands. /// Message flag commands.
pub enum Command<'a> { pub enum Command<'a> {
Set(Uid<'a>, Flags<'a>), Set(SeqRange<'a>, Flags<'a>),
Add(Uid<'a>, Flags<'a>), Add(SeqRange<'a>, Flags<'a>),
Remove(Uid<'a>, Flags<'a>), Remove(SeqRange<'a>, Flags<'a>),
} }
/// Message flag command matcher. /// Message flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("set") {
debug!("set command matched");
let uid = m.value_of("uid").unwrap();
debug!("uid: {}", uid);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
debug!("flags: `{:?}`", flags);
return Ok(Some(Command::Set(uid, flags)));
}
if let Some(m) = m.subcommand_matches("add") { if let Some(m) = m.subcommand_matches("add") {
debug!("add command matched"); debug!("add command matched");
let uid = m.value_of("uid").unwrap(); let seq_range = m.value_of("seq-range").unwrap();
debug!("uid: {}", uid); trace!(r#"seq range: "{:?}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
debug!("flags: `{:?}`", flags); trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Add(uid, flags))); return Ok(Some(Command::Add(seq_range, flags)));
}
if let Some(m) = m.subcommand_matches("set") {
debug!("set command matched");
let seq_range = m.value_of("seq-range").unwrap();
trace!(r#"seq range: "{:?}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Set(seq_range, flags)));
} }
if let Some(m) = m.subcommand_matches("remove") { if let Some(m) = m.subcommand_matches("remove") {
debug!("remove command matched"); debug!("remove command matched");
let uid = m.value_of("uid").unwrap(); let seq_range = m.value_of("seq-range").unwrap();
debug!("uid: {}", uid); trace!(r#"seq range: "{:?}""#, seq_range);
let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect(); let flags: Vec<&str> = m.values_of("flags").unwrap_or_default().collect();
debug!("flags: `{:?}`", flags); trace!(r#"flags: "{:?}""#, flags);
return Ok(Some(Command::Remove(uid, flags))); return Ok(Some(Command::Remove(seq_range, flags)));
} }
Ok(None) Ok(None)
@ -53,9 +53,8 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
/// Message flag flags argument. /// Message flag flags argument.
fn flags_arg<'a>() -> Arg<'a, 'a> { fn flags_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("flags") Arg::with_name("flags")
.help( .help("IMAP flags")
"IMAP flags (they do not need to be prefixed with `\\` and they are case-insensitive)", .long_help("IMAP flags. Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
)
.value_name("FLAGS…") .value_name("FLAGS…")
.multiple(true) .multiple(true)
.required(true) .required(true)
@ -68,22 +67,22 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.about("Handles flags") .about("Handles flags")
.setting(AppSettings::SubcommandRequiredElseHelp) .setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand( .subcommand(
SubCommand::with_name("set") SubCommand::with_name("add")
.about("Replaces all message flags") .about("Adds flags to a message")
.arg(msg::arg::uid_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name("add") SubCommand::with_name("set")
.about("Adds flags to a message") .about("Replaces all message flags")
.arg(msg::arg::uid_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),
) )
.subcommand( .subcommand(
SubCommand::with_name("remove") SubCommand::with_name("remove")
.aliases(&["rm"]) .aliases(&["rm"])
.about("Removes flags from a message") .about("Removes flags from a message")
.arg(msg::arg::uid_arg()) .arg(msg_arg::seq_range_arg())
.arg(flags_arg()), .arg(flags_arg()),
)] )]
} }

View file

@ -0,0 +1,26 @@
pub use imap::types::Flag;
use serde::ser::{Serialize, Serializer};
/// Serializable wrapper arround [`imap::types::Flag`].
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct SerializableFlag<'a>(pub &'a Flag<'a>);
impl<'a> Serialize for SerializableFlag<'a> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(match self.0 {
Flag::Seen => "Seen",
Flag::Answered => "Answered",
Flag::Flagged => "Flagged",
Flag::Deleted => "Deleted",
Flag::Draft => "Draft",
Flag::Recent => "Recent",
Flag::MayCreate => "MayCreate",
Flag::Custom(cow) => cow,
// TODO: find a way to return an error
_ => "Unknown",
})
}
}

View file

@ -0,0 +1,58 @@
//! Module related to message flag handling.
//!
//! This module gathers all message flag commands.
use anyhow::Result;
use crate::{
domain::{imap::ImapServiceInterface, msg::Flags},
output::OutputServiceInterface,
};
/// Add flags to all messages within the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq_range: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.add_flags(seq_range, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully added to message(s) "{}""#,
flags, seq_range
))
}
/// Remove flags from all messages within the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq_range: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.remove_flags(seq_range, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully removed from message(s) "{}""#,
flags, seq_range
))
}
/// Replace flags of all messages within the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq_range: &'a str,
flags: Vec<&'a str>,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let flags = Flags::from(flags);
imap.set_flags(seq_range, &flags)?;
output.print(format!(
r#"Flag(s) "{}" successfully set for message(s) "{}""#,
flags, seq_range
))
}

View file

@ -0,0 +1,239 @@
use anyhow::{anyhow, Error, Result};
use serde::ser::{Serialize, SerializeSeq, Serializer};
use std::{
borrow::Cow,
collections::HashSet,
convert::{TryFrom, TryInto},
fmt::{self, Display},
ops::{Deref, DerefMut},
};
use crate::domain::msg::{Flag, SerializableFlag};
/// Wrapper arround [`imap::types::Flag`]s.
#[derive(Debug, Clone, Default)]
pub struct Flags(pub HashSet<Flag<'static>>);
impl Flags {
/// Build a symbols string based on flags contained in the hashset.
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&Flag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&Flag::Answered) {
""
} else {
" "
});
flags.push_str(if self.contains(&Flag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut glue = "";
for flag in &self.0 {
write!(f, "{}", glue)?;
match flag {
Flag::Seen => write!(f, "\\Seen")?,
Flag::Answered => write!(f, "\\Answered")?,
Flag::Flagged => write!(f, "\\Flagged")?,
Flag::Deleted => write!(f, "\\Deleted")?,
Flag::Draft => write!(f, "\\Draft")?,
Flag::Recent => write!(f, "\\Recent")?,
Flag::MayCreate => write!(f, "\\MayCreate")?,
Flag::Custom(cow) => write!(f, "{}", cow)?,
_ => (),
}
glue = " ";
}
Ok(())
}
}
impl<'a> TryFrom<Vec<Flag<'a>>> for Flags {
type Error = Error;
fn try_from(flags: Vec<Flag<'a>>) -> Result<Flags> {
let mut set: HashSet<Flag<'static>> = HashSet::new();
for flag in flags {
set.insert(match flag {
Flag::Seen => Flag::Seen,
Flag::Answered => Flag::Answered,
Flag::Flagged => Flag::Flagged,
Flag::Deleted => Flag::Deleted,
Flag::Draft => Flag::Draft,
Flag::Recent => Flag::Recent,
Flag::MayCreate => Flag::MayCreate,
Flag::Custom(cow) => Flag::Custom(Cow::Owned(cow.to_string())),
flag => return Err(anyhow!(r#"cannot parse flag "{}""#, flag)),
});
}
Ok(Self(set))
}
}
impl<'a> TryFrom<&'a [Flag<'a>]> for Flags {
type Error = Error;
fn try_from(flags: &'a [Flag<'a>]) -> Result<Flags> {
flags.to_vec().try_into()
}
}
impl Deref for Flags {
type Target = HashSet<Flag<'static>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Flags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Serialize for Flags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for flag in &self.0 {
seq.serialize_element(&SerializableFlag(flag))?;
}
seq.end()
}
}
///// Converst a string of flags into their appropriate flag representation. For example `"Seen"` is
///// gonna be convertred to `Flag::Seen`.
/////
///// # Example
///// ```rust
///// use himalaya::flag::model::Flags;
///// use imap::types::Flag;
///// use std::collections::HashSet;
/////
///// fn main() {
///// let flags = "Seen Answered";
/////
///// let mut expected = HashSet::new();
///// expected.insert(Flag::Seen);
///// expected.insert(Flag::Answered);
/////
///// let output = Flags::from(flags);
/////
///// assert_eq!(output.0, expected);
///// }
///// ```
//impl From<&str> for Flags {
// fn from(flags: &str) -> Self {
// let mut content: HashSet<Flag<'static>> = HashSet::new();
// for flag in flags.split_ascii_whitespace() {
// match flag {
// "Answered" => content.insert(Flag::Answered),
// "Deleted" => content.insert(Flag::Deleted),
// "Draft" => content.insert(Flag::Draft),
// "Flagged" => content.insert(Flag::Flagged),
// "MayCreate" => content.insert(Flag::MayCreate),
// "Recent" => content.insert(Flag::Recent),
// "Seen" => content.insert(Flag::Seen),
// custom => content.insert(Flag::Custom(Cow::Owned(custom.to_string()))),
// };
// }
// Self(content)
// }
//}
impl<'a> From<Vec<&'a str>> for Flags {
fn from(flags: Vec<&'a str>) -> Self {
let mut map: HashSet<Flag<'static>> = HashSet::new();
for f in flags {
match f {
"Answered" | _ if f.eq_ignore_ascii_case("answered") => map.insert(Flag::Answered),
"Deleted" | _ if f.eq_ignore_ascii_case("deleted") => map.insert(Flag::Deleted),
"Draft" | _ if f.eq_ignore_ascii_case("draft") => map.insert(Flag::Draft),
"Flagged" | _ if f.eq_ignore_ascii_case("flagged") => map.insert(Flag::Flagged),
"MayCreate" | _ if f.eq_ignore_ascii_case("maycreate") => {
map.insert(Flag::MayCreate)
}
"Recent" | _ if f.eq_ignore_ascii_case("recent") => map.insert(Flag::Recent),
"Seen" | _ if f.eq_ignore_ascii_case("seen") => map.insert(Flag::Seen),
custom => map.insert(Flag::Custom(Cow::Owned(custom.into()))),
};
}
Self(map)
}
}
//#[cfg(test)]
//mod tests {
// use crate::domain::msg::flag::entity::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.to_symbols_string(), " ↵ ".to_string());
// }
// #[test]
// fn test_from_string() {
// let flags = Flags::from("Seen Answered");
// let expected = Flags::from(vec![Flag::Seen, Flag::Answered]);
// assert_eq!(flags, expected);
// }
// #[test]
// fn test_to_string() {
// let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// // since we can't influence the order in the HashSet, we're gonna convert it into a vec,
// // sort it according to the names and compare it aftwards.
// let flag_string = flags.to_string();
// let mut flag_vec: Vec<String> = flag_string
// .split_ascii_whitespace()
// .map(|word| word.to_string())
// .collect();
// flag_vec.sort();
// assert_eq!(
// flag_vec,
// vec!["\\Answered".to_string(), "\\Seen".to_string()]
// );
// }
// #[test]
// fn test_from_vec() {
// let flags = Flags::from(vec![Flag::Seen, Flag::Answered]);
// let mut expected = HashSet::new();
// expected.insert(Flag::Seen);
// expected.insert(Flag::Answered);
// assert_eq!(flags.0, expected);
// }
//}

View file

@ -1,454 +0,0 @@
//! Module related to message handling.
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream;
use imap::types::Flag;
use lettre::message::header::ContentTransferEncoding;
use log::{debug, trace};
use std::{
borrow::Cow,
convert::TryFrom,
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
config::entity::Account,
domain::{
imap::service::ImapServiceInterface,
mbox::entity::Mbox,
msg::{
self,
body::entity::Body,
entity::{Msg, MsgSerialized, Msgs},
flag::entity::Flags,
header::entity::Headers,
},
smtp::service::SmtpServiceInterface,
},
output::service::OutputServiceInterface,
ui::{
choice::{self, PostEditChoice},
editor,
},
};
// TODO: move this function to the right folder
fn msg_interaction<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
output: &OutputService,
msg: &mut Msg,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<bool> {
// let the user change the body a little bit first, before opening the prompt
msg.edit_body()?;
loop {
match choice::post_edit()? {
PostEditChoice::Send => {
debug!("sending message…");
// 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(&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);
let mbox = Mbox::from("Sent");
imap.append_msg(&mbox, msg)?;
// remove the draft, since we sent it
msg::utils::remove_draft()?;
output.print("Message successfully sent")?;
break;
}
// edit the body of the msg
PostEditChoice::Edit => {
Msg::parse_from_str(msg, &editor::open_editor_with_draft()?)?;
continue;
}
PostEditChoice::LocalDraft => break,
PostEditChoice::RemoteDraft => {
debug!("saving to draft…");
msg.flags.insert(Flag::Seen);
let mbox = Mbox::from("Drafts");
match imap.append_msg(&mbox, msg) {
Ok(_) => {
msg::utils::remove_draft()?;
output.print("Message successfully saved to Drafts")?;
}
Err(err) => {
output.print("Cannot save draft to the server")?;
return Err(err.into());
}
};
break;
}
PostEditChoice::Discard => {
msg::utils::remove_draft()?;
break;
}
}
}
Ok(true)
}
/// Download all attachments from the given message UID to the user account downloads directory.
pub fn attachments<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = imap.get_msg(&uid)?;
let attachments = msg.attachments.clone();
debug!(
"{} attachment(s) found for message {}",
attachments.len(),
uid
);
for attachment in &attachments {
let filepath = account.downloads_dir.join(&attachment.filename);
debug!("downloading {}…", attachment.filename);
fs::write(&filepath, &attachment.body_raw)
.context(format!("cannot download attachment {:?}", filepath))?;
}
output.print(format!(
"{} attachment(s) successfully downloaded to {:?}",
attachments.len(),
account.downloads_dir
))?;
imap.logout()?;
Ok(())
}
/// Copy the given message UID from the selected mailbox to the targetted mailbox.
pub fn copy<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let target = Mbox::try_from(mbox)?;
let mut msg = imap.get_msg(&uid)?;
msg.flags.insert(Flag::Seen);
imap.append_msg(&target, &mut msg)?;
output.print(format!(
r#"Message {} successfully copied to folder "{}""#,
uid, target
))?;
imap.logout()?;
Ok(())
}
/// Delete the given message UID from the selected mailbox.
pub fn delete<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::from(vec![Flag::Seen, Flag::Deleted]);
imap.add_flags(uid, &flags)?;
imap.expunge()?;
output.print(format!("Message(s) {} successfully deleted", uid))?;
imap.logout()?;
Ok(())
}
/// Forward the given message UID from the selected mailbox.
pub fn forward<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
uid: &str,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
msg.change_to_forwarding(&account);
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
debug!("found {} attachments", attachments_paths.len());
trace!("attachments: {:?}", attachments_paths);
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
/// List messages with pagination from the selected mailbox.
pub fn list<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
page_size: Option<usize>,
page: usize,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
let msgs = imap.list_msgs(&page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)?
} else {
Msgs::new()
};
trace!("messages: {:#?}", msgs);
output.print(msgs)?;
imap.logout()?;
Ok(())
}
/// Parse and edit a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
url: &Url,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
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![account.address()],
to: vec![url.path().to_string()],
encoding: ContentTransferEncoding::Base64,
cc: if cc.is_empty() { None } else { Some(cc) },
bcc: if bcc.is_empty() { None } else { Some(bcc) },
subject: Some(subject.into()),
..Headers::default()
};
let mut msg = Msg::new_with_headers(&account, headers);
msg.body = Body::new_with_text(body);
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
/// Move the given message UID from the selected mailbox to the targetted mailbox.
pub fn move_<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let target = Mbox::try_from(mbox)?;
let mut msg = imap.get_msg(&uid)?;
// create the msg in the target-msgbox
msg.flags.insert(Flag::Seen);
imap.append_msg(&target, &mut msg)?;
output.print(format!(
r#"Message {} successfully moved to folder "{}""#,
uid, target
))?;
// delete the msg in the old mailbox
let flags = Flags::from(vec![Flag::Seen, Flag::Deleted]);
imap.add_flags(uid, &flags)?;
imap.expunge()?;
imap.logout()?;
Ok(())
}
/// Read a message from the given UID.
pub fn read<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
// TODO: use the mime to select the right body
_mime: String,
raw: bool,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let msg = imap.get_msg(&uid)?;
if raw {
output.print(msg.get_raw_as_string()?)?;
} else {
output.print(MsgSerialized::try_from(&msg)?)?;
}
imap.logout()?;
Ok(())
}
/// Reply to the given message UID.
pub fn reply<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
uid: &str,
all: bool,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
// Change the msg to a reply-msg.
msg.change_to_reply(&account, all)?;
// Apply the given attachments to the reply-msg.
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
debug!("found {} attachments", attachments_paths.len());
trace!("attachments: {:#?}", attachments_paths);
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}
/// Save a raw message to the targetted mailbox.
pub fn save<ImapService: ImapServiceInterface>(
mbox: Option<&str>,
msg: &str,
imap: &mut ImapService,
) -> Result<()> {
let mbox = Mbox::try_from(mbox)?;
let mut msg = Msg::try_from(msg)?;
msg.flags.insert(Flag::Seen);
imap.append_msg(&mbox, &mut msg)?;
imap.logout()?;
Ok(())
}
/// Search messages from the given IMAP query.
pub fn search<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
page_size: Option<usize>,
page: usize,
query: String,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
let msgs = imap.search_msgs(&query, &page_size, &page)?;
let msgs = if let Some(ref fetches) = msgs {
Msgs::try_from(fetches)?
} else {
Msgs::new()
};
trace!("messages: {:?}", msgs);
output.print(msgs)?;
imap.logout()?;
Ok(())
}
/// Send a raw message.
pub fn send<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
msg: &str,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let msg = if atty::is(Stream::Stdin) || output.is_json() {
msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(|ln| ln.ok())
.map(|ln| ln.to_string())
.collect::<Vec<String>>()
.join("\r\n")
};
let mut msg = Msg::try_from(msg.as_str())?;
// send the message/msg
let sendable = msg.to_sendable_msg()?;
smtp.send(&sendable)?;
debug!("message sent!");
// add the message/msg to the Sent-Mailbox of the user
msg.flags.insert(Flag::Seen);
let mbox = Mbox::from("Sent");
imap.append_msg(&mbox, &mut msg)?;
imap.logout()?;
Ok(())
}
/// Compose a new message.
pub fn write<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let mut msg = Msg::new_with_headers(
&account,
Headers {
subject: Some(String::new()),
to: Vec::new(),
..Headers::default()
},
);
attachments_paths
.iter()
.for_each(|path| msg.add_attachment(path));
msg_interaction(output, &mut msg, imap, smtp)?;
imap.logout()?;
Ok(())
}

View file

@ -1,608 +0,0 @@
use anyhow::{anyhow, Error, Result};
use lettre::message::header::ContentTransferEncoding;
use log::{debug, warn};
use rfc2047_decoder;
use serde::Serialize;
use std::{borrow::Cow, collections::HashMap, convert::TryFrom, fmt};
/// This struct is a wrapper for the [Envelope struct] of the [imap_proto]
/// crate. It's should mainly help to interact with the mails by using more
/// common data types like `Vec` or `String` since a `[u8]` array is a little
/// bit limited to use.
///
/// # Usage
/// The general idea is, that you create a new instance like that:
///
/// ```
/// use himalaya::msg::headers::Headers;
/// # fn main() {
///
/// let headers = Headers {
/// from: vec![String::from("From <address@example.com>")],
/// to: vec![String::from("To <address@to.com>")],
/// ..Headers::default()
/// };
///
/// # }
/// ```
///
/// We don't have a build-pattern here, because this is easy as well and we
/// don't need a dozens of functions, just to set some values.
///
/// [Envelope struct]: https://docs.rs/imap-proto/0.14.3/imap_proto/types/struct.Headers.html
/// [imap_proto]: https://docs.rs/imap-proto/0.14.3/imap_proto/index.html
#[derive(Debug, Serialize, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Headers {
// -- Must-Fields --
// These fields are the mininum needed to send a msg.
pub from: Vec<String>,
pub to: Vec<String>,
pub encoding: ContentTransferEncoding,
// -- Optional fields --
pub bcc: Option<Vec<String>>,
pub cc: Option<Vec<String>>,
pub custom_headers: Option<HashMap<String, Vec<String>>>,
pub in_reply_to: Option<String>,
pub message_id: Option<String>,
pub reply_to: Option<Vec<String>>,
pub sender: Option<String>,
pub subject: Option<String>,
}
impl Headers {
/// This method works similiar to the [`Display Trait`] but it will only
/// convert the header into a string **without** the signature.
///
/// # Example
///
/// <details>
///
/// ```
/// # use himalaya::msg::headers::Headers;
/// # use std::collections::HashMap;
/// # use lettre::message::header::ContentTransferEncoding;
/// # fn main() {
/// // our headers
/// let headers = Headers {
/// from: vec!["TornaxO7 <tornax07@gmail.com>".to_string()],
/// to: vec!["Soywod <clement.douin@posteo.net>".to_string()],
/// encoding: ContentTransferEncoding::Base64,
/// bcc: Some(vec!["ThirdOne <some@msg.net>".to_string()]),
/// cc: Some(vec!["CcAccount <cc@ccmail.net>".to_string()]),
/// custom_headers: None,
/// in_reply_to: Some("1234@local.machine.example".to_string()),
/// message_id: Some("123456789".to_string()),
/// reply_to: Some(vec!["reply@msg.net".to_string()]),
/// sender: Some("himalaya@secretary.net".to_string()),
/// signature: Some("Signature of Headers".to_string()),
/// subject: Some("Himalaya is cool".to_string()),
/// };
///
/// // get the header
/// let headers_string = headers.get_header_as_string();
///
/// // how the header part should look like
/// let expected_output = concat![
/// "From: TornaxO7 <tornax07@gmail.com>\n",
/// "To: Soywod <clement.douin@posteo.net>\n",
/// "In-Reply-To: 1234@local.machine.example\n",
/// "Sender: himalaya@secretary.net\n",
/// "Message-ID: 123456789\n",
/// "Reply-To: reply@msg.net\n",
/// "Cc: CcAccount <cc@ccmail.net>\n",
/// "Bcc: ThirdOne <some@msg.net>\n",
/// "Subject: Himalaya is cool\n",
/// ];
///
/// assert_eq!(headers_string, expected_output,
/// "{}, {}",
/// headers_string, expected_output);
/// # }
/// ```
///
/// </details>
///
/// [`Display Trait`]: https://doc.rust-lang.org/std/fmt/trait.Display.html
pub fn get_header_as_string(&self) -> String {
let mut header = String::new();
// -- Must-Have-Fields --
// the "From: " header
header.push_str(&merge_addresses_to_one_line("From", &self.from, ','));
// the "To: " header
header.push_str(&merge_addresses_to_one_line("To", &self.to, ','));
// -- Optional fields --
// Here we are adding only the header parts which have a value (are not
// None). That's why we are always checking here with "if let Some()".
// in reply to
if let Some(in_reply_to) = &self.in_reply_to {
header.push_str(&format!("In-Reply-To: {}\n", in_reply_to));
}
// Sender
if let Some(sender) = &self.sender {
header.push_str(&format!("Sender: {}\n", sender));
}
// Message-ID
if let Some(message_id) = &self.message_id {
header.push_str(&format!("Message-ID: {}\n", message_id));
}
// reply_to
if let Some(reply_to) = &self.reply_to {
header.push_str(&merge_addresses_to_one_line("Reply-To", &reply_to, ','));
}
// cc
if let Some(cc) = &self.cc {
header.push_str(&merge_addresses_to_one_line("Cc", &cc, ','));
}
// bcc
if let Some(bcc) = &self.bcc {
header.push_str(&merge_addresses_to_one_line("Bcc", &bcc, ','));
}
// custom headers
if let Some(custom_headers) = &self.custom_headers {
for (key, value) in custom_headers.iter() {
header.push_str(&merge_addresses_to_one_line(key, &value, ','));
}
}
// Subject
if let Some(subject) = &self.subject {
header.push_str(&format!("Subject: {}\n", subject));
}
header
}
}
/// Returns a Headers with the following values:
///
/// ```no_run
/// # use himalaya::msg::headers::Headers;
/// # use lettre::message::header::ContentTransferEncoding;
/// Headers {
/// from: Vec::new(),
/// to: Vec::new(),
/// encoding: ContentTransferEncoding::Base64,
/// bcc: None,
/// cc: None,
/// custom_headers: None,
/// in_reply_to: None,
/// message_id: None,
/// reply_to: None,
/// sender: None,
/// signature: None,
/// subject: None,
/// };
/// ```
impl Default for Headers {
fn default() -> Self {
Self {
// must-fields
from: Vec::new(),
to: Vec::new(),
encoding: ContentTransferEncoding::Base64,
// optional fields
bcc: None,
cc: None,
custom_headers: None,
in_reply_to: None,
message_id: None,
reply_to: None,
sender: None,
subject: None,
}
}
}
// == From implementations ==
impl TryFrom<Option<&imap_proto::types::Envelope<'_>>> for Headers {
type Error = Error;
fn try_from(envelope: Option<&imap_proto::types::Envelope<'_>>) -> Result<Self> {
if let Some(envelope) = envelope {
debug!("Fetch has headers.");
let subject = envelope
.subject
.as_ref()
.and_then(|subj| rfc2047_decoder::decode(subj).ok());
let from = match convert_vec_address_to_string(envelope.from.as_ref())? {
Some(from) => from,
None => return Err(anyhow!("cannot extract senders from envelope")),
};
// 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(anyhow!("cannot extract recipients from envelope")),
};
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,
encoding: ContentTransferEncoding::Base64,
})
} else {
debug!("Fetch hasn't headers.");
Ok(Headers::default())
}
}
}
impl<'from> From<&mailparse::ParsedMail<'from>> for Headers {
fn from(parsed_mail: &mailparse::ParsedMail<'from>) -> Self {
let mut new_headers = Headers::default();
let header_iter = parsed_mail.headers.iter();
for header in header_iter {
// get the value of the header. For example if we have this header:
//
// Subject: I use Arch btw
//
// than `value` would be like that: `let value = "I use Arch btw".to_string()`
let value = header.get_value().replace("\r", "");
let header_name = header.get_key().to_lowercase();
let header_name = header_name.as_str();
// now go through all headers and look which values they have.
match header_name {
"from" => {
new_headers.from = value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect()
}
"to" => {
new_headers.to = value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect()
}
"bcc" => {
new_headers.bcc = Some(
value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect(),
)
}
"cc" => {
new_headers.cc = Some(
value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect(),
)
}
"in_reply_to" => new_headers.in_reply_to = Some(value),
"reply_to" => {
new_headers.reply_to = Some(
value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect(),
)
}
"sender" => new_headers.sender = Some(value),
"subject" => new_headers.subject = Some(value),
"message-id" => new_headers.message_id = Some(value),
"content-transfer-encoding" => {
match value.to_lowercase().as_str() {
"8bit" => new_headers.encoding = ContentTransferEncoding::EightBit,
"7bit" => new_headers.encoding = ContentTransferEncoding::SevenBit,
"quoted-printable" => {
new_headers.encoding = ContentTransferEncoding::QuotedPrintable
}
"base64" => new_headers.encoding = ContentTransferEncoding::Base64,
_ => warn!("Unsupported encoding, default to QuotedPrintable"),
};
}
// it's a custom header => Add it to our
// custom-header-hash-map
_ => {
let custom_header = header.get_key();
// If we don't have a HashMap yet => Create one! Otherwise
// we'll keep using it, because why should we reset its
// values again?
if let None = new_headers.custom_headers {
new_headers.custom_headers = Some(HashMap::new());
}
let mut updated_hashmap = new_headers.custom_headers.unwrap();
updated_hashmap.insert(
custom_header,
value
.rsplit(',')
.map(|addr| addr.trim().to_string())
.collect(),
);
new_headers.custom_headers = Some(updated_hashmap);
}
}
}
new_headers
}
}
// -- Common Traits --
/// This trait just returns the headers but as a string. But be careful! **The
/// signature is printed as well!!!**, so it isn't really useable to create the
/// content of a msg! Use [get_header_as_string] instead!
///
/// # Example
///
/// ```
/// # use himalaya::msg::headers::Headers;
/// # fn main() {
/// let headers = Headers {
/// subject: Some(String::from("Himalaya is cool")),
/// to: vec![String::from("Soywod <clement.douin@posteo.net>")],
/// from: vec![String::from("TornaxO7 <tornax07@gmail.com>")],
/// signature: Some(String::from("Signature of Headers")),
/// ..Headers::default()
/// };
///
/// // use the `fmt::Display` trait
/// let headers_output = format!("{}", headers);
///
/// // How the output of the `fmt::Display` trait should look like
/// let expected_output = concat![
/// "From: TornaxO7 <tornax07@gmail.com>\n",
/// "To: Soywod <clement.douin@posteo.net>\n",
/// "Subject: Himalaya is cool\n",
/// "\n\n\n",
/// "Signature of Headers",
/// ];
///
/// assert_eq!(headers_output, expected_output,
/// "{:#?}, {:#?}",
/// headers_output, expected_output);
/// # }
/// ```
///
/// [get_header_as_string]: struct.Headers.html#method.get_header_as_string
impl fmt::Display for Headers {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "{}", self.get_header_as_string())
}
}
// -- Helper functions --
/// This function is mainly used for the `imap_proto::types::Address` struct to
/// convert one field into a String. Take a look into the
/// `test_convert_cow_u8_to_string` test function to see it in action.
fn convert_cow_u8_to_string<'val>(value: Option<&Cow<'val, [u8]>>) -> Result<Option<String>> {
if let Some(value) = value {
// convert the `[u8]` list into a vector and try to get a string out of
// it. If everything worked fine, return the content of the list
Ok(Some(rfc2047_decoder::decode(&value.to_vec())?))
} else {
Ok(None)
}
}
/// This function is mainly used for the `imap_proto::types::Address` struct as
/// well to change the Address into an address-string like this:
/// `TornaxO7 <tornax07@gmail.com>`.
///
/// If you provide two addresses as the function argument, then this functions
/// returns their "parsed" address in the same order. Take a look into the
/// `test_convert_vec_address_to_string` for an example.
fn convert_vec_address_to_string<'val>(
addresses: Option<&Vec<imap_proto::types::Address<'val>>>,
) -> Result<Option<Vec<String>>> {
if let Some(addresses) = addresses {
let mut parsed_addresses: Vec<String> = Vec::new();
for address in addresses.iter() {
// This variable will hold the parsed version of the Address-struct,
// like this:
//
// "Name <msg@host>"
let mut parsed_address = String::new();
// -- Get the fields --
// add the name field (if it exists) like this:
// "Name"
if let Some(name) = convert_cow_u8_to_string(address.name.as_ref())? {
parsed_address.push_str(&name);
}
// add the mailaddress
if let Some(mailbox) = convert_cow_u8_to_string(address.mailbox.as_ref())? {
if let Some(host) = convert_cow_u8_to_string(address.host.as_ref())? {
let mail_address = format!("{}@{}", mailbox, host);
// some mail clients add a trailing space, after the address
let trimmed = mail_address.trim();
if parsed_address.is_empty() {
// parsed_address = "msg@host"
parsed_address.push_str(&trimmed);
} else {
// parsed_address = "Name <msg@host>"
parsed_address.push_str(&format!(" <{}>", trimmed));
}
}
}
parsed_addresses.push(parsed_address);
}
Ok(Some(parsed_addresses))
} else {
Ok(None)
}
}
/// This function is used, in order to merge multiple msg accounts into one
/// line. Take a look into the `test_merge_addresses_to_one_line` test-function
/// to see an example how to use it.
fn merge_addresses_to_one_line(header: &str, addresses: &Vec<String>, separator: char) -> String {
let mut output = header.to_string();
let mut address_iter = addresses.iter();
// Convert the header to this (for example): `Cc: `
output.push_str(": ");
// the first emsg doesn't need a comma before, so we should append the msg
// to it
output.push_str(address_iter.next().unwrap_or(&String::new()));
// add the rest of the emails. It should look like this after the for_each:
//
// Addr1, Addr2, Addr2, ...
address_iter.for_each(|address| output.push_str(&format!("{}{}", separator, address)));
// end the header-line by using a newline character
output.push('\n');
output
}
// ==========
// Tests
// ==========
/// This tests only test the helper functions.
#[cfg(test)]
mod tests {
#[test]
fn test_merge_addresses_to_one_line() {
use super::merge_addresses_to_one_line;
// In this function, we want to create the following Cc header:
//
// Cc: TornaxO7 <tornax07@gmail.com>, Soywod <clement.douin@posteo.net>
//
// by a vector of email-addresses.
// our msg addresses for the "Cc" header
let mail_addresses = vec![
"TornaxO7 <tornax07@gmail.com>".to_string(),
"Soywod <clement.douin@posteo.net>".to_string(),
];
let cc_header = merge_addresses_to_one_line("Cc", &mail_addresses, ',');
let expected_output = concat![
"Cc: TornaxO7 <tornax07@gmail.com>",
",",
"Soywod <clement.douin@posteo.net>\n",
];
assert_eq!(
cc_header, expected_output,
"{:#?}, {:#?}",
cc_header, expected_output
);
}
#[test]
fn test_convert_cow_u8_to_string() {
use super::convert_cow_u8_to_string;
use std::borrow::Cow;
let output1 = convert_cow_u8_to_string(None);
let output2 = convert_cow_u8_to_string(Some(&Cow::Owned(b"Test".to_vec())));
// test output1
if let Ok(output1) = output1 {
assert!(output1.is_none());
} else {
assert!(false);
}
// test output2
if let Ok(output2) = output2 {
if let Some(string) = output2 {
assert_eq!(String::from("Test"), string);
} else {
assert!(false);
}
} else {
assert!(false);
}
}
#[test]
fn test_convert_vec_address_to_string() {
use super::convert_vec_address_to_string;
use imap_proto::types::Address;
use std::borrow::Cow;
let addresses = vec![
Address {
name: Some(Cow::Owned(b"Name1".to_vec())),
adl: None,
mailbox: Some(Cow::Owned(b"Mailbox1".to_vec())),
host: Some(Cow::Owned(b"Host1".to_vec())),
},
Address {
name: None,
adl: None,
mailbox: Some(Cow::Owned(b"Mailbox2".to_vec())),
host: Some(Cow::Owned(b"Host2".to_vec())),
},
];
// the expected addresses
let expected_output = vec![
String::from("Name1 <Mailbox1@Host1>"),
String::from("Mailbox2@Host2"),
];
if let Ok(converted) = convert_vec_address_to_string(Some(&addresses)) {
assert_eq!(converted, Some(expected_output));
} else {
assert!(false);
}
}
}

View file

@ -1 +0,0 @@
pub mod entity;

View file

@ -18,24 +18,36 @@
/// ///
/// Execute `himalaya help <cmd>` where `<cmd>` is one entry of this list above /// Execute `himalaya help <cmd>` where `<cmd>` is one entry of this list above
/// to get more information about them. /// to get more information about them.
pub mod arg; pub mod msg_arg;
/// Here are the two **main structs** of this module: `Msg` and `Msgs` which pub mod msg_handler;
/// represent a *Mail* or *multiple Mails* in this crate. pub mod msg_utils;
pub mod entity;
/// This module is used in the `Msg` struct, which should represent an pub mod flag_arg;
/// attachment of a msg. pub mod flag_handler;
pub mod attachment;
/// This module is used in the `Msg` struct, which should represent the headers pub mod flag_entity;
/// fields like `To:` and `From:`. pub use flag_entity::*;
pub mod header;
/// This module is used in the `Msg` struct, which should represent the body of pub mod flags_entity;
/// a msg; The part where you're writing some text like `Dear Mr. LMAO`. pub use flags_entity::*;
pub mod body;
pub mod flag; pub mod envelope_entity;
pub mod handler; pub use envelope_entity::*;
pub mod tpl;
pub mod utils; pub mod envelopes_entity;
pub use envelopes_entity::*;
pub mod tpl_arg;
pub use tpl_arg::TplOverride;
pub mod tpl_handler;
pub mod tpl_entity;
pub use tpl_entity::*;
pub mod msg_entity;
pub use msg_entity::*;
pub mod parts_entity;
pub use parts_entity::*;

View file

@ -4,14 +4,17 @@
use anyhow::Result; use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand}; use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::debug; use log::{debug, trace};
use crate::domain::{mbox, msg}; use crate::domain::{
mbox::mbox_arg,
msg::{flag_arg, msg_arg, tpl_arg},
};
type Uid<'a> = &'a str; type Seq<'a> = &'a str;
type PageSize = usize; type PageSize = usize;
type Page = usize; type Page = usize;
type TargetMbox<'a> = Option<&'a str>; type Mbox<'a> = Option<&'a str>;
type Mime = String; type Mime = String;
type Raw = bool; type Raw = bool;
type All = bool; type All = bool;
@ -21,61 +24,61 @@ type AttachmentsPaths<'a> = Vec<&'a str>;
/// Message commands. /// Message commands.
pub enum Command<'a> { pub enum Command<'a> {
Attachments(Uid<'a>), Attachments(Seq<'a>),
Copy(Uid<'a>, TargetMbox<'a>), Copy(Seq<'a>, Mbox<'a>),
Delete(Uid<'a>), Delete(Seq<'a>),
Forward(Uid<'a>, AttachmentsPaths<'a>), Forward(Seq<'a>, AttachmentsPaths<'a>),
List(Option<PageSize>, Page), List(Option<PageSize>, Page),
Move(Uid<'a>, TargetMbox<'a>), Move(Seq<'a>, Mbox<'a>),
Read(Uid<'a>, Mime, Raw), Read(Seq<'a>, Mime, Raw),
Reply(Uid<'a>, All, AttachmentsPaths<'a>), Reply(Seq<'a>, All, AttachmentsPaths<'a>),
Save(TargetMbox<'a>, RawMsg<'a>), Save(Mbox<'a>, RawMsg<'a>),
Search(Query, Option<PageSize>, Page), Search(Query, Option<PageSize>, Page),
Send(RawMsg<'a>), Send(RawMsg<'a>),
Write(AttachmentsPaths<'a>), Write(AttachmentsPaths<'a>),
Flag(Option<msg::flag::arg::Command<'a>>), Flag(Option<flag_arg::Command<'a>>),
Tpl(Option<msg::tpl::arg::Command<'a>>), Tpl(Option<tpl_arg::Command<'a>>),
} }
/// Message command matcher. /// Message command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("attachments") { if let Some(m) = m.subcommand_matches("attachments") {
debug!("attachments command matched"); debug!("attachments command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!("seq: {}", seq);
return Ok(Some(Command::Attachments(uid))); return Ok(Some(Command::Attachments(seq)));
} }
if let Some(m) = m.subcommand_matches("copy") { if let Some(m) = m.subcommand_matches("copy") {
debug!("copy command matched"); debug!("copy command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!("seq: {}", seq);
let target = m.value_of("target"); let target = m.value_of("target");
debug!("target mailbox: `{:?}`", target); trace!(r#"target mailbox: "{:?}""#, target);
return Ok(Some(Command::Copy(uid, target))); return Ok(Some(Command::Copy(seq, target)));
} }
if let Some(m) = m.subcommand_matches("delete") { if let Some(m) = m.subcommand_matches("delete") {
debug!("copy command matched"); debug!("copy command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!("seq: {}", seq);
return Ok(Some(Command::Delete(uid))); return Ok(Some(Command::Delete(seq)));
} }
if let Some(m) = m.subcommand_matches("forward") { if let Some(m) = m.subcommand_matches("forward") {
debug!("forward command matched"); debug!("forward command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
trace!("seq: {}", seq);
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", paths); trace!("attachments paths: {:?}", paths);
debug!("uid: {}", uid); return Ok(Some(Command::Forward(seq, paths)));
return Ok(Some(Command::Forward(uid, paths)));
} }
if let Some(m) = m.subcommand_matches("list") { if let Some(m) = m.subcommand_matches("list") {
debug!("list command matched"); debug!("list command matched");
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: `{:?}`", page_size); trace!(r#"page size: "{:?}""#, page_size);
let page = m let page = m
.value_of("page") .value_of("page")
.unwrap_or("1") .unwrap_or("1")
@ -83,38 +86,39 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.ok() .ok()
.map(|page| 1.max(page) - 1) .map(|page| 1.max(page) - 1)
.unwrap_or_default(); .unwrap_or_default();
debug!("page: `{:?}`", page); trace!(r#"page: "{:?}""#, page);
return Ok(Some(Command::List(page_size, page))); return Ok(Some(Command::List(page_size, page)));
} }
if let Some(m) = m.subcommand_matches("move") { if let Some(m) = m.subcommand_matches("move") {
debug!("move command matched"); debug!("move command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!("seq: {}", seq);
let target = m.value_of("target"); let target = m.value_of("target");
debug!("target mailbox: `{:?}`", target); trace!(r#"target mailbox: "{:?}""#, target);
return Ok(Some(Command::Move(uid, target))); return Ok(Some(Command::Move(seq, target)));
} }
if let Some(m) = m.subcommand_matches("read") { if let Some(m) = m.subcommand_matches("read") {
let uid = m.value_of("uid").unwrap(); debug!("read command matched");
debug!("uid: {}", uid); let seq = m.value_of("seq").unwrap();
trace!("seq: {}", seq);
let mime = format!("text/{}", m.value_of("mime-type").unwrap()); let mime = format!("text/{}", m.value_of("mime-type").unwrap());
debug!("mime: {}", mime); trace!("mime: {}", mime);
let raw = m.is_present("raw"); let raw = m.is_present("raw");
debug!("raw: {}", raw); trace!("raw: {}", raw);
return Ok(Some(Command::Read(uid, mime, raw))); return Ok(Some(Command::Read(seq, mime, raw)));
} }
if let Some(m) = m.subcommand_matches("reply") { if let Some(m) = m.subcommand_matches("reply") {
debug!("reply command matched"); debug!("reply command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!("seq: {}", seq);
let all = m.is_present("reply-all"); let all = m.is_present("reply-all");
debug!("reply all: {}", all); trace!("reply all: {}", all);
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", paths); trace!("attachments paths: {:#?}", paths);
return Ok(Some(Command::Reply(uid, all, paths))); return Ok(Some(Command::Reply(seq, all, paths)));
} }
if let Some(m) = m.subcommand_matches("save") { if let Some(m) = m.subcommand_matches("save") {
@ -129,7 +133,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("search") { if let Some(m) = m.subcommand_matches("search") {
debug!("search command matched"); debug!("search command matched");
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok()); let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: `{:?}`", page_size); trace!(r#"page size: "{:?}""#, page_size);
let page = m let page = m
.value_of("page") .value_of("page")
.unwrap() .unwrap()
@ -137,7 +141,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
.ok() .ok()
.map(|page| 1.max(page) - 1) .map(|page| 1.max(page) - 1)
.unwrap_or_default(); .unwrap_or_default();
debug!("page: `{:?}`", page); trace!(r#"page: "{:?}""#, page);
let query = m let query = m
.values_of("query") .values_of("query")
.unwrap_or_default() .unwrap_or_default()
@ -162,40 +166,50 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
}) })
.1 .1
.join(" "); .join(" ");
trace!(r#"query: "{:?}""#, query);
return Ok(Some(Command::Search(query, page_size, page))); return Ok(Some(Command::Search(query, page_size, page)));
} }
if let Some(m) = m.subcommand_matches("send") { if let Some(m) = m.subcommand_matches("send") {
debug!("send command matched"); debug!("send command matched");
let msg = m.value_of("message").unwrap_or_default(); let msg = m.value_of("message").unwrap_or_default();
debug!("message: {}", msg); trace!("message: {}", msg);
return Ok(Some(Command::Send(msg))); return Ok(Some(Command::Send(msg)));
} }
if let Some(m) = m.subcommand_matches("write") { if let Some(m) = m.subcommand_matches("write") {
debug!("write command matched"); debug!("write command matched");
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect(); let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", attachment_paths); trace!("attachments paths: {:?}", attachment_paths);
return Ok(Some(Command::Write(attachment_paths))); return Ok(Some(Command::Write(attachment_paths)));
} }
if let Some(m) = m.subcommand_matches("template") { if let Some(m) = m.subcommand_matches("template") {
return Ok(Some(Command::Tpl(msg::tpl::arg::matches(&m)?))); return Ok(Some(Command::Tpl(tpl_arg::matches(&m)?)));
} }
if let Some(m) = m.subcommand_matches("flag") { if let Some(m) = m.subcommand_matches("flag") {
return Ok(Some(Command::Flag(msg::flag::arg::matches(&m)?))); return Ok(Some(Command::Flag(flag_arg::matches(&m)?)));
} }
debug!("default list command matched"); debug!("default list command matched");
Ok(Some(Command::List(None, 0))) Ok(Some(Command::List(None, 0)))
} }
/// Message UID argument. /// Message sequence number argument.
pub(crate) fn uid_arg<'a>() -> Arg<'a, 'a> { pub(crate) fn seq_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("uid") Arg::with_name("seq")
.help("Specifies the targetted message") .help("Specifies the targetted message")
.value_name("UID") .value_name("SEQ")
.required(true)
}
/// Message sequence range argument.
pub(crate) fn seq_range_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("seq-range")
.help("Specifies targetted message(s)")
.long_help("Specifies a range of targetted messages. The range follows the [RFC3501](https://datatracker.ietf.org/doc/html/rfc3501#section-9) format: `1:5` matches messages with sequence number between 1 and 5, `1,5` matches messages with sequence number 1 or 5, * matches all messages.")
.value_name("SEQ")
.required(true) .required(true)
} }
@ -226,13 +240,26 @@ fn page_arg<'a>() -> Arg<'a, 'a> {
.default_value("0") .default_value("0")
} }
/// Message attachment argument.
fn attachment_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("attachments")
.help("Adds attachment to the message")
.short("a")
.long("attachment")
.value_name("PATH")
.multiple(true)
}
/// Message subcommands. /// Message subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> { pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![ vec![
msg::flag::arg::subcmds(), flag_arg::subcmds(),
msg::tpl::arg::subcmds(), tpl_arg::subcmds(),
msg::attachment::arg::subcmds(),
vec![ vec![
SubCommand::with_name("attachments")
.aliases(&["attachment", "att", "a"])
.about("Downloads all message attachments")
.arg(msg_arg::seq_arg()),
SubCommand::with_name("list") SubCommand::with_name("list")
.aliases(&["lst", "l"]) .aliases(&["lst", "l"])
.about("Lists all messages") .about("Lists all messages")
@ -245,14 +272,15 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.arg(page_arg()) .arg(page_arg())
.arg( .arg(
Arg::with_name("query") Arg::with_name("query")
.help("IMAP query (see https://tools.ietf.org/html/rfc3501#section-6.4.4)") .help("IMAP query")
.long_help("The IMAP query format follows the [RFC3501](https://tools.ietf.org/html/rfc3501#section-6.4.4). The query is case-insensitive.")
.value_name("QUERY") .value_name("QUERY")
.multiple(true) .multiple(true)
.required(true), .required(true),
), ),
SubCommand::with_name("write") SubCommand::with_name("write")
.about("Writes a new message") .about("Writes a new message")
.arg(msg::attachment::arg::path_arg()), .arg(attachment_arg()),
SubCommand::with_name("send") SubCommand::with_name("send")
.about("Sends a raw message") .about("Sends a raw message")
.arg(Arg::with_name("message").raw(true).last(true)), .arg(Arg::with_name("message").raw(true).last(true)),
@ -261,7 +289,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
.arg(Arg::with_name("message").raw(true)), .arg(Arg::with_name("message").raw(true)),
SubCommand::with_name("read") SubCommand::with_name("read")
.about("Reads text bodies of a message") .about("Reads text bodies of a message")
.arg(uid_arg()) .arg(seq_arg())
.arg( .arg(
Arg::with_name("mime-type") Arg::with_name("mime-type")
.help("MIME type to use") .help("MIME type to use")
@ -280,28 +308,28 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
SubCommand::with_name("reply") SubCommand::with_name("reply")
.aliases(&["rep", "r"]) .aliases(&["rep", "r"])
.about("Answers to a message") .about("Answers to a message")
.arg(uid_arg()) .arg(seq_arg())
.arg(reply_all_arg()) .arg(reply_all_arg())
.arg(msg::attachment::arg::path_arg()), .arg(attachment_arg()),
SubCommand::with_name("forward") SubCommand::with_name("forward")
.aliases(&["fwd", "f"]) .aliases(&["fwd", "f"])
.about("Forwards a message") .about("Forwards a message")
.arg(uid_arg()) .arg(seq_arg())
.arg(msg::attachment::arg::path_arg()), .arg(attachment_arg()),
SubCommand::with_name("copy") SubCommand::with_name("copy")
.aliases(&["cp", "c"]) .aliases(&["cp", "c"])
.about("Copies a message to the targetted mailbox") .about("Copies a message to the targetted mailbox")
.arg(uid_arg()) .arg(seq_arg())
.arg(mbox::arg::target_arg()), .arg(mbox_arg::target_arg()),
SubCommand::with_name("move") SubCommand::with_name("move")
.aliases(&["mv"]) .aliases(&["mv"])
.about("Moves a message to the targetted mailbox") .about("Moves a message to the targetted mailbox")
.arg(uid_arg()) .arg(seq_arg())
.arg(mbox::arg::target_arg()), .arg(mbox_arg::target_arg()),
SubCommand::with_name("delete") SubCommand::with_name("delete")
.aliases(&["del", "d", "remove", "rm"]) .aliases(&["del", "d", "remove", "rm"])
.about("Deletes a message") .about("Deletes a message")
.arg(uid_arg()), .arg(seq_arg()),
], ],
] ]
.concat() .concat()

View file

@ -0,0 +1,814 @@
use ammonia;
use anyhow::{anyhow, Context, Error, Result};
use chrono::{DateTime, FixedOffset};
use htmlescape;
use imap::types::Flag;
use lettre::message::{Attachment, MultiPart, SinglePart};
use regex::Regex;
use rfc2047_decoder;
use serde::Serialize;
use std::{
convert::{TryFrom, TryInto},
fmt, fs,
path::PathBuf,
};
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
mbox::Mbox,
msg::{msg_utils, Flags, Parts, TextHtmlPart, TextPlainPart, Tpl, TplOverride},
smtp::SmtpServiceInterface,
},
output::OutputServiceInterface,
ui::{
choice::{self, PostEditChoice, PreEditChoice},
editor,
},
};
use super::{BinaryPart, Part};
type Addr = lettre::message::Mailbox;
/// Representation of a message.
#[derive(Debug, Default)]
pub struct Msg {
/// The sequence number of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
pub id: u32,
/// The flags attached to the message.
pub flags: Flags,
/// The subject of the message.
pub subject: String,
pub from: Option<Vec<Addr>>,
pub reply_to: Option<Vec<Addr>>,
pub to: Option<Vec<Addr>>,
pub cc: Option<Vec<Addr>>,
pub bcc: Option<Vec<Addr>>,
pub in_reply_to: Option<String>,
pub message_id: Option<String>,
/// The internal date of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
pub date: Option<DateTime<FixedOffset>>,
pub parts: Parts,
}
impl Msg {
pub fn attachments(&self) -> Vec<BinaryPart> {
self.parts
.iter()
.filter_map(|part| match part {
Part::Binary(part) => Some(part.clone()),
_ => None,
})
.collect()
}
pub fn join_text_plain_parts(&self) -> String {
let text_parts = self
.parts
.iter()
.filter_map(|part| match part {
Part::TextPlain(part) => Some(part.content.to_owned()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n\n");
let text_parts = ammonia::Builder::new()
.tags(Default::default())
.clean(&text_parts)
.to_string();
let text_parts = match htmlescape::decode_html(&text_parts) {
Ok(text_parts) => text_parts,
Err(_) => text_parts,
};
text_parts
}
pub fn join_text_html_parts(&self) -> String {
let text_parts = self
.parts
.iter()
.filter_map(|part| match part {
Part::TextPlain(part) => Some(part.content.to_owned()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n\n");
let text_parts = Regex::new(r"(\r?\n){2,}")
.unwrap()
.replace_all(&text_parts, "\n\n")
.to_string();
text_parts
}
pub fn join_text_parts(&self) -> String {
let text_parts = self.join_text_plain_parts();
if text_parts.is_empty() {
self.join_text_html_parts()
} else {
text_parts
}
}
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
let account_addr: Addr = account.address().parse()?;
// Message-Id
self.message_id = None;
// In-Reply-To
self.in_reply_to = self.message_id.to_owned();
// From
self.from = Some(vec![account_addr.to_owned()]);
// To
let addrs = self
.reply_to
.as_ref()
.or_else(|| self.from.as_ref())
.map(|addrs| {
addrs
.clone()
.into_iter()
.filter(|addr| addr != &account_addr)
});
if all {
self.to = addrs.map(|addrs| addrs.collect());
} else {
self.to = addrs
.and_then(|mut addrs| addrs.next())
.map(|addr| vec![addr]);
}
// Cc & Bcc
if !all {
self.cc = None;
self.bcc = None;
}
// Subject
if !self.subject.starts_with("Re:") {
self.subject = format!("Re: {}", self.subject);
}
// Text plain parts
let plain_content = {
let date = self
.date
.as_ref()
.map(|date| date.format("%d %b %Y, at %H:%M").to_string())
.unwrap_or("unknown date".into());
let sender = self
.reply_to
.as_ref()
.or(self.from.as_ref())
.and_then(|addrs| addrs.first())
.map(|addr| addr.name.to_owned().unwrap_or(addr.email.to_string()))
.unwrap_or("unknown sender".into());
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
let mut glue = "";
for line in self.join_text_plain_parts().trim().lines() {
if line == "-- \n" {
break;
}
content.push_str(glue);
content.push_str(">");
content.push_str(if line.starts_with(">") { "" } else { " " });
content.push_str(line);
glue = "\n";
}
content
};
// Text HTML parts
let html_content = {
let date = self
.date
.as_ref()
.map(|date| date.format("%d %b %Y, at %H:%M").to_string())
.unwrap_or("unknown date".into());
let sender = self
.reply_to
.as_ref()
.or(self.from.as_ref())
.and_then(|addrs| addrs.first())
.map(|addr| addr.name.to_owned().unwrap_or(addr.email.to_string()))
.unwrap_or("unknown sender".into());
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
let mut glue = "";
for line in self.join_text_html_parts().trim().lines() {
if line == "-- \n" {
break;
}
content.push_str(glue);
content.push_str(">");
content.push_str(if line.starts_with(">") { "" } else { " " });
content.push_str(line);
glue = "\n";
}
content
};
self.parts = Parts::default();
if !plain_content.is_empty() {
self.parts.push(Part::TextPlain(TextPlainPart {
content: plain_content,
}));
}
if !html_content.is_empty() {
self.parts.push(Part::TextHtml(TextHtmlPart {
content: html_content,
}));
}
Ok(self)
}
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
let account_addr: Addr = account.address().parse()?;
let prev_subject = self.subject.to_owned();
let prev_date = self.date.to_owned();
let prev_from = self.reply_to.to_owned().or_else(|| self.from.to_owned());
let prev_to = self.to.to_owned();
// Message-Id
self.message_id = None;
// In-Reply-To
self.in_reply_to = None;
// From
self.from = Some(vec![account_addr.to_owned()]);
// To
self.to = Some(vec![]);
// Cc
self.cc = None;
// Bcc
self.bcc = None;
// Subject
if !self.subject.starts_with("Fwd:") {
self.subject = format!("Fwd: {}", self.subject);
}
// Text plain parts
{
let mut content = String::default();
content.push_str("\n\n-------- Forwarded Message --------\n");
content.push_str(&format!("Subject: {}\n", prev_subject));
if let Some(date) = prev_date {
content.push_str(&format!("Date: {}\n", date.to_rfc2822()));
}
if let Some(addrs) = prev_from.as_ref() {
content.push_str("From: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str("\n");
}
if let Some(addrs) = prev_to.as_ref() {
content.push_str("To: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str("\n");
}
content.push_str("\n");
content.push_str(&self.join_text_plain_parts());
self.parts
.replace_text_plain_parts_with(TextPlainPart { content })
}
// Text HTML parts
{
let mut content = String::default();
content.push_str("\n\n-------- Forwarded Message --------\n");
content.push_str(&format!("Subject: {}\n", prev_subject));
if let Some(date) = prev_date {
content.push_str(&format!("Date: {}\n", date.to_rfc2822()));
}
if let Some(addrs) = prev_from.as_ref() {
content.push_str("From: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str("\n");
}
if let Some(addrs) = prev_to.as_ref() {
content.push_str("To: ");
let mut glue = "";
for addr in addrs {
content.push_str(glue);
content.push_str(&addr.to_string());
glue = ", ";
}
content.push_str("\n");
}
content.push_str("\n");
content.push_str(&self.join_text_html_parts());
self.parts
.replace_text_html_parts_with(TextHtmlPart { content })
}
Ok(self)
}
fn _edit_with_editor(&self, account: &Account) -> Result<Self> {
let tpl = Tpl::from_msg(TplOverride::default(), self, account);
let tpl = editor::open_with_tpl(tpl)?;
Self::try_from(&tpl)
}
pub fn edit_with_editor<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
mut self,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let draft = msg_utils::local_draft_path();
if draft.exists() {
loop {
match choice::pre_edit() {
Ok(choice) => match choice {
PreEditChoice::Edit => {
let tpl = editor::open_with_draft()?;
self.merge_with(Msg::try_from(&tpl)?);
break;
}
PreEditChoice::Discard => {
self.merge_with(self._edit_with_editor(account)?);
break;
}
PreEditChoice::Quit => return Ok(()),
},
Err(err) => {
println!("{}", err);
continue;
}
}
}
} else {
self.merge_with(self._edit_with_editor(account)?);
}
loop {
match choice::post_edit() {
Ok(PostEditChoice::Send) => {
let mbox = Mbox::from("Sent");
let sent_msg = smtp.send_msg(&self)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &sent_msg.formatted(), flags)?;
msg_utils::remove_local_draft()?;
output.print("Message successfully sent")?;
break;
}
Ok(PostEditChoice::Edit) => {
self.merge_with(self._edit_with_editor(account)?);
continue;
}
Ok(PostEditChoice::LocalDraft) => {
output.print("Message successfully saved locally")?;
break;
}
Ok(PostEditChoice::RemoteDraft) => {
let mbox = Mbox::from("Drafts");
let flags = Flags::try_from(vec![Flag::Seen, Flag::Draft])?;
let tpl = Tpl::from_msg(TplOverride::default(), &self, account);
imap.append_raw_msg_with_flags(&mbox, tpl.as_bytes(), flags)?;
msg_utils::remove_local_draft()?;
output.print("Message successfully saved to Drafts")?;
break;
}
Ok(PostEditChoice::Discard) => {
msg_utils::remove_local_draft()?;
break;
}
Err(err) => {
println!("{}", err);
continue;
}
}
}
Ok(())
}
pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result<Self> {
for path in attachments_paths {
let path = shellexpand::full(path)
.context(format!(r#"cannot expand attachment path "{}""#, path))?;
let path = PathBuf::from(path.to_string());
let filename: String = path
.file_name()
.ok_or(anyhow!("cannot get file name of attachment {:?}", path))?
.to_string_lossy()
.into();
let content = fs::read(&path).context(format!("cannot read attachment {:?}", path))?;
let mime = tree_magic::from_u8(&content);
self.parts.push(Part::Binary(BinaryPart {
filename,
mime,
content,
}))
}
Ok(self)
}
pub fn merge_with(&mut self, msg: Msg) {
if msg.from.is_some() {
self.from = msg.from;
}
if msg.to.is_some() {
self.to = msg.to;
}
if msg.cc.is_some() {
self.cc = msg.cc;
}
if msg.bcc.is_some() {
self.bcc = msg.bcc;
}
if !msg.subject.is_empty() {
self.subject = msg.subject;
}
for part in msg.parts.0.into_iter() {
match part {
Part::Binary(_) => self.parts.push(part),
Part::TextPlain(_) => {
self.parts.retain(|p| match p {
Part::TextPlain(_) => false,
_ => true,
});
self.parts.push(part);
}
Part::TextHtml(_) => {
self.parts.retain(|p| match p {
Part::TextHtml(_) => false,
_ => true,
});
self.parts.push(part);
}
}
}
}
}
impl TryFrom<&Tpl> for Msg {
type Error = Error;
fn try_from(tpl: &Tpl) -> Result<Msg> {
let mut msg = Msg::default();
let parsed_msg =
mailparse::parse_mail(tpl.as_bytes()).context("cannot parse message from template")?;
for header in parsed_msg.get_headers() {
let key = header.get_key();
let val = String::from_utf8(header.get_value_raw().to_vec())
.map(|val| val.trim().to_string())?;
match key.as_str() {
"Message-Id" | _ if key.eq_ignore_ascii_case("message-id") => {
msg.message_id = Some(val.to_owned())
}
"From" | _ if key.eq_ignore_ascii_case("from") => {
msg.from = Some(
val.split(',')
.filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(),
);
}
"To" | _ if key.eq_ignore_ascii_case("to") => {
msg.to = Some(
val.split(',')
.filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(),
);
}
"Reply-To" | _ if key.eq_ignore_ascii_case("reply-to") => {
msg.reply_to = Some(
val.split(',')
.filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(),
);
}
"In-Reply-To" | _ if key.eq_ignore_ascii_case("in-reply-to") => {
msg.in_reply_to = Some(val.to_owned())
}
"Cc" | _ if key.eq_ignore_ascii_case("cc") => {
msg.cc = Some(
val.split(',')
.filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(),
);
}
"Bcc" | _ if key.eq_ignore_ascii_case("bcc") => {
msg.bcc = Some(
val.split(',')
.filter_map(|addr| addr.parse().ok())
.collect::<Vec<_>>(),
);
}
"Subject" | _ if key.eq_ignore_ascii_case("subject") => {
msg.subject = val;
}
_ => (),
}
}
let content = parsed_msg
.get_body_raw()
.context("cannot get body from parsed message")?;
let content = String::from_utf8(content).context("cannot decode body from utf-8")?;
msg.parts.push(Part::TextPlain(TextPlainPart { content }));
Ok(msg)
}
}
impl TryInto<lettre::address::Envelope> for Msg {
type Error = Error;
fn try_into(self) -> Result<lettre::address::Envelope> {
let from: Option<lettre::Address> = self
.from
.and_then(|addrs| addrs.into_iter().next())
.map(|addr| addr.email);
let to = self
.to
.map(|addrs| addrs.into_iter().map(|addr| addr.email).collect())
.unwrap_or_default();
let envelope =
lettre::address::Envelope::new(from, to).context("cannot create envelope")?;
Ok(envelope)
}
}
impl TryInto<lettre::Message> for &Msg {
type Error = Error;
fn try_into(self) -> Result<lettre::Message> {
let mut msg_builder = lettre::Message::builder()
.message_id(self.message_id.to_owned())
.subject(self.subject.to_owned());
if let Some(id) = self.in_reply_to.as_ref() {
msg_builder = msg_builder.in_reply_to(id.to_owned());
};
if let Some(addrs) = self.from.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.from(addr.to_owned()))
};
if let Some(addrs) = self.to.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.to(addr.to_owned()))
};
if let Some(addrs) = self.reply_to.as_ref() {
msg_builder = addrs.iter().fold(msg_builder, |builder, addr| {
builder.reply_to(addr.to_owned())
})
};
if let Some(addrs) = self.cc.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.cc(addr.to_owned()))
};
if let Some(addrs) = self.bcc.as_ref() {
msg_builder = addrs
.iter()
.fold(msg_builder, |builder, addr| builder.bcc(addr.to_owned()))
};
let mut multipart =
MultiPart::mixed().singlepart(SinglePart::plain(self.join_text_plain_parts()));
for part in self.attachments() {
let filename = part.filename;
let content = part.content;
let mime = part.mime.parse().context(format!(
r#"cannot parse content type of attachment "{}""#,
filename
))?;
multipart = multipart.singlepart(Attachment::new(filename).body(content, mime))
}
msg_builder
.multipart(multipart)
.context("cannot build sendable message")
}
}
impl TryInto<Vec<u8>> for &Msg {
type Error = Error;
fn try_into(self) -> Result<Vec<u8>> {
let msg: lettre::Message = self.try_into()?;
Ok(msg.formatted())
}
}
impl<'a> TryFrom<&'a imap::types::Fetch> for Msg {
type Error = Error;
fn try_from(fetch: &'a imap::types::Fetch) -> Result<Msg> {
let envelope = fetch
.envelope()
.ok_or(anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number
let id = fetch.message;
// Get the flags
let flags = Flags::try_from(fetch.flags())?;
// Get the subject
let subject = envelope
.subject
.as_ref()
.ok_or(anyhow!("cannot get subject of message {}", fetch.message))
.and_then(|subj| {
rfc2047_decoder::decode(subj).context(format!(
"cannot decode subject of message {}",
fetch.message
))
})?;
// Get the sender(s) address(es)
let from = match envelope
.sender
.as_ref()
.or_else(|| envelope.from.as_ref())
.map(parse_addrs)
{
Some(addrs) => Some(addrs?),
None => None,
};
// Get the "Reply-To" address(es)
let reply_to = parse_some_addrs(&envelope.reply_to).context(format!(
r#"cannot parse "reply to" address of message {}"#,
id
))?;
// Get the recipient(s) address(es)
let to = parse_some_addrs(&envelope.to)
.context(format!(r#"cannot parse "to" address of message {}"#, id))?;
// Get the "Cc" recipient(s) address(es)
let cc = parse_some_addrs(&envelope.cc)
.context(format!(r#"cannot parse "cc" address of message {}"#, id))?;
// Get the "Bcc" recipient(s) address(es)
let bcc = parse_some_addrs(&envelope.bcc)
.context(format!(r#"cannot parse "bcc" address of message {}"#, id))?;
// Get the "In-Reply-To" message identifier
let in_reply_to = match envelope
.in_reply_to
.as_ref()
.map(|cow| String::from_utf8(cow.to_vec()))
{
Some(id) => Some(id?),
None => None,
};
// Get the message identifier
let message_id = match envelope
.message_id
.as_ref()
.map(|cow| String::from_utf8(cow.to_vec()))
{
Some(id) => Some(id?),
None => None,
};
// Get the internal date
let date = fetch.internal_date();
// Get all parts
let parts = Parts::from(
&mailparse::parse_mail(
fetch
.body()
.ok_or(anyhow!("cannot get body of message {}", id))?,
)
.context(format!("cannot parse body of message {}", id))?,
);
Ok(Self {
id,
flags,
subject,
message_id,
from,
reply_to,
in_reply_to,
to,
cc,
bcc,
date,
parts,
})
}
}
pub fn parse_addr(addr: &imap_proto::Address) -> Result<Addr> {
let name = addr
.name
.as_ref()
.map(|name| {
rfc2047_decoder::decode(&name.to_vec())
.context("cannot decode address name")
.map(|name| Some(name))
})
.unwrap_or(Ok(None))?;
let mbox = addr
.mailbox
.as_ref()
.ok_or(anyhow!("cannot get address mailbox"))
.and_then(|mbox| {
rfc2047_decoder::decode(&mbox.to_vec()).context("cannot decode address mailbox")
})?;
let host = addr
.host
.as_ref()
.ok_or(anyhow!("cannot get address host"))
.and_then(|host| {
rfc2047_decoder::decode(&host.to_vec()).context("cannot decode address host")
})?;
Ok(Addr::new(name, lettre::Address::new(mbox, host)?))
}
pub fn parse_addrs(addrs: &Vec<imap_proto::Address>) -> Result<Vec<Addr>> {
let mut parsed_addrs = vec![];
for addr in addrs {
parsed_addrs
.push(parse_addr(addr).context(format!(r#"cannot parse address "{:?}""#, addr))?);
}
Ok(parsed_addrs)
}
pub fn parse_some_addrs(addrs: &Option<Vec<imap_proto::Address>>) -> Result<Option<Vec<Addr>>> {
Ok(match addrs.as_ref().map(parse_addrs) {
Some(addrs) => Some(addrs?),
None => None,
})
}
#[derive(Debug, Serialize)]
pub struct PrintableMsg(pub String);
impl fmt::Display for PrintableMsg {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", self.0)
}
}

View file

@ -0,0 +1,321 @@
//! Module related to message handling.
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream;
use imap::types::Flag;
use log::{debug, trace};
use std::{
borrow::Cow,
convert::{TryFrom, TryInto},
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
mbox::Mbox,
msg::{Flags, Msg, Part, TextPlainPart, Tpl},
smtp::SmtpServiceInterface,
},
output::OutputServiceInterface,
};
use super::PrintableMsg;
/// Download all attachments from the given message sequence number to the user account downloads
/// directory.
pub fn attachments<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let attachments = imap.find_msg(&seq)?.attachments();
let attachments_len = attachments.len();
debug!(
r#"{} attachment(s) found for message "{}""#,
attachments_len, seq
);
for attachment in attachments {
let filepath = account.downloads_dir.join(&attachment.filename);
debug!("downloading {}…", attachment.filename);
fs::write(&filepath, &attachment.content)
.context(format!("cannot download attachment {:?}", filepath))?;
}
output.print(format!(
"{} attachment(s) successfully downloaded to {:?}",
attachments_len, account.downloads_dir
))
}
/// Copy a message from a mailbox to another.
pub fn copy<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let mbox = Mbox::try_from(mbox)?;
let msg = imap.find_raw_msg(&seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
output.print(format!(
r#"Message {} successfully copied to folder "{}""#,
seq, mbox
))
}
/// Delete messages matching the given sequence range.
pub fn delete<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?;
imap.add_flags(seq, &flags)?;
imap.expunge()?;
output.print(format!(r#"Message(s) {} successfully deleted"#, seq))
}
/// Forward the given message UID from the selected mailbox.
pub fn forward<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
imap.find_msg(seq)?
.into_forward(account)?
.add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp)
}
/// List paginated messages from the selected mailbox.
pub fn list<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
page_size: Option<usize>,
page: usize,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size);
let msgs = imap.get_msgs(&page_size, &page)?;
trace!("messages: {:#?}", msgs);
output.print(msgs)
}
/// Parse and edit a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
url: &Url,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let to: Vec<lettre::message::Mailbox> = url
.path()
.split(";")
.filter_map(|s| s.parse().ok())
.collect();
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.parse()?);
}
b"bcc" => {
bcc.push(val.parse()?);
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (),
}
}
let mut msg = Msg::default();
msg.from = Some(vec![account.address().parse()?]);
msg.to = if to.is_empty() { None } else { Some(to) };
msg.cc = if cc.is_empty() { None } else { Some(cc) };
msg.bcc = if bcc.is_empty() { None } else { Some(bcc) };
msg.subject = subject.into();
msg.parts.push(Part::TextPlain(TextPlainPart {
content: body.into(),
}));
msg.edit_with_editor(account, output, imap, smtp)
}
/// Move a message from a mailbox to another.
pub fn move_<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
// The sequence number of the message to move
seq: &str,
// The mailbox to move the message in
mbox: Option<&str>,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
// Copy the message to targetted mailbox
let mbox = Mbox::try_from(mbox)?;
let msg = imap.find_raw_msg(&seq)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, &msg, flags)?;
// Delete the original message
let flags = Flags::try_from(vec![Flag::Seen, Flag::Deleted])?;
imap.add_flags(seq, &flags)?;
imap.expunge()?;
output.print(format!(
r#"Message {} successfully moved to folder "{}""#,
seq, mbox
))
}
/// Read a message by its sequence number.
pub fn read<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
// TODO: use the mime to select the right body
_mime: String,
raw: bool,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
if raw {
let msg = String::from_utf8(imap.find_raw_msg(&seq)?)?;
output.print(PrintableMsg(msg))
} else {
let msg = imap.find_msg(&seq)?.join_text_parts();
output.print(PrintableMsg(msg))
}
}
/// Reply to the given message UID.
pub fn reply<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
seq: &str,
all: bool,
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
imap.find_msg(seq)?
.into_reply(all, account)?
.add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp)?;
let flags = Flags::try_from(vec![Flag::Answered])?;
imap.add_flags(seq, &flags)
}
/// Save a raw message to the targetted mailbox.
pub fn save<ImapService: ImapServiceInterface>(
mbox: Option<&str>,
msg: &str,
imap: &mut ImapService,
) -> Result<()> {
let mbox = Mbox::try_from(mbox)?;
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, msg.as_bytes(), flags)
}
/// Paginate messages from the selected mailbox matching the specified query.
pub fn search<OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
query: String,
page_size: Option<usize>,
page: usize,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
) -> Result<()> {
let page_size = page_size.unwrap_or(account.default_page_size);
trace!("page size: {}", page_size);
let msgs = imap.find_msgs(&query, &page_size, &page)?;
trace!("messages: {:#?}", msgs);
output.print(msgs)
}
/// Send a raw message.
pub fn send<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
raw_msg: &str,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
let raw_msg = if atty::is(Stream::Stdin) || output.is_json() {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(|ln| ln.ok())
.map(|ln| ln.to_string())
.collect::<Vec<String>>()
.join("\r\n")
};
let tpl = Tpl(raw_msg.to_string());
let msg = Msg::try_from(&tpl)?;
let envelope: lettre::address::Envelope = msg.try_into()?;
smtp.send_raw_msg(&envelope, raw_msg.as_bytes())?;
debug!("message sent!");
// Save message to sent folder
let mbox = Mbox::from("Sent");
let flags = Flags::try_from(vec![Flag::Seen])?;
imap.append_raw_msg_with_flags(&mbox, raw_msg.as_bytes(), flags)
}
/// Compose a new message.
pub fn write<
OutputService: OutputServiceInterface,
ImapService: ImapServiceInterface,
SmtpService: SmtpServiceInterface,
>(
attachments_paths: Vec<&str>,
account: &Account,
output: &OutputService,
imap: &mut ImapService,
smtp: &mut SmtpService,
) -> Result<()> {
Msg::default()
.add_attachments(attachments_paths)?
.edit_with_editor(account, output, imap, smtp)
}

View file

@ -0,0 +1,15 @@
use anyhow::{Context, Result};
use log::{debug, trace};
use std::{env, fs, path::PathBuf};
pub fn local_draft_path() -> PathBuf {
let path = env::temp_dir().join("himalaya-draft.mail");
trace!("local draft path: {:?}", path);
path
}
pub fn remove_local_draft() -> Result<()> {
let path = local_draft_path();
debug!("remove draft path at {:?}", path);
fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path))
}

View file

@ -0,0 +1,117 @@
use mailparse::MailHeaderMap;
use serde::Serialize;
use std::ops::{Deref, DerefMut};
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextPlainPart {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct TextHtmlPart {
pub content: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct BinaryPart {
pub filename: String,
pub mime: String,
pub content: Vec<u8>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum Part {
TextPlain(TextPlainPart),
TextHtml(TextHtmlPart),
Binary(BinaryPart),
}
#[derive(Debug, Clone, Default, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Parts(pub Vec<Part>);
impl Parts {
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
self.retain(|part| {
if let Part::TextPlain(_) = part {
false
} else {
true
}
});
self.push(Part::TextPlain(part));
}
pub fn replace_text_html_parts_with(&mut self, part: TextHtmlPart) {
self.retain(|part| {
if let Part::TextHtml(_) = part {
false
} else {
true
}
});
self.push(Part::TextHtml(part));
}
}
impl Deref for Parts {
type Target = Vec<Part>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Parts {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<'a> From<&'a mailparse::ParsedMail<'a>> for Parts {
fn from(part: &'a mailparse::ParsedMail<'a>) -> Self {
let mut parts = vec![];
build_parts_map_rec(part, &mut parts);
Self(parts)
}
}
fn build_parts_map_rec(part: &mailparse::ParsedMail, parts: &mut Vec<Part>) {
if part.subparts.is_empty() {
let content_disp = part.get_content_disposition();
match content_disp.disposition {
mailparse::DispositionType::Attachment => {
let filename = content_disp
.params
.get("filename")
.map(String::from)
.unwrap_or(String::from("noname"));
let content = part.get_body_raw().unwrap_or_default();
let mime = tree_magic::from_u8(&content);
parts.push(Part::Binary(BinaryPart {
filename,
mime,
content,
}));
}
// TODO: manage other use cases
_ => {
part.get_headers()
.get_first_value("content-type")
.map(|ctype| {
let content = part.get_body().unwrap_or_default();
if ctype.starts_with("text/plain") {
parts.push(Part::TextPlain(TextPlainPart { content }))
} else if ctype.starts_with("text/html") {
parts.push(Part::TextHtml(TextHtmlPart { content }))
}
});
}
};
} else {
part.subparts
.iter()
.for_each(|part| build_parts_map_rec(part, parts));
}
}

View file

@ -1,158 +0,0 @@
use std::{
collections::HashMap,
convert::TryFrom,
io::{self, BufRead},
};
use anyhow::Result;
use atty::Stream;
use log::{debug, trace};
use crate::{
config::entity::Account,
domain::{
imap::service::ImapServiceInterface,
msg::{
body::entity::Body,
entity::{Msg, MsgSerialized},
header::entity::Headers,
tpl::arg::Tpl,
},
},
output::service::OutputServiceInterface,
};
pub fn new<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
tpl: Tpl<'a>,
account: &'a Account,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let mut msg = Msg::new(&account);
override_msg_with_args(&mut msg, tpl);
trace!("message: {:#?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
all: bool,
tpl: Tpl<'a>,
account: &'a Account,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let mut msg = imap.get_msg(uid)?;
msg.change_to_reply(account, all)?;
override_msg_with_args(&mut msg, tpl);
trace!("Message: {:?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
uid: &str,
tpl: Tpl<'a>,
account: &'a Account,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let mut msg = imap.get_msg(&uid)?;
msg.sig = account.signature.to_owned();
msg.change_to_forwarding(&account);
override_msg_with_args(&mut msg, tpl);
trace!("Message: {:?}", msg);
output.print(MsgSerialized::try_from(&msg)?)?;
imap.logout()?;
Ok(())
}
// == Helper functions ==
// -- Template Subcommands --
// These functions are more used for the "template" subcommand
fn override_msg_with_args<'a>(msg: &mut Msg, tpl: Tpl<'a>) {
// -- Collecting credentials --
let from: Vec<String> = match tpl.from {
Some(from) => from.map(|arg| arg.to_string()).collect(),
None => msg.headers.from.clone(),
};
let to: Vec<String> = match tpl.to {
Some(to) => to.map(|arg| arg.to_string()).collect(),
None => Vec::new(),
};
let subject = tpl
.subject
.map(String::from)
.or_else(|| msg.headers.subject.clone())
.or_else(|| Some(String::new()));
let cc: Option<Vec<String>> = tpl
.cc
.map(|cc| cc.map(|arg| arg.to_string()).collect())
.or_else(|| msg.headers.cc.clone());
let bcc: Option<Vec<String>> = tpl
.bcc
.map(|bcc| bcc.map(|arg| arg.to_string()).collect())
.or_else(|| msg.headers.bcc.clone());
let custom_headers: Option<HashMap<String, Vec<String>>> = {
if let Some(matched_headers) = tpl.headers {
let mut custom_headers: HashMap<String, Vec<String>> = HashMap::new();
// collect the custom headers
for header in matched_headers {
let mut header = header.split(":");
let key = header.next().unwrap_or_default();
let val = header.next().unwrap_or_default().trim_start();
custom_headers.insert(key.to_string(), vec![val.to_string()]);
}
Some(custom_headers)
} else {
None
}
};
let body = {
if atty::isnt(Stream::Stdin) {
let body = io::stdin()
.lock()
.lines()
.filter_map(|line| line.ok())
.map(|line| line.to_string())
.collect::<Vec<String>>()
.join("\n");
debug!("overriden body from stdin: {:?}", body);
body
} else if let Some(body) = tpl.body {
debug!("overriden body: {:?}", body);
body.to_string()
} else {
msg.body
.plain
.as_ref()
.map(String::from)
.unwrap_or_default()
}
};
let body = Body::new_with_text(body);
// -- Creating and printing --
let headers = Headers {
from,
subject,
to,
cc,
bcc,
custom_headers,
..msg.headers.clone()
};
msg.headers = headers;
msg.body = body;
msg.sig = tpl.sig.map(String::from).unwrap_or(msg.sig.to_owned());
}

View file

@ -1,4 +0,0 @@
//! Module related to messages template.
pub mod arg;
pub mod handler;

View file

@ -3,87 +3,87 @@
//! This module provides subcommands, arguments and a command matcher related to message template. //! This module provides subcommands, arguments and a command matcher related to message template.
use anyhow::Result; use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand, Values}; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::debug; use log::{debug, trace};
use crate::domain::msg::{self, arg::uid_arg}; use crate::domain::msg::msg_arg;
type Uid<'a> = &'a str; type Seq<'a> = &'a str;
type All = bool; type All = bool;
#[derive(Debug)] #[derive(Debug, Default)]
pub struct Tpl<'a> { pub struct TplOverride<'a> {
pub subject: Option<&'a str>, pub subject: Option<&'a str>,
pub from: Option<Values<'a>>, pub from: Option<Vec<&'a str>>,
pub to: Option<Values<'a>>, pub to: Option<Vec<&'a str>>,
pub cc: Option<Values<'a>>, pub cc: Option<Vec<&'a str>>,
pub bcc: Option<Values<'a>>, pub bcc: Option<Vec<&'a str>>,
pub headers: Option<Values<'a>>, pub headers: Option<Vec<&'a str>>,
pub body: Option<&'a str>, pub body: Option<&'a str>,
pub sig: Option<&'a str>, pub sig: Option<&'a str>,
} }
/// Message template commands. /// Message template commands.
pub enum Command<'a> { pub enum Command<'a> {
New(Tpl<'a>), New(TplOverride<'a>),
Reply(Uid<'a>, All, Tpl<'a>), Reply(Seq<'a>, All, TplOverride<'a>),
Forward(Uid<'a>, Tpl<'a>), Forward(Seq<'a>, TplOverride<'a>),
} }
/// Message template command matcher. /// Message template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> { pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
if let Some(m) = m.subcommand_matches("new") { if let Some(m) = m.subcommand_matches("new") {
debug!("new command matched"); debug!("new command matched");
let tpl = Tpl { let tpl = TplOverride {
subject: m.value_of("subject"), subject: m.value_of("subject"),
from: m.values_of("from"), from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to"), to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc"), cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc"), bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers"), headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"), body: m.value_of("body"),
sig: m.value_of("signature"), sig: m.value_of("signature"),
}; };
debug!("template: `{:?}`", tpl); trace!(r#"template args: "{:?}""#, tpl);
return Ok(Some(Command::New(tpl))); return Ok(Some(Command::New(tpl)));
} }
if let Some(m) = m.subcommand_matches("reply") { if let Some(m) = m.subcommand_matches("reply") {
debug!("reply command matched"); debug!("reply command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!(r#"seq: "{}""#, seq);
let all = m.is_present("reply-all"); let all = m.is_present("reply-all");
debug!("reply all: {}", all); trace!("reply all: {}", all);
let tpl = Tpl { let tpl = TplOverride {
subject: m.value_of("subject"), subject: m.value_of("subject"),
from: m.values_of("from"), from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to"), to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc"), cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc"), bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers"), headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"), body: m.value_of("body"),
sig: m.value_of("signature"), sig: m.value_of("signature"),
}; };
debug!("template: `{:?}`", tpl); trace!(r#"template args: "{:?}""#, tpl);
return Ok(Some(Command::Reply(uid, all, tpl))); return Ok(Some(Command::Reply(seq, all, tpl)));
} }
if let Some(m) = m.subcommand_matches("forward") { if let Some(m) = m.subcommand_matches("forward") {
debug!("forward command matched"); debug!("forward command matched");
let uid = m.value_of("uid").unwrap(); let seq = m.value_of("seq").unwrap();
debug!("uid: {}", uid); trace!(r#"seq: "{}""#, seq);
let tpl = Tpl { let tpl = TplOverride {
subject: m.value_of("subject"), subject: m.value_of("subject"),
from: m.values_of("from"), from: m.values_of("from").map(|v| v.collect()),
to: m.values_of("to"), to: m.values_of("to").map(|v| v.collect()),
cc: m.values_of("cc"), cc: m.values_of("cc").map(|v| v.collect()),
bcc: m.values_of("bcc"), bcc: m.values_of("bcc").map(|v| v.collect()),
headers: m.values_of("headers"), headers: m.values_of("headers").map(|v| v.collect()),
body: m.value_of("body"), body: m.value_of("body"),
sig: m.value_of("signature"), sig: m.value_of("signature"),
}; };
debug!("template: `{:?}`", tpl); trace!(r#"template args: "{:?}""#, tpl);
return Ok(Some(Command::Forward(uid, tpl))); return Ok(Some(Command::Forward(seq, tpl)));
} }
Ok(None) Ok(None)
@ -156,15 +156,15 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
SubCommand::with_name("reply") SubCommand::with_name("reply")
.aliases(&["rep", "r"]) .aliases(&["rep", "r"])
.about("Generates a reply message template") .about("Generates a reply message template")
.arg(uid_arg()) .arg(msg_arg::seq_arg())
.arg(msg::arg::reply_all_arg()) .arg(msg_arg::reply_all_arg())
.args(&tpl_args()), .args(&tpl_args()),
) )
.subcommand( .subcommand(
SubCommand::with_name("forward") SubCommand::with_name("forward")
.aliases(&["fwd", "fw", "f"]) .aliases(&["fwd", "fw", "f"])
.about("Generates a forward message template") .about("Generates a forward message template")
.arg(uid_arg()) .arg(msg_arg::seq_arg())
.args(&tpl_args()), .args(&tpl_args()),
)] )]
} }

View file

@ -0,0 +1,118 @@
use log::trace;
use serde::Serialize;
use std::{
fmt::{self, Display},
ops::Deref,
};
use crate::{
config::Account,
domain::msg::{Msg, TplOverride},
};
#[derive(Debug, Default, Clone, Serialize)]
pub struct Tpl(pub String);
impl Tpl {
pub fn from_msg(opts: TplOverride, msg: &Msg, account: &Account) -> Tpl {
let mut tpl = String::default();
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
if let Some(in_reply_to) = msg.in_reply_to.as_ref() {
tpl.push_str(&format!("In-Reply-To: {}\n", in_reply_to))
}
// From
tpl.push_str(&format!(
"From: {}\n",
opts.from
.map(|addrs| addrs.join(", "))
.unwrap_or_else(|| account.address())
));
// To
tpl.push_str(&format!(
"To: {}\n",
opts.to
.map(|addrs| addrs.join(", "))
.or_else(|| msg.to.clone().map(|addrs| addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")))
.unwrap_or_default()
));
// Cc
if let Some(addrs) = opts.cc.map(|addrs| addrs.join(", ")).or_else(|| {
msg.cc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Cc: {}\n", addrs));
}
// Bcc
if let Some(addrs) = opts.bcc.map(|addrs| addrs.join(", ")).or_else(|| {
msg.bcc.clone().map(|addrs| {
addrs
.iter()
.map(|addr| addr.to_string())
.collect::<Vec<_>>()
.join(", ")
})
}) {
tpl.push_str(&format!("Bcc: {}\n", addrs));
}
// Subject
tpl.push_str(&format!(
"Subject: {}\n",
opts.subject.unwrap_or(&msg.subject)
));
// Headers <=> body separator
tpl.push_str("\n");
// Body
if let Some(body) = opts.body {
tpl.push_str(body);
} else {
tpl.push_str(&msg.join_text_plain_parts())
}
// Signature
if let Some(sig) = opts.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
} else if let Some(ref sig) = account.sig {
tpl.push_str("\n\n");
tpl.push_str(sig);
}
tpl.push_str("\n");
let tpl = Tpl(tpl);
trace!("template: {:#?}", tpl);
tpl
}
}
impl Deref for Tpl {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Tpl {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.deref())
}
}

View file

@ -0,0 +1,52 @@
//! Module related to message template handling.
//!
//! This module gathers all message template commands.
use anyhow::Result;
use crate::{
config::Account,
domain::{
imap::ImapServiceInterface,
msg::{Msg, Tpl, TplOverride},
},
output::OutputServiceInterface,
};
/// Generate a new message template.
pub fn new<'a, OutputService: OutputServiceInterface>(
opts: TplOverride<'a>,
account: &'a Account,
output: &'a OutputService,
) -> Result<()> {
let msg = Msg::default();
let tpl = Tpl::from_msg(opts, &msg, account);
output.print(tpl)
}
/// Generate a reply message template.
pub fn reply<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
all: bool,
opts: TplOverride<'a>,
account: &'a Account,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let msg = imap.find_msg(seq)?.into_reply(all, account)?;
let tpl = Tpl::from_msg(opts, &msg, account);
output.print(tpl)
}
/// Generate a forward message template.
pub fn forward<'a, OutputService: OutputServiceInterface, ImapService: ImapServiceInterface>(
seq: &str,
opts: TplOverride<'a>,
account: &'a Account,
output: &'a OutputService,
imap: &'a mut ImapService,
) -> Result<()> {
let msg = imap.find_msg(seq)?.into_forward(account)?;
let tpl = Tpl::from_msg(opts, &msg, account);
output.print(tpl)
}

View file

@ -1,15 +0,0 @@
use anyhow::{Context, Result};
use log::debug;
use std::{env, fs, path::PathBuf};
pub fn draft_path() -> PathBuf {
let path = env::temp_dir().join("himalaya-draft.mail");
debug!("draft path: `{:?}`", path);
path
}
pub fn remove_draft() -> Result<()> {
let path = draft_path();
debug!("remove draft path: `{:?}`", path);
fs::remove_file(&path).context(format!("cannot delete draft file at `{:?}`", path))
}

View file

@ -1,3 +1,4 @@
//! Module related to SMTP. //! Module related to SMTP.
pub mod service; pub mod smtp_service;
pub use smtp_service::*;

View file

@ -8,11 +8,13 @@ use lettre::{
Transport, Transport,
}; };
use log::debug; use log::debug;
use std::convert::TryInto;
use crate::config::entity::Account; use crate::{config::Account, domain::msg::Msg};
pub trait SmtpServiceInterface { pub trait SmtpServiceInterface {
fn send(&mut self, msg: &lettre::Message) -> Result<()>; fn send_msg(&mut self, msg: &Msg) -> Result<lettre::Message>;
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()>;
} }
pub struct SmtpService<'a> { pub struct SmtpService<'a> {
@ -55,8 +57,16 @@ impl<'a> SmtpService<'a> {
} }
impl<'a> SmtpServiceInterface for SmtpService<'a> { impl<'a> SmtpServiceInterface for SmtpService<'a> {
fn send(&mut self, msg: &lettre::Message) -> Result<()> { fn send_msg(&mut self, msg: &Msg) -> Result<lettre::Message> {
self.transport()?.send(msg)?; debug!("sending message…");
let sendable_msg: lettre::Message = msg.try_into()?;
self.transport()?.send(&sendable_msg)?;
Ok(sendable_msg)
}
fn send_raw_msg(&mut self, envelope: &lettre::address::Envelope, msg: &[u8]) -> Result<()> {
debug!("sending raw message…");
self.transport()?.send_raw(envelope, msg)?;
Ok(()) Ok(())
} }
} }

View file

@ -10,14 +10,14 @@ mod domain;
mod output; mod output;
mod ui; mod ui;
use config::entity::{Account, Config}; use config::{Account, Config};
use domain::{ use domain::{
imap::{self, service::ImapService}, imap::{imap_arg, imap_handler, ImapService, ImapServiceInterface},
mbox::{self, entity::Mbox}, mbox::{mbox_arg, mbox_handler, Mbox},
msg, msg::{flag_arg, flag_handler, msg_arg, msg_handler, tpl_arg, tpl_handler},
smtp::service::SmtpService, smtp::SmtpService,
}; };
use output::service::OutputService; use output::OutputService;
fn create_app<'a>() -> clap::App<'a, 'a> { fn create_app<'a>() -> clap::App<'a, 'a> {
clap::App::new(env!("CARGO_PKG_NAME")) clap::App::new(env!("CARGO_PKG_NAME"))
@ -25,13 +25,13 @@ fn create_app<'a>() -> clap::App<'a, 'a> {
.about(env!("CARGO_PKG_DESCRIPTION")) .about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS")) .author(env!("CARGO_PKG_AUTHORS"))
.setting(AppSettings::GlobalVersion) .setting(AppSettings::GlobalVersion)
.args(&config::arg::args()) .args(&config::config_arg::args())
.args(&output::arg::args()) .args(&output::output_arg::args())
.arg(mbox::arg::source_arg()) .arg(mbox_arg::source_arg())
.subcommands(compl::arg::subcmds()) .subcommands(compl::compl_arg::subcmds())
.subcommands(imap::arg::subcmds()) .subcommands(imap_arg::subcmds())
.subcommands(mbox::arg::subcmds()) .subcommands(mbox_arg::subcmds())
.subcommands(msg::arg::subcmds()) .subcommands(msg_arg::subcmds())
} }
fn main() -> Result<()> { fn main() -> Result<()> {
@ -50,17 +50,17 @@ fn main() -> Result<()> {
let url = Url::parse(&raw_args[1])?; let url = Url::parse(&raw_args[1])?;
let mut imap = ImapService::from((&account, &mbox)); let mut imap = ImapService::from((&account, &mbox));
let mut smtp = SmtpService::from(&account); let mut smtp = SmtpService::from(&account);
return msg::handler::mailto(&url, &account, &output, &mut imap, &mut smtp); return msg_handler::mailto(&url, &account, &output, &mut imap, &mut smtp);
} }
let app = create_app(); let app = create_app();
let m = app.get_matches(); let m = app.get_matches();
// Check completion match BEFORE entities and services initialization. // Check completion match BEFORE entities and services initialization.
// See https://github.com/soywod/himalaya/issues/115. // Linked issue: https://github.com/soywod/himalaya/issues/115.
match compl::arg::matches(&m)? { match compl::compl_arg::matches(&m)? {
Some(compl::arg::Command::Generate(shell)) => { Some(compl::compl_arg::Command::Generate(shell)) => {
return compl::handler::generate(create_app(), shell); return compl::compl_handler::generate(create_app(), shell);
} }
_ => (), _ => (),
} }
@ -73,89 +73,88 @@ fn main() -> Result<()> {
let mut smtp = SmtpService::from(&account); let mut smtp = SmtpService::from(&account);
// Check IMAP matches. // Check IMAP matches.
match imap::arg::matches(&m)? { match imap_arg::matches(&m)? {
Some(imap::arg::Command::Notify(keepalive)) => { Some(imap_arg::Command::Notify(keepalive)) => {
return imap::handler::notify(keepalive, &config, &mut imap); return imap_handler::notify(keepalive, &config, &mut imap);
} }
Some(imap::arg::Command::Watch(keepalive)) => { Some(imap_arg::Command::Watch(keepalive)) => {
return imap::handler::watch(keepalive, &mut imap); return imap_handler::watch(keepalive, &mut imap);
} }
_ => (), _ => (),
} }
// Check mailbox matches. // Check mailbox matches.
match mbox::arg::matches(&m)? { match mbox_arg::matches(&m)? {
Some(mbox::arg::Command::List) => { Some(mbox_arg::Command::List) => {
return mbox::handler::list(&output, &mut imap); return mbox_handler::list(&output, &mut imap);
} }
_ => (), _ => (),
} }
// Check message matches. // Check message matches.
match msg::arg::matches(&m)? { match msg_arg::matches(&m)? {
Some(msg::arg::Command::Attachments(uid)) => { Some(msg_arg::Command::Attachments(seq)) => {
return msg::handler::attachments(uid, &account, &output, &mut imap); return msg_handler::attachments(seq, &account, &output, &mut imap);
} }
Some(msg::arg::Command::Copy(uid, mbox)) => { Some(msg_arg::Command::Copy(seq, target)) => {
return msg::handler::copy(uid, mbox, &output, &mut imap); return msg_handler::copy(seq, target, &output, &mut imap);
} }
Some(msg::arg::Command::Delete(uid)) => { Some(msg_arg::Command::Delete(seq)) => {
return msg::handler::delete(uid, &output, &mut imap); return msg_handler::delete(seq, &output, &mut imap);
} }
Some(msg::arg::Command::Forward(uid, paths)) => { Some(msg_arg::Command::Forward(seq, atts)) => {
return msg::handler::forward(uid, paths, &account, &output, &mut imap, &mut smtp); return msg_handler::forward(seq, atts, &account, &output, &mut imap, &mut smtp);
} }
Some(msg::arg::Command::List(page_size, page)) => { Some(msg_arg::Command::List(page_size, page)) => {
return msg::handler::list(page_size, page, &account, &output, &mut imap); return msg_handler::list(page_size, page, &account, &output, &mut imap);
} }
Some(msg::arg::Command::Move(uid, mbox)) => { Some(msg_arg::Command::Move(seq, target)) => {
return msg::handler::move_(uid, mbox, &output, &mut imap); return msg_handler::move_(seq, target, &output, &mut imap);
} }
Some(msg::arg::Command::Read(uid, mime, raw)) => { Some(msg_arg::Command::Read(seq, mime, raw)) => {
return msg::handler::read(uid, mime, raw, &output, &mut imap); return msg_handler::read(seq, mime, raw, &output, &mut imap);
} }
Some(msg::arg::Command::Reply(uid, all, paths)) => { Some(msg_arg::Command::Reply(seq, all, atts)) => {
return msg::handler::reply(uid, all, paths, &account, &output, &mut imap, &mut smtp); return msg_handler::reply(seq, all, atts, &account, &output, &mut imap, &mut smtp);
} }
Some(msg::arg::Command::Save(mbox, msg)) => { Some(msg_arg::Command::Save(target, msg)) => {
return msg::handler::save(mbox, msg, &mut imap); return msg_handler::save(target, msg, &mut imap);
} }
Some(msg::arg::Command::Search(query, page_size, page)) => { Some(msg_arg::Command::Search(query, page_size, page)) => {
return msg::handler::search(page_size, page, query, &account, &output, &mut imap); return msg_handler::search(query, page_size, page, &account, &output, &mut imap);
} }
Some(msg::arg::Command::Send(msg)) => { Some(msg_arg::Command::Send(raw_msg)) => {
return msg::handler::send(msg, &output, &mut imap, &mut smtp); return msg_handler::send(raw_msg, &output, &mut imap, &mut smtp);
} }
Some(msg::arg::Command::Write(paths)) => { Some(msg_arg::Command::Write(atts)) => {
return msg::handler::write(paths, &account, &output, &mut imap, &mut smtp); return msg_handler::write(atts, &account, &output, &mut imap, &mut smtp);
} }
Some(msg_arg::Command::Flag(m)) => match m {
Some(msg::arg::Command::Flag(m)) => match m { Some(flag_arg::Command::Set(seq_range, flags)) => {
Some(msg::flag::arg::Command::Set(uid, flags)) => { return flag_handler::set(seq_range, flags, &output, &mut imap);
return msg::flag::handler::set(uid, flags, &output, &mut imap);
} }
Some(msg::flag::arg::Command::Add(uid, flags)) => { Some(flag_arg::Command::Add(seq_range, flags)) => {
return msg::flag::handler::add(uid, flags, &output, &mut imap); return flag_handler::add(seq_range, flags, &output, &mut imap);
} }
Some(msg::flag::arg::Command::Remove(uid, flags)) => { Some(flag_arg::Command::Remove(seq_range, flags)) => {
return msg::flag::handler::remove(uid, flags, &output, &mut imap); return flag_handler::remove(seq_range, flags, &output, &mut imap);
} }
_ => (), _ => (),
}, },
Some(msg::arg::Command::Tpl(m)) => match m { Some(msg_arg::Command::Tpl(m)) => match m {
Some(msg::tpl::arg::Command::New(tpl)) => { Some(tpl_arg::Command::New(tpl)) => {
return msg::tpl::handler::new(tpl, &account, &output, &mut imap); return tpl_handler::new(tpl, &account, &output);
} }
Some(msg::tpl::arg::Command::Reply(uid, all, tpl)) => { Some(tpl_arg::Command::Reply(seq, all, tpl)) => {
return msg::tpl::handler::reply(uid, all, tpl, &account, &output, &mut imap); return tpl_handler::reply(seq, all, tpl, &account, &output, &mut imap);
} }
Some(msg::tpl::arg::Command::Forward(uid, tpl)) => { Some(tpl_arg::Command::Forward(seq, tpl)) => {
return msg::tpl::handler::forward(uid, tpl, &account, &output, &mut imap); return tpl_handler::forward(seq, tpl, &account, &output, &mut imap);
} }
_ => (), _ => (),
}, },
_ => (), _ => (),
} }
Ok(()) imap.logout()
} }

View file

@ -1,5 +1,9 @@
//! Module related to output formatting and printing. //! Module related to output formatting and printing.
pub mod arg; pub mod output_arg;
pub mod service;
pub mod utils; pub mod output_utils;
pub use output_utils::*;
pub mod output_service;
pub use output_service::*;

View file

@ -0,0 +1,12 @@
use anyhow::Result;
use std::process::Command;
pub fn run_cmd(cmd: &str) -> Result<String> {
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output()
} else {
Command::new("sh").arg("-c").arg(cmd).output()
}?;
Ok(String::from_utf8(output.stdout)?)
}

View file

@ -1,32 +0,0 @@
use anyhow::Result;
use serde::ser::{self, SerializeStruct};
use std::{fmt, process::Command, result};
pub struct Info(pub String);
impl fmt::Display for Info {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ser::Serialize for Info {
fn serialize<S>(&self, serializer: S) -> result::Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
let mut state = serializer.serialize_struct("Info", 1)?;
state.serialize_field("info", &self.0)?;
state.end()
}
}
pub fn run_cmd(cmd: &str) -> Result<String> {
let output = if cfg!(target_os = "windows") {
Command::new("cmd").args(&["/C", cmd]).output()
} else {
Command::new("sh").arg("-c").arg(cmd).output()
}?;
Ok(String::from_utf8(output.stdout)?)
}

View file

@ -1,5 +1,5 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use log::debug; use log::{debug, error};
use std::io::{self, Write}; use std::io::{self, Write};
pub enum PreEditChoice { pub enum PreEditChoice {
@ -32,11 +32,11 @@ pub fn pre_edit() -> Result<PreEditChoice> {
Ok(PreEditChoice::Quit) Ok(PreEditChoice::Quit)
} }
Some(choice) => { Some(choice) => {
debug!("invalid choice `{}`", choice); error!(r#"invalid choice "{}""#, choice);
Err(anyhow!("invalid choice `{}`", choice)) Err(anyhow!(r#"invalid choice "{}""#, choice))
} }
None => { None => {
debug!("empty choice"); error!("empty choice");
Err(anyhow!("empty choice")) Err(anyhow!("empty choice"))
} }
} }
@ -81,11 +81,11 @@ pub fn post_edit() -> Result<PostEditChoice> {
Ok(PostEditChoice::Discard) Ok(PostEditChoice::Discard)
} }
Some(choice) => { Some(choice) => {
debug!("invalid choice `{}`", choice); error!(r#"invalid choice "{}""#, choice);
Err(anyhow!("invalid choice `{}`", choice)) Err(anyhow!(r#"invalid choice "{}""#, choice))
} }
None => { None => {
debug!("empty choice"); error!("empty choice");
Err(anyhow!("empty choice")) Err(anyhow!("empty choice"))
} }
} }

View file

@ -1,70 +1,32 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{Context, Result};
use log::{debug, error}; use log::debug;
use std::{ use std::{env, fs, process::Command};
env,
fs::File,
io::{Read, Write},
process::Command,
};
use crate::{ use crate::domain::msg::{msg_utils, Tpl};
domain::msg,
ui::choice::{self, PreEditChoice},
};
pub fn open_editor_with_tpl(tpl: &[u8]) -> Result<String> { pub fn open_with_tpl(tpl: Tpl) -> Result<Tpl> {
let path = msg::utils::draft_path(); let path = msg_utils::local_draft_path();
if path.exists() {
debug!("draft found");
loop {
match choice::pre_edit() {
Ok(choice) => match choice {
PreEditChoice::Edit => return open_editor_with_draft(),
PreEditChoice::Discard => break,
PreEditChoice::Quit => return Err(anyhow!("edition aborted")),
},
Err(err) => error!("{}", err),
}
}
}
debug!("create draft"); debug!("create draft");
File::create(&path) fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?;
.context(format!("cannot create draft file `{:?}`", path))?
.write(tpl)
.context(format!("cannot write draft file `{:?}`", path))?;
debug!("open editor"); debug!("open editor");
Command::new(env::var("EDITOR").context("cannot find `$EDITOR` env var")?) Command::new(env::var("EDITOR").context(r#"cannot find "$EDITOR" env var"#)?)
.arg(&path) .arg(&path)
.status() .status()
.context("cannot launch editor")?; .context("cannot launch editor")?;
debug!("read draft"); debug!("read draft");
let mut draft = String::new(); let content =
File::open(&path) fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
.context(format!("cannot open draft file `{:?}`", path))?
.read_to_string(&mut draft)
.context(format!("cannot read draft file `{:?}`", path))?;
Ok(draft) Ok(Tpl(content))
} }
pub fn open_editor_with_draft() -> Result<String> { pub fn open_with_draft() -> Result<Tpl> {
let path = msg::utils::draft_path(); let path = msg_utils::local_draft_path();
let content =
// Opens editor and saves user input to draft file fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
Command::new(env::var("EDITOR").context("cannot find `EDITOR` env var")?) let tpl = Tpl(content);
.arg(&path) open_with_tpl(tpl)
.status()
.context("cannot launch editor")?;
// Extracts draft file content
let mut draft = String::new();
File::open(&path)
.context(format!("cannot open file `{:?}`", path))?
.read_to_string(&mut draft)
.context(format!("cannot read file `{:?}`", path))?;
Ok(draft)
} }

View file

@ -2,4 +2,6 @@
pub mod choice; pub mod choice;
pub mod editor; pub mod editor;
pub mod table; pub mod table;
pub use table::*;

View file

@ -11,11 +11,11 @@ use unicode_width::UnicodeWidthStr;
/// Define the default terminal size. /// Define the default terminal size.
/// It is used when the size cannot be determined by the `terminal_size` crate. /// It is used when the size cannot be determined by the `terminal_size` crate.
const DEFAULT_TERM_WIDTH: usize = 80; pub const DEFAULT_TERM_WIDTH: usize = 80;
/// Define the minimum size of a shrinked cell. /// Define the minimum size of a shrinked cell.
/// TODO: make this customizable. /// TODO: make this customizable.
const MAX_SHRINK_WIDTH: usize = 5; pub const MAX_SHRINK_WIDTH: usize = 5;
/// Wrapper around [ANSI escape codes] for styling cells. /// Wrapper around [ANSI escape codes] for styling cells.
/// ///

View file

@ -10,7 +10,7 @@ endfunction
function! s:fzf_picker(cb, mboxes) function! s:fzf_picker(cb, mboxes)
call fzf#run({ call fzf#run({
\"source": a:mboxes, \"source": a:mboxes,
\"sink": a:cb, \"sink": function(a:cb),
\"down": "25%", \"down": "25%",
\}) \})
endfunction endfunction
@ -24,14 +24,14 @@ endfunction
" Pagination " Pagination
let s:curr_page = 0 let s:curr_page = 1
function! himalaya#mbox#curr_page() function! himalaya#mbox#curr_page()
return s:curr_page return s:curr_page
endfunction endfunction
function! himalaya#mbox#prev_page() function! himalaya#mbox#prev_page()
let s:curr_page = max([0, s:curr_page - 1]) let s:curr_page = max([1, s:curr_page - 1])
call himalaya#msg#list() call himalaya#msg#list()
endfunction endfunction
@ -79,6 +79,6 @@ endfunction
function! himalaya#mbox#_change(mbox) function! himalaya#mbox#_change(mbox)
let s:curr_mbox = a:mbox let s:curr_mbox = a:mbox
let s:curr_page = 0 let s:curr_page = 1
call himalaya#msg#list() call himalaya#msg#list()
endfunction endfunction

View file

@ -8,16 +8,11 @@ let s:draft = ""
" Message " Message
function! s:format_msg_for_list(msg) function! s:format_msg_for_list(msg)
let msg = {}
let msg.uid = a:msg.uid
let flag_new = index(a:msg.flags, "Seen") == -1 ? "✷" : " " let flag_new = index(a:msg.flags, "Seen") == -1 ? "✷" : " "
let flag_flagged = index(a:msg.flags, "Flagged") == -1 ? " " : "!" let flag_flagged = index(a:msg.flags, "Flagged") == -1 ? " " : "!"
let flag_replied = index(a:msg.flags, "Answered") == -1 ? " " : "↵" let flag_replied = index(a:msg.flags, "Answered") == -1 ? " " : "↵"
let msg.flags = printf("%s %s %s", flag_new, flag_replied, flag_flagged) let a:msg.flags = printf("%s %s %s", flag_new, flag_replied, flag_flagged)
let msg.subject = a:msg.subject return a:msg
let msg.sender = a:msg.sender
let msg.date = a:msg.date
return msg
endfunction endfunction
function! himalaya#msg#list_with(account, mbox, page, should_throw) function! himalaya#msg#list_with(account, mbox, page, should_throw)
@ -30,7 +25,7 @@ function! himalaya#msg#list_with(account, mbox, page, should_throw)
\) \)
let msgs = map(msgs, "s:format_msg_for_list(v:val)") let msgs = map(msgs, "s:format_msg_for_list(v:val)")
let buftype = stridx(bufname("%"), "Himalaya messages") == 0 ? "file" : "edit" let buftype = stridx(bufname("%"), "Himalaya messages") == 0 ? "file" : "edit"
execute printf("silent! %s Himalaya messages [%s] [page %d]", buftype, a:mbox, a:page + 1) execute printf("silent! %s Himalaya messages [%s] [page %d]", buftype, a:mbox, a:page)
setlocal modifiable setlocal modifiable
silent execute "%d" silent execute "%d"
call append(0, s:render("list", msgs)) call append(0, s:render("list", msgs))
@ -43,7 +38,9 @@ endfunction
function! himalaya#msg#list(...) function! himalaya#msg#list(...)
try try
call himalaya#account#set(a:0 > 0 ? a:1 : "") if a:0 > 0
call himalaya#account#set(a:1)
endif
let account = himalaya#account#curr() let account = himalaya#account#curr()
let mbox = himalaya#mbox#curr_mbox() let mbox = himalaya#mbox#curr_mbox()
let page = himalaya#mbox#curr_page() let page = himalaya#mbox#curr_page()
@ -67,11 +64,10 @@ function! himalaya#msg#read()
\printf("Fetching message %d", s:msg_id), \printf("Fetching message %d", s:msg_id),
\1, \1,
\) \)
let attachment = msg.hasAttachment ? " []" : "" execute printf("silent! botright new Himalaya read message [%d]", s:msg_id)
execute printf("silent! edit Himalaya read message [%d]%s", s:msg_id, attachment)
setlocal modifiable setlocal modifiable
silent execute "%d" silent execute "%d"
call append(0, split(substitute(msg.content, "\r", "", "g"), "\n")) call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
silent execute "$d" silent execute "$d"
setlocal filetype=himalaya-msg-read setlocal filetype=himalaya-msg-read
let &modified = 0 let &modified = 0
@ -90,7 +86,7 @@ function! himalaya#msg#write()
let account = himalaya#account#curr() let account = himalaya#account#curr()
let msg = s:cli("--account %s template new", [shellescape(account)], "Fetching new template", 0) let msg = s:cli("--account %s template new", [shellescape(account)], "Fetching new template", 0)
silent! edit Himalaya write silent! edit Himalaya write
call append(0, split(substitute(msg.raw, "\r", "", "g"), "\n")) call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
silent execute "$d" silent execute "$d"
setlocal filetype=himalaya-msg-write setlocal filetype=himalaya-msg-write
let &modified = 0 let &modified = 0
@ -116,7 +112,7 @@ function! himalaya#msg#reply()
\0, \0,
\) \)
execute printf("silent! edit Himalaya reply [%d]", msg_id) execute printf("silent! edit Himalaya reply [%d]", msg_id)
call append(0, split(substitute(msg.raw, "\r", "", "g"), "\n")) call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
silent execute "$d" silent execute "$d"
setlocal filetype=himalaya-msg-write setlocal filetype=himalaya-msg-write
let &modified = 0 let &modified = 0
@ -142,7 +138,7 @@ function! himalaya#msg#reply_all()
\0 \0
\) \)
execute printf("silent! edit Himalaya reply all [%d]", msg_id) execute printf("silent! edit Himalaya reply all [%d]", msg_id)
call append(0, split(substitute(msg.raw, "\r", "", "g"), "\n")) call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
silent execute "$d" silent execute "$d"
setlocal filetype=himalaya-msg-write setlocal filetype=himalaya-msg-write
let &modified = 0 let &modified = 0
@ -168,7 +164,7 @@ function! himalaya#msg#forward()
\0 \0
\) \)
execute printf("silent! edit Himalaya forward [%d]", msg_id) execute printf("silent! edit Himalaya forward [%d]", msg_id)
call append(0, split(substitute(msg.raw, "\r", "", "g"), "\n")) call append(0, split(substitute(msg, "\r", "", "g"), "\n"))
silent execute "$d" silent execute "$d"
setlocal filetype=himalaya-msg-write setlocal filetype=himalaya-msg-write
let &modified = 0 let &modified = 0
@ -259,7 +255,7 @@ function! himalaya#msg#delete() range
endfunction endfunction
function! himalaya#msg#draft_save() function! himalaya#msg#draft_save()
let s:draft = join(getline(1, "$"), "\n") let s:draft = join(getline(1, "$"), "\n") . "\n"
redraw | call s:log("Save draft [OK]") redraw | call s:log("Save draft [OK]")
let &modified = 0 let &modified = 0
endfunction endfunction
@ -322,10 +318,10 @@ endfunction
let s:config = { let s:config = {
\"list": { \"list": {
\"columns": ["uid", "flags", "subject", "sender", "date"], \"columns": ["id", "flags", "subject", "sender", "date"],
\}, \},
\"labels": { \"labels": {
\"uid": "UID", \"id": "ID",
\"flags": "FLAGS", \"flags": "FLAGS",
\"subject": "SUBJECT", \"subject": "SUBJECT",
\"sender": "SENDER", \"sender": "SENDER",

View file

@ -6,5 +6,5 @@ setlocal startofline
augroup himalaya_write augroup himalaya_write
autocmd! * <buffer> autocmd! * <buffer>
autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save() autocmd BufWriteCmd <buffer> call himalaya#msg#draft_save()
autocmd BufUnload <buffer> call himalaya#msg#draft_handle() autocmd BufLeave <buffer> call himalaya#msg#draft_handle()
augroup end augroup end