mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-25 04:20:22 +00:00
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:
parent
794860befe
commit
b7d068c729
64 changed files with 3100 additions and 4260 deletions
368
Cargo.lock
generated
368
Cargo.lock
generated
|
@ -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",
|
||||||
|
]
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
|
162
src/config/account_entity.rs
Normal file
162
src/config/account_entity.rs
Normal 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
158
src/config/config_entity.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>");
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
}
|
|
@ -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> {
|
|
@ -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::*;
|
||||||
|
|
|
@ -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};
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
|
@ -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::*;
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//! Module related to message attachment.
|
|
||||||
|
|
||||||
pub mod arg;
|
|
||||||
pub mod entity;
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod entity;
|
|
File diff suppressed because it is too large
Load diff
145
src/domain/msg/envelope_entity.rs
Normal file
145
src/domain/msg/envelope_entity.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
42
src/domain/msg/envelopes_entity.rs
Normal file
42
src/domain/msg/envelopes_entity.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(())
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
//! Module related to messages flag.
|
|
||||||
|
|
||||||
pub mod arg;
|
|
||||||
pub mod entity;
|
|
||||||
pub mod handler;
|
|
|
@ -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()),
|
||||||
)]
|
)]
|
||||||
}
|
}
|
26
src/domain/msg/flag_entity.rs
Normal file
26
src/domain/msg/flag_entity.rs
Normal 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
58
src/domain/msg/flag_handler.rs
Normal file
58
src/domain/msg/flag_handler.rs
Normal 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
|
||||||
|
))
|
||||||
|
}
|
239
src/domain/msg/flags_entity.rs
Normal file
239
src/domain/msg/flags_entity.rs
Normal 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);
|
||||||
|
// }
|
||||||
|
//}
|
|
@ -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(())
|
|
||||||
}
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
pub mod entity;
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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()
|
814
src/domain/msg/msg_entity.rs
Normal file
814
src/domain/msg/msg_entity.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
321
src/domain/msg/msg_handler.rs
Normal file
321
src/domain/msg/msg_handler.rs
Normal 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)
|
||||||
|
}
|
15
src/domain/msg/msg_utils.rs
Normal file
15
src/domain/msg/msg_utils.rs
Normal 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))
|
||||||
|
}
|
117
src/domain/msg/parts_entity.rs
Normal file
117
src/domain/msg/parts_entity.rs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
//! Module related to messages template.
|
|
||||||
|
|
||||||
pub mod arg;
|
|
||||||
pub mod handler;
|
|
|
@ -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()),
|
||||||
)]
|
)]
|
||||||
}
|
}
|
118
src/domain/msg/tpl_entity.rs
Normal file
118
src/domain/msg/tpl_entity.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
52
src/domain/msg/tpl_handler.rs
Normal file
52
src/domain/msg/tpl_handler.rs
Normal 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)
|
||||||
|
}
|
|
@ -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))
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
//! Module related to SMTP.
|
//! Module related to SMTP.
|
||||||
|
|
||||||
pub mod service;
|
pub mod smtp_service;
|
||||||
|
pub use smtp_service::*;
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
133
src/main.rs
133
src/main.rs
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
12
src/output/output_utils.rs
Normal file
12
src/output/output_utils.rs
Normal 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)?)
|
||||||
|
}
|
|
@ -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)?)
|
|
||||||
}
|
|
|
@ -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"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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.
|
||||||
///
|
///
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue