Compare commits

...

597 commits

Author SHA1 Message Date
Cody Hiar
cf008c0ca7
docs(readme): fix gmail doc example
Refs: #564
2025-03-05 15:44:10 +01:00
Alan
d697cbc16b
docs(readme): add faq entry for disabling color
Refs: #567
2025-02-25 21:06:23 +01:00
Clément DOUIN
26feecd80a
build(deps): bump all
Refs: #525
2025-02-17 15:03:45 +01:00
Clément DOUIN
55bd9247e7
build(nix): bump pimalaya/nix flake
Refs: <https://github.com/pimalaya/nix/issues/1>
2025-01-27 14:32:18 +01:00
Clément DOUIN
15a9a4a69f
build: bump all external dependencies 2025-01-12 16:01:46 +01:00
Clément DOUIN
2b25a4d1fb
build: add job for publishing to crates.io 2025-01-11 21:08:02 +01:00
ajanvrin
12afd84d2b
docs(contributing): link to conventionalcommits.org instead of git repo
I was not aware of what Conventional Commits are, and their repository does not describe what the commits are meant to look like.
Linking to the summary is IMHO more convenient.

Refs: #540
2025-01-11 17:08:40 +01:00
Clément DOUIN
5632fdac3b
build: release v1.1.0 2025-01-11 15:44:24 +01:00
Clément DOUIN
21a97b83f3
build: bump all deps before release 2025-01-11 15:18:38 +01:00
Clément DOUIN
0f6217e6e5
build(nix): fix darwin x86_64 build
Also re-format nix codes with nixfmt-rfc-style.

Refs: #538
2025-01-11 14:35:04 +01:00
Clément DOUIN
9d773e947b
docs(contributing): add dependencies overriding section
Refs: <https://github.com/pimalaya/core/issues/32>
2025-01-11 08:53:13 +01:00
Clément DOUIN
c79cabc168
feat(folder): add -y|--yes flag for purge and delete commands
Refs: #469
2025-01-10 16:28:53 +01:00
Clément DOUIN
f5695cad53
refactor(config): change default downloads directory
Refs: <https://github.com/pimalaya/core/issues/1>
2025-01-10 16:02:40 +01:00
Clément DOUIN
25edd9e106
ci(release-on-demand): fix wrong targets 2025-01-10 15:01:35 +01:00
Clément DOUIN
1e26033e28
ci(release-on-demand): add missing isStatic true 2025-01-10 14:48:29 +01:00
Clément DOUIN
354848baf4
ci(release-on-demand): add missing x86_64-linux target 2025-01-10 14:33:35 +01:00
Clément DOUIN
d51ba0850a
ci: init release-on-demand workflow 2025-01-10 14:26:43 +01:00
Clément DOUIN
e3cbbbc6c4
refactor(envelope): add trace log to debug imap sequence set
Refs: <https://github.com/pimalaya/himalaya/issues/518>
2025-01-10 11:54:52 +01:00
Clément DOUIN
50b8d3667e
fix(envelope): prevent out of bound error on empty search result
Refs: #518
2025-01-10 11:36:19 +01:00
Clément DOUIN
9aa408ae17
refactor(message): make message.send.save-copy true by default
Refs: #536
2025-01-10 10:19:04 +01:00
Clément DOUIN
97e40b5f59
fix(config): fix de/serialization of backends' none variant
Refs: #523
2025-01-10 09:58:35 +01:00
Clément DOUIN
dc5b8a34c8
ci: fix wrong nixpkgs channel 2025-01-10 06:38:33 +01:00
Clément DOUIN
0eb7bfc419
ci: improve pimalaya/nix logs 2025-01-10 06:34:06 +01:00
Clément DOUIN
082e680b32
fix(tracing): make env filter warn the default
Refs: #522
2025-01-09 18:29:32 +01:00
Clément DOUIN
1928b36859
build(nix): update flake dependencies ter 2025-01-09 17:23:06 +01:00
Clément DOUIN
7ab615054f
build(nix): update flake dependencies bis
Refs: #515
2025-01-09 16:00:58 +01:00
Clément DOUIN
b503027585
build(nix): update flake dependencies
Refs: #515
2025-01-09 15:42:01 +01:00
Clément DOUIN
3ceef291a1
fix: permission denied when using install.sh
Refs: #515
2025-01-09 15:23:24 +01:00
Clément DOUIN
5eeda248fd
docs: fix folder.aliases
Fixed wrong folder.alias is the README and in the config.sample.toml.

Refs: #526
2024-12-19 09:20:13 +01:00
Tim Chase
118a3f9779
docs(readme): fix typo FAQ MML
Refs: #520
2024-12-12 20:56:14 +01:00
Clément DOUIN
4953aae860
doc(readme): add other interfaces section 2024-12-09 21:52:52 +01:00
Clément DOUIN
bf3d5342c2
doc(readme): add FAQ entry about aerc, mutt and alpine comparison 2024-12-09 21:52:49 +01:00
Clément DOUIN
1db785ac0b
doc(readme): remove $ in front of shell commands
Also removed the bash annotations, as most of the code blocks refer to
shell commands and not bash code.

Refs: #516
2024-12-09 21:52:44 +01:00
Clément DOUIN
ce0b2dd8d3
build: release v1.0.0
Refs: #514
2024-12-09 12:04:15 +01:00
Clément DOUIN
6e658fef33
Merge pull request #511 from KoviRobi/master
Allow using `override` to add features
2024-12-05 06:33:18 +01:00
Kovacsics Robert
0302a77f73 Fix Outlook SMTP server typo
From
https://support.microsoft.com/en-us/office/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040
2024-12-04 21:38:40 +00:00
Kovacsics Robert
77d2292e5c Allow using override to add features
This way you can do
```nix
himalaya.override {
  buildFeatures = [ "notmuch" "oauth2" ];
}
```

Also this uses any unspecified arguments as if they were given from
`pkgs`, so no need to explicitly specify.
2024-12-04 19:19:51 +00:00
Clément DOUIN
f9f2aaeab7
fix answered flag not set when replying to a message #508 2024-11-29 16:02:12 +01:00
Clément DOUIN
4b731f3cca
fix IMAP STARTTLS infinite loop 2024-11-29 15:23:16 +01:00
Clément DOUIN
7aa576400a
fix mailto parsing issue 2024-11-29 11:58:00 +01:00
Clément DOUIN
eafeeb28a4
clean 2024-11-29 10:21:55 +01:00
Clément DOUIN
85a12a54c0
make use of pimalaya/tui build module 2024-11-29 08:37:38 +01:00
Clément DOUIN
1a193f3ec3
make use of re-usable pre-releases pimalaya/nix workflow 2024-11-28 20:38:14 +01:00
Clément DOUIN
60dc3afd5b
fix wrong build and host platforms 2024-11-28 16:14:27 +01:00
Clément DOUIN
f92b6a6bb5
make use of pimalaya/nix 2024-11-28 15:34:58 +01:00
Clément DOUIN
86baf1c483
remove custom-release workflow
workflow_dispatch requires read and write access to the repo, which
is not suitable for public usage.

Also restricted pre-release workflow to master branch.
2024-11-28 07:26:22 +01:00
Clément DOUIN
d262418baa
renamed build-on-demand to custom-release 2024-11-27 21:08:09 +01:00
Clément DOUIN
eb65464e34
Merge pull request #506 from pimalaya/nixpkgs-common
Nix refactor
2024-11-27 20:59:12 +01:00
Clément DOUIN
0917caa400
fix features and workflows 2024-11-27 20:39:13 +01:00
Clément DOUIN
f0fbd3d213
try imap only 2024-11-27 16:15:13 +01:00
Clément DOUIN
d2ee5dbf98
try minimal build 2024-11-27 15:54:19 +01:00
Clément DOUIN
0ae35beb0d
fix typo 2024-11-27 15:36:34 +01:00
Clément DOUIN
0db15511c5
fix gh action matrix 2024-11-27 15:24:35 +01:00
Clément DOUIN
b55935cc39
add minimal features set 2024-11-27 15:20:26 +01:00
Clément DOUIN
b6a062d8bd
adjust postInstall hook 2024-11-27 15:04:09 +01:00
Clément DOUIN
cb077131b2
fix pre-release syntax 2024-11-27 14:38:50 +01:00
Clément DOUIN
6644801452
remove emulators 2024-11-27 14:35:43 +01:00
Clément DOUIN
9e15acf14f
remove wine dep from build 2024-11-27 11:56:40 +01:00
Clément DOUIN
085aea0fe9
fix build-on-demand gh workflow 2024-11-27 11:47:13 +01:00
Clément DOUIN
f94e592d63
remove unused systems.nix 2024-11-27 10:55:12 +01:00
Clément DOUIN
6a67d18683
fix wrong nixpkgs + wineboot arg 2024-11-27 10:48:13 +01:00
Clément DOUIN
eca47cf2f7
try to fix windows wine 2024-11-27 10:36:10 +01:00
Clément DOUIN
4cf8b2ded0
fix wrong apple target name with darwin 2024-11-27 10:13:29 +01:00
Clément DOUIN
74fcc0d44f
fix target name in CI 2024-11-27 10:10:20 +01:00
Clément DOUIN
6dc448b062
clean both flake and non-flake API 2024-11-27 10:04:12 +01:00
Clément DOUIN
7806de626e
put back other systems except i686-windows 2024-11-26 11:23:02 +01:00
Clément DOUIN
b6faf069cb
add missing isStatic in CI 2024-11-26 11:15:10 +01:00
Clément DOUIN
250ef63030
link to -lunwind 2024-11-26 11:09:29 +01:00
Clément DOUIN
4781f92ce8
bump rust to 1.82.0 2024-11-26 11:05:25 +01:00
Clément DOUIN
6b45314f1a
try to fix windows 2024-11-26 10:56:33 +01:00
Clément DOUIN
842db08710
put back real himalaya sources 2024-11-26 10:46:07 +01:00
Clément DOUIN
69e66b307a
clean all 2024-11-26 10:37:14 +01:00
Clément DOUIN
d7c565cadc
remove empty libgcc_eh 2024-11-26 10:06:48 +01:00
Clément DOUIN
91ca961e3d
use libunwind from llvmPackages 2024-11-26 10:02:55 +01:00
Clément DOUIN
5a1a835791
add libunwind 2024-11-26 09:58:29 +01:00
Clément DOUIN
98715db67b
put back -lmcfgthread 2024-11-26 09:56:21 +01:00
Clément DOUIN
d1c5d0397e
remove NIX_LDFLAGS 2024-11-26 09:49:32 +01:00
Clément DOUIN
8f9e016936
bad 2024-11-26 09:23:51 +01:00
Clément DOUIN
b4f337ea0d
fix shell 2024-11-26 09:02:58 +01:00
Clément DOUIN
065493ac7a
isolate i686 windows with rust-platform-verifier 2024-11-26 07:31:35 +01:00
Clément DOUIN
e17c2544f3
add w32api 2024-11-25 22:55:49 +01:00
Clément DOUIN
f55fa1faad
try missing other deps bis 2024-11-25 22:53:26 +01:00
Clément DOUIN
eb07cb60d7
try missing other deps 2024-11-25 22:51:44 +01:00
Clément DOUIN
41f5aa4fe4
try missing -lunwind 2024-11-25 22:42:17 +01:00
Clément DOUIN
126756a2a4
try missing -lmcfgthread 2024-11-25 22:30:19 +01:00
Clément DOUIN
2054618ce8
manually link mcfthreads 2024-11-25 21:57:27 +01:00
Clément DOUIN
e8a74bb156
add missing mcfgthreads windows build input on i686 2024-11-25 21:38:00 +01:00
Clément DOUIN
d3cf63a39e
add back other systems 2024-11-25 21:12:09 +01:00
Clément DOUIN
dd860c5bf0
use new apple sdk api 2024-11-25 20:17:57 +01:00
Clément DOUIN
bc8f0b3c51
try 11.0 2024-11-25 18:29:09 +01:00
Clément DOUIN
c51e411dc1
use 10.12 bis 2024-11-25 18:27:49 +01:00
Clément DOUIN
4fb6d6569d
use 10.12 2024-11-25 18:04:17 +01:00
Clément DOUIN
aa698e0572
try with default sdk 2024-11-25 16:29:25 +01:00
Clément DOUIN
6206970f47
remove macos-15 2024-11-25 16:29:15 +01:00
Clément DOUIN
e64286c341
sdk 11 for x86_64 2024-11-25 16:10:11 +01:00
Clément DOUIN
54de48c98a
put back own fork 2024-11-25 16:09:21 +01:00
Clément DOUIN
5fbd407b44
fix macos trial 4 2024-11-25 16:06:48 +01:00
Clément DOUIN
5330073b98
fix macos trial 3 2024-11-25 16:00:59 +01:00
Clément DOUIN
105b8309dd
fix macos trial 2 2024-11-25 15:59:49 +01:00
Clément DOUIN
49de48151b
fix macos trial 1 2024-11-25 15:59:09 +01:00
Clément DOUIN
a38897d880
fix emulators 2 2024-11-25 15:28:17 +01:00
Clément DOUIN
b22aa9dcb0
fix emulators 2024-11-25 15:21:16 +01:00
Clément DOUIN
025eebb549
try to cross compile x86_64-apple from aarch64 2024-11-25 15:10:13 +01:00
Clément DOUIN
199355b7d9
adjust build packages 2024-11-25 14:49:03 +01:00
Clément DOUIN
51ba814ac1
add custom emulator 2024-11-25 14:40:57 +01:00
Clément DOUIN
17af039600
remove libiconv 2024-11-25 14:35:44 +01:00
Clément DOUIN
676eb30cd0
add NIXPKGS_ALLOW_UNSUPPORTED_SYSTEM 2024-11-25 14:18:12 +01:00
Clément DOUIN
8bb28cf7a3
test nixpkgs fix 2024-11-25 14:03:54 +01:00
Clément DOUIN
c8f226bbaa
fix typo 2024-11-25 13:31:15 +01:00
Clément DOUIN
c23cb86691
remove trace 2024-11-25 11:41:49 +01:00
Clément DOUIN
d826d8ddde
fix typo postInstall 2024-11-25 11:26:30 +01:00
Clément DOUIN
abbd67e5e1
use libiconv-darwin 2024-11-25 11:21:23 +01:00
Clément DOUIN
bbb09ec03b
move windows hack to default.nix 2024-11-25 11:17:31 +01:00
Clément DOUIN
18cc8a8769
force static libiconv 2024-11-25 10:53:02 +01:00
Clément DOUIN
fc5c6816a5
forward libiconv 2024-11-25 10:52:27 +01:00
Clément DOUIN
358affb6bf
fix build inputs set 2024-11-25 10:50:19 +01:00
Clément DOUIN
624a44f773
fix typo 2024-11-25 10:40:52 +01:00
Clément DOUIN
eb959c29e3
experiment empty libgcc_eh 2024-11-25 10:33:35 +01:00
Clément DOUIN
2b1973dbce
use apple_sdk 2024-11-25 10:31:17 +01:00
Clément DOUIN
fda9cf1e4f
disable static for windows 2024-11-25 10:30:57 +01:00
Clément DOUIN
379d1dc97d
patch package.nix 2024-11-25 08:48:33 +01:00
Clément DOUIN
72d4397012
debug postPatch 2024-11-25 08:44:39 +01:00
Clément DOUIN
311c87ecca
add pkg-config for non-Linux platforms 2024-11-25 08:32:03 +01:00
Clément DOUIN
2b670d90f0
remove libiconv 2024-11-25 08:31:24 +01:00
Clément DOUIN
38488e93db
add missing pkg-config for darwin 2024-11-25 08:30:45 +01:00
Clément DOUIN
82aca9c9ba
use libiconv from darwin pkgs 2024-11-25 08:28:52 +01:00
Clément DOUIN
623e2e0f68
try postPatch to fix lgcc_eh 2024-11-25 08:27:05 +01:00
Clément DOUIN
557d341b24
move libiconv from native to build inputs 2024-11-25 08:20:43 +01:00
Clément DOUIN
73fbb8ebf6
add missing libiconv 2024-11-25 08:09:35 +01:00
Clément DOUIN
f04362572f
try with unstable 2024-11-25 00:37:48 +01:00
Clément DOUIN
7ae109eaae
try pkgsStatic 2024-11-25 00:18:13 +01:00
Clément DOUIN
fcfb7adb16
check macos err msg 2024-11-25 00:14:03 +01:00
Clément DOUIN
ba4d2758cd
rename artifacts 2024-11-24 23:30:15 +01:00
Clément DOUIN
44d94be99d
make target mandatory input 2024-11-24 23:11:01 +01:00
Clément DOUIN
3852b5abca
add missing darwin libiconv input 2024-11-24 21:42:30 +01:00
Clément DOUIN
f250551c91
clean 2024-11-24 21:04:57 +01:00
Clément DOUIN
d9db1af6b2
fix armv7 typo 2024-11-24 16:07:59 +01:00
Clément DOUIN
c6b674ed1d
add missing mapping 2024-11-24 15:40:45 +01:00
Clément DOUIN
b6d690188a
add build-on-demand workflow 2024-11-24 15:19:51 +01:00
Clément DOUIN
7a9a4b5b1f
add more systems to ci 2024-11-24 01:43:54 +01:00
Clément DOUIN
80e0b54a26
Revert "fix binary ext in derivation alt"
This reverts commit af1cd2b895.
2024-11-24 01:28:42 +01:00
Clément DOUIN
af1cd2b895
fix binary ext in derivation alt 2024-11-24 01:18:38 +01:00
Clément DOUIN
3e6a07821c
fix binary ext in derivation 2024-11-24 01:15:15 +01:00
Clément DOUIN
4d243aaa9d
fix runner 2024-11-24 00:59:02 +01:00
Clément DOUIN
bed96518c2
fix target ci 2024-11-24 00:33:00 +01:00
Clément DOUIN
3290074618
clean implem part 1 2024-11-24 00:32:11 +01:00
Clément DOUIN
2b115dd284
make things working for windows (wip) 2024-11-24 00:11:52 +01:00
Clément DOUIN
f35fa7cdb4
test d 2024-11-23 21:38:44 +01:00
Clément DOUIN
dc5ca1999b
test c 2024-11-23 21:31:34 +01:00
Clément DOUIN
d9699a3cb9
test b 2024-11-23 21:10:48 +01:00
Clément DOUIN
524b22cb5e
test a 2024-11-23 20:59:37 +01:00
Clément DOUIN
cffce1140a
try to fix windows build 2024-11-23 20:47:12 +01:00
Clément DOUIN
c4ae8626ed
use stdenv from pkgsCross on windows 2 2024-11-23 16:03:39 +01:00
Clément DOUIN
a9bb2d287f
use stdenv from pkgsCross on windows 2024-11-23 15:54:00 +01:00
Clément DOUIN
6f86884ab4
fix darwin framework version 2024-11-23 15:22:53 +01:00
Clément DOUIN
7641090c4e
fix host condition 2024-11-23 14:48:58 +01:00
Clément DOUIN
ddef9e5cc8
fix throw condition 2024-11-23 14:36:32 +01:00
Clément DOUIN
942bf5d163
fix nix-build args 2024-11-23 14:21:51 +01:00
Clément DOUIN
8c08b67be3
refactor cross systems structure 2024-11-23 14:06:32 +01:00
Clément DOUIN
4fb7ff93db
adjust crossSystem condition 2024-11-23 13:41:43 +01:00
Clément DOUIN
83621cc0c0
fix null condition 2 2024-11-23 12:37:16 +01:00
Clément DOUIN
78b2be8499
fix null condition 2024-11-23 12:35:24 +01:00
Clément DOUIN
e814b8aec2
add missing arg 2024-11-23 12:35:03 +01:00
Clément DOUIN
772f689eda
test ci 2024-11-23 12:33:47 +01:00
Clément DOUIN
924d3cfefd
Merge pull request #505 from KoviRobi/crane
Use crane instead of naersk
2024-11-22 11:51:06 +01:00
Clément DOUIN
4597b01e78
bump pimalaya libs 2024-11-22 11:20:02 +01:00
Kovacsics Robert
90c3bef172 Use Crane instead of Naersk
See
3cb338ce81/examples/quick-start/flake.nix

Fixes pimalaya/himalaya#483
2024-11-22 10:17:42 +00:00
Kovacsics Robert
590d0a0e8c Fix warning about getExe deprecation 2024-11-22 10:15:22 +00:00
Clément DOUIN
d62486f4c5
document build.rs 2024-11-22 10:28:24 +01:00
Clément DOUIN
5c4b03474e
add cargo features to the long version 2024-11-22 09:51:43 +01:00
Clément DOUIN
14c77e4629
Merge pull request #504 from KoviRobi/master
Draft: Long version & nix flakes
2024-11-22 09:50:55 +01:00
Kovacsics Robert
a0485ff8d1 Override git describe in CI 2024-11-21 16:31:11 +00:00
Kovacsics Robert
c36e72b5f6 Allow passing in GIT_DESCRIBE/GIT_REV to avoid git repo
This should allow building inside a flake
2024-11-21 16:31:11 +00:00
Kovacsics Robert
2e3a3397a5 Update cargo lock file for openssl removal 2024-11-21 16:20:27 +00:00
Clément DOUIN
53dc4c2e97
fix gmail message reading issue #498 2024-11-21 13:36:46 +01:00
Clément DOUIN
a88843669a
make oauth2 doc more clear about sharing tokens #499 2024-11-21 09:45:58 +01:00
Clément DOUIN
59ed5f8687
make oauth2 client secret optional #494 2024-11-21 09:20:23 +01:00
Clément DOUIN
130629309c
adjust code from new pimalaya/tui config api 2024-11-21 09:07:22 +01:00
Clément DOUIN
d7c4abf2e3
remove git2 default features
The crate comes with openssl-related features by default, which breaks
the CI on MacOS.
2024-11-20 15:49:22 +01:00
Clément DOUIN
36f3690cba
fix long version on nix and ci 2024-11-20 15:00:17 +01:00
Clément DOUIN
396a91a322
improve long version arg #496 2024-11-11 21:42:16 +01:00
Clément DOUIN
d4b81a8294
replace reset color by black in config sample #495 2024-11-11 17:56:49 +01:00
Clément DOUIN
92814d6043
fix pre-release archives
commit f644ba7052
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Wed Oct 30 16:30:39 2024 +0100

    do not remove share folder

commit 00e7148352
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Wed Oct 30 16:21:35 2024 +0100

    upload folder and exe instead of archive
2024-10-30 16:50:15 +01:00
Clément DOUIN
fecbae001c
clean darwin fix
commit 994515f3dbd3fa41c50fb5a080d584a7be365601
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 19:38:43 2024 +0100

    clean

commit ac3d181628
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 19:25:35 2024 +0100

    replace buildInputs by NIX_LDFLAGS

commit 4d69fd56e2
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 19:06:03 2024 +0100

    remove NIX_LDFLAGS

commit 69a6d17570
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 18:50:52 2024 +0100

    use framework 11.0

commit 97ee430bc8
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 18:19:42 2024 +0100

    revert flake lock

commit 61905bb849
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 18:01:21 2024 +0100

    clean aarch64 nix conf

commit 55594cc6a7
Author: Clément DOUIN <clement.douin@posteo.net>
Date:   Tue Oct 29 17:41:48 2024 +0100

    replace rustls-native-certs by rustls-platform-verifier
2024-10-29 19:39:43 +01:00
Clément DOUIN
ff1996107b
add missing darwin frameworks 2024-10-29 16:57:43 +01:00
Clément DOUIN
d54e2b31b7
use nixpkgs unstable 2024-10-29 12:12:42 +01:00
Clément DOUIN
c44cac50eb
use back apple sdk 11.0 on x86_64-darwin 2024-10-29 12:09:41 +01:00
Clément DOUIN
7b55da8c40
revert flake nixpkgs rev 2024-10-29 11:49:06 +01:00
Clément DOUIN
52aa6336a0
bump flake deps, use apple sdk 11.0 on x86_64-darwin 2024-10-29 11:21:27 +01:00
Clément DOUIN
7c17f801eb
try to fix macos-13 build 2024-10-29 10:53:03 +01:00
Clément DOUIN
b6068ef9e7
fix MML markup being displayed in HTML-based reply thread 2024-10-29 10:46:27 +01:00
Clément DOUIN
6ff3771135
Merge branch 'pimalaya-tui-refactor' 2024-10-28 11:29:37 +01:00
Clément DOUIN
151adf09e6
pin pimalaya core lib versions 2024-10-28 11:29:15 +01:00
Clément DOUIN
0101f7bf34
reflect docs and sample to new structure 2024-10-26 11:39:09 +02:00
Clément DOUIN
3b271c3e67
bump pimalaya core v1 2024-10-24 15:21:13 +02:00
Clément DOUIN
a0dea19cdf
wip: use shared stuff from pimalaya-tui 2024-10-16 11:46:12 +02:00
Clément DOUIN
2386d0f517
make imap clients pool more reliable 2024-09-28 16:00:10 +02:00
Clément DOUIN
2083e106f8
improve smtp retry algorithm 2024-09-25 08:31:06 +02:00
Clément DOUIN
32b72fb769
bump deps 2024-09-24 16:24:37 +02:00
Clément DOUIN
55ecb547c1
use git version of secret and keyring libs 2024-09-20 10:24:26 +02:00
Clément DOUIN
24c9e3b384
fix smtp backend typo in config sample 2024-09-20 08:33:20 +02:00
Clément DOUIN
63cf9ca3da
bump deps 2024-09-19 10:36:59 +02:00
Clément DOUIN
08f299f186
adjust readme outdated version section wording 2 2024-09-17 21:12:49 +02:00
Clément DOUIN
553ecd3c23
adjust readme outdated version section wording 2024-09-17 21:11:16 +02:00
Clément DOUIN
47d61d2c3d
adjust readme installation section 2024-09-17 21:09:47 +02:00
Clément DOUIN
e1f6739be3
fix typo in readme 2024-09-17 21:07:37 +02:00
Clément DOUIN
ab56c493aa
improve install section for v1.0.0 2024-09-17 21:06:32 +02:00
Clément DOUIN
adadc78743
rename test gh workflow to pre-release 2024-09-17 10:03:19 +02:00
Clément DOUIN
ff56268fb3
bump action checkout to v4 2024-09-17 09:54:00 +02:00
Clément DOUIN
5d0ea22e3c
make artifact name unique 2024-09-17 09:47:17 +02:00
Clément DOUIN
90183fc302
upload all result to artifacts 2024-09-17 09:18:59 +02:00
Clément DOUIN
a6344d682d
Revert "remove cargo cache"
This partially reverts commit 2d53144c7c.
2024-09-17 09:02:02 +02:00
Clément DOUIN
2d53144c7c
remove cargo cache
Looks like cargo cache is inside the Nix store.
2024-09-17 08:55:54 +02:00
Clément DOUIN
a61ff559e6
fix upload artifact path 2024-09-17 08:22:19 +02:00
Clément DOUIN
6b2e018ea3
fix test.yml workflow syntax issue 3 2024-09-17 08:08:36 +02:00
Clément DOUIN
0cf6ba01ce
fix test.yml workflow syntax issue 2 2024-09-17 08:05:42 +02:00
Clément DOUIN
360284184e
fix test.yml workflow syntax issue 2024-09-17 08:03:12 +02:00
Clément DOUIN
a6300e6498
cache cargo home 2024-09-17 07:59:23 +02:00
Clément DOUIN
6789993913
adjust artifact path 2 2024-09-17 07:13:27 +02:00
Clément DOUIN
068bb4c853
adjust artifact path 2024-09-17 06:55:02 +02:00
Clément DOUIN
34fbf5b603
upload built test binary to github artifacts 2024-09-17 06:46:57 +02:00
Clément DOUIN
681837b48d
uncomment {imap,smtp}.auth.type in sample 2024-09-08 23:25:11 +02:00
Clément DOUIN
ee91a41fbb
cargo update 2024-09-08 23:18:32 +02:00
Clément DOUIN
e31bbf4b7b
fix tls issues 2024-09-07 22:02:23 +02:00
Clément DOUIN
2b5e2c1c14
fix wrong printer method used for folder listing 2024-09-07 00:04:04 +02:00
Clément DOUIN
bdb78f98ba
add exit code log 2024-09-04 11:55:32 +02:00
Clément DOUIN
74ec31014c
fix ci macos version 2024-09-04 11:38:46 +02:00
Clément DOUIN
afd7d79e41
try to fix ci 2024-09-04 11:20:21 +02:00
Clément DOUIN
a2fa0dcf55
remove imap and smtp auth serde flatten 2024-09-03 12:02:02 +02:00
Clément DOUIN
cfc88118bb
clean unused deps 2024-09-03 11:02:23 +02:00
Clément DOUIN
cce0baf81a
fix pimalaya-tui package name 2024-09-01 16:07:50 +02:00
Clément DOUIN
b92d7b4a08
make use of pimalaya_tui::config::TomlConfig 2024-09-01 13:46:56 +02:00
Clément DOUIN
6f5f943875
remove unused inquire dependency 2024-08-31 07:24:03 +02:00
Clément DOUIN
5a22cab781
make use of pimalaya-tui 2024-08-30 12:13:06 +02:00
Clément DOUIN
c5b33b9623
update fundings 2024-08-30 08:02:11 +02:00
Clément DOUIN
248a7b97a2
fix typo in readme#features section 2024-08-28 12:12:01 +02:00
Clément DOUIN
bd2a425832
improving config sample doc, part 2 2024-08-28 11:55:02 +02:00
Clément DOUIN
3f4a1e7eb2
improving config sample doc, part 1 2024-08-27 11:55:00 +02:00
Clément DOUIN
bec2522e7f
improve readme faq sections 2024-08-27 10:47:35 +02:00
Clément DOUIN
3044dda8f4
fix readme typo 2024-08-27 10:37:01 +02:00
Clément DOUIN
f793d60ca2
add readme faq missing questions 2024-08-27 10:35:31 +02:00
Clément DOUIN
3fa617cf8f
add readme config example for main email providers 2024-08-27 08:56:36 +02:00
Clément DOUIN
cd3f5ff6a6
refactor readme 2024-08-27 08:13:39 +02:00
Clément DOUIN
3d9c45e374
make deps point to new pimalaya organization path 2024-08-27 07:21:01 +02:00
Clément DOUIN
48382b3e45
fix imap env list page size 0 2024-08-25 08:43:40 +02:00
Clément DOUIN
b93642b3bc
make imap envelope addresses parser more relaxed 2024-08-25 08:14:47 +02:00
Clément DOUIN
519955fb96
make imap client skip malformed fetches 2024-08-24 11:52:13 +02:00
Clément DOUIN
470815a227
improve imap auth mechanism selection and auto id exchange 2024-08-21 12:02:21 +02:00
Clément DOUIN
d823f32c31
put imap retry behind the oauth2 cargo feature 2024-08-20 11:38:16 +02:00
Clément DOUIN
cf064f8e0d
add missing row height for accounts table 2024-08-20 10:58:07 +02:00
Clément DOUIN
8ccabf1fc0
make tables more customizable
All tables can customize the color of their column, and the envelopes
table can customize its flag chars.
2024-08-20 10:53:21 +02:00
Clément DOUIN
daf2c7c87a
make table full width 2024-08-19 09:55:08 +02:00
Clément DOUIN
444efc6beb
make imap env list fetch in chunks 2024-08-18 10:40:07 +02:00
Clément DOUIN
0ccee5082a
wip: try to fix proton bridge 2024-08-17 15:35:13 +02:00
Clément DOUIN
b45944ef46
add attachment check in env list flags column 2024-08-17 11:55:41 +02:00
Clément DOUIN
d85bc1e8ae
Merge branch 'v1' 2024-08-16 14:29:46 +02:00
Clément DOUIN
146f5f628a
pin pimalaya libs to fixed version 2024-08-16 14:29:02 +02:00
Clément DOUIN
d26314cd48
fix cargo features 2024-08-13 11:49:46 +02:00
Clément DOUIN
f9b92e6e7a
Merge branch 'thread'
Replaced `imap` crate by `imap-{types,codec,flow,client}`, and added
thread support for IMAP and Maildir.
2024-05-29 10:55:54 +02:00
Clément DOUIN
c6cf93a276
bump imap libs 2024-05-29 10:55:21 +02:00
Clément DOUIN
f1371f42e4
Merge pull request #467 from gl-yziquel/yziquel
Enabling HIMALAYA_CONFIG to locate configuration file.
2024-05-27 09:27:29 +02:00
Clément DOUIN
ec3f915922
bump email-lib and imap-flow suite 2024-05-26 13:36:25 +02:00
Clément DOUIN
16d273febc
wip: fix thread id mapping 2024-05-26 11:54:13 +02:00
Clément DOUIN
b773218c94
wip: fix printer, make thread compatible with it 2024-05-23 15:04:48 +02:00
Guillaume Yziquel
1b35da2d07 Using HIMALAYA_CONFIG as a way to provide himalaya configuration file from environment. 2024-05-22 21:53:15 +00:00
Clément DOUIN
6cbfc57c83
wip: add message thread command 2024-05-22 11:07:40 +02:00
Clément DOUIN
2eff215934
wip: style thread tree using crossterm 2024-05-21 15:25:24 +02:00
Clément DOUIN
55ba892436
wip: use custom struct ThreadedEnvelopes 2024-05-18 09:45:03 +02:00
Clément DOUIN
90e12ddc51
wip: design basic tree using petgraph 2024-05-17 23:22:06 +02:00
Clément DOUIN
7a951b4830
fix envelope list --max-width arg
The --max-width has been accidentally renamed --table-max-width. This
commit revert the thing.
2024-05-15 14:44:38 +02:00
Clément DOUIN
f3151c3f84
rearrange try_to_sync_cache_builder func 2024-05-14 18:34:45 +02:00
Perma Alesheikh
098ae380c3
use comfy-table instead of builtin impl for table
This is to out-source the table making in terminal to the external
library.

I removed the in-house table implementation since it is not used any
more, and had been replaced by comfy-table, we use this instead.

I also have reimplemented table_max_width since new implementation
removed max width , with the new implemetation it will work again.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-05-14 18:23:34 +02:00
Perma Alesheikh
1e448e56eb
replace dialoguer with inquire
In order to reduce our dependencies, we are replacing the dependencies
that use console_rs with those that use crossterm.

This commit will completely replace dialoguer with inquire.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-05-14 18:20:54 +02:00
Clément DOUIN
d54dd6429e
replace default log level warn by off 2024-05-14 18:19:53 +02:00
Clément DOUIN
9dee1784df
replace imap by imap-codec 2024-05-14 18:19:39 +02:00
Perma Alesheikh
c779081381
use inquire for one set of prompts
Considering that "dialoguer" uses "console" backend library, and the
future of himalaya is reliant on "crossterm", we are moving from
dialoguer, to inquire.

This commit is going to include some experimental changes to one file.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-05-04 11:36:07 +02:00
Clément DOUIN
ccddfeb799
fix install.sh aarch64 2024-04-20 09:07:45 +02:00
Clément DOUIN
30f00d0867
fix mailto being parsed after cli 2024-04-20 07:52:22 +02:00
Clément DOUIN
3c417d14eb
make nix flake apps usable 2024-04-20 07:51:15 +02:00
Clément DOUIN
8d0f013374
fix release gh action 2024-04-20 07:50:57 +02:00
Clément DOUIN
a389434fde
remove darwing cross compile 2024-04-19 22:11:59 +02:00
Clément DOUIN
095d519dd0
clean remaining parts 2024-04-19 22:11:16 +02:00
Clément DOUIN
087a0821bc
fix typo getExec 2024-04-19 20:44:03 +02:00
Clément DOUIN
cf6000f1e4
clean apps and packages part 1 2024-04-19 17:04:02 +02:00
Clément DOUIN
b4fcb427a4
replace tree by ls -R 2024-04-19 11:57:59 +02:00
Clément DOUIN
849deb9a20
replace ls by tree 2024-04-19 09:56:25 +02:00
Clément DOUIN
c022e66289
fix gh action ls 2024-04-19 09:44:16 +02:00
Clément DOUIN
5003abe1e1
fix post install 2024-04-19 08:34:15 +02:00
Clément DOUIN
4590348bf2
merge archives packages with regular ones 2024-04-19 07:22:25 +02:00
Clément DOUIN
a066774f22
clean namings 2024-04-18 23:34:15 +02:00
Clément DOUIN
c57988770a
fix windows ext 2024-04-18 22:42:11 +02:00
Clément DOUIN
9b1a090329
fix nix run args 2024-04-18 22:29:10 +02:00
Clément DOUIN
7fbd97ceba
add nix test github workflow 2024-04-18 22:06:24 +02:00
Clément DOUIN
7899484942
separate simple packages from archives packages for releases 2024-04-18 16:54:48 +02:00
Clément DOUIN
10de8e9fb4
release v1.0.0-beta.4 2024-04-16 22:26:10 +02:00
Clément DOUIN
23ae40e728
enable clap cargo feature wrap_help, update changelog 2024-04-16 07:18:44 +02:00
Clément DOUIN
220008d0b4
fix in reply to header skipped from mailto url 2024-04-15 14:29:30 +02:00
Clément DOUIN
a9e177b77b
bump deps 2024-04-15 12:29:18 +02:00
Perma Alesheikh
7f8b08bd81
remove unused crates from dependencies
After using cargo shear, there are 3 crates that are shown to be unused.
I have checked the files, no mentions there. I have removed them, and
cargo check --all-features --all-targets gives no errors.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-04-15 12:18:43 +02:00
Perma Alesheikh
5a0ff83a5e
replace anyhow and log with color_eyre and tracing
Since Himalaya is intended to be ran as a CLI in the terminal emulator
environment, their user experience could vastly improve with better and
more colorful error messages and logging.

This change will replace more minimal libraries for error-reporting/han-
dling with their more advanced counterparts.

Since these crates have tight integrations, this commit will change both
in one shot.

Also we have don't need env_logger any more. So I also have removed that
guy as well.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-04-15 12:17:56 +02:00
Clément DOUIN
cc79f5cc38
fix wrong deps 2024-04-14 16:03:04 +02:00
Clément DOUIN
58df66b5fa
update deps 2024-04-07 11:47:09 +02:00
Clément DOUIN
d95f277bab
adjust code for pimalaya new errors + sync hash 2024-04-05 11:05:55 +02:00
Clément DOUIN
ee9718a482
add message.delete.style config option 2024-03-23 17:07:41 +01:00
Clément DOUIN
a5ef14da9f
bump deps 2024-03-21 14:45:39 +01:00
Clément DOUIN
2cf30e2fda
fix template cursor row issue 2024-03-21 14:27:09 +01:00
Clément DOUIN
799ee8b25b
use new template cursor api 2024-03-21 13:57:26 +01:00
Clément DOUIN
1c23adc8a2
fix unit tests 2024-03-16 22:31:32 +01:00
Clément DOUIN
7ee710634b
bump deps, make global config option repeatable 2024-03-16 22:20:19 +01:00
Clément DOUIN
3868c62511
prevent unknown fields at top config level 2024-03-12 11:57:37 +01:00
Clément DOUIN
362a5ca647
add missing envelope property to 2024-03-10 11:40:37 +01:00
Clément DOUIN
2566d45a96
fix typos in changelog and readme 2024-03-10 11:06:50 +01:00
Clément DOUIN
3b53bcc529
add note about envelopes filtering and sorting in readme 2024-03-10 10:40:45 +01:00
Clément DOUIN
c56a5f285b
bump email-lib, update changelog 2024-03-10 10:37:50 +01:00
Clément DOUIN
c1ffc40bd3
add list envelopes query cli doc 2024-03-10 10:01:01 +01:00
Jalil David Salamé Messina
ed5407a5c7
remove flake-utils from the flake inputs
As requested in <https://todo.sr.ht/~soywod/pimalaya/131> I removed
flake-utils. This reduces the number of flake inputs and doesn't add
much code.

The way this works, is that instead of `eachDefaultSystem` we have a
function `forEachSupportedSystem`, this function generates an attrset
with a key for each system in the `supportedSystems` array, whose value
is the result of calling the provided function with the system as an
argument:

```nix repl
repl> forEachSupportedSystem f
{
  "x86_64-linux" = f "x86_64-linux";
  ...
}
```

This is slightly clumsier than `flake-utils.lib.eachDefaultSystem`,
which rewrites the returned attrset, but it is much less code and
simpler to understand.

I tested the build with `nix build` on `x86_64-linux` and it still works
c:
2024-03-09 13:54:37 +01:00
Jalil David Salamé Messina
da49352d4e
add himalaya-watch@.service
As discussed in
<https://github.com/nix-community/home-manager/issues/5069>.

I set `ExecStart=%install_dir%/himalaya` so when packaging himalaya
people nee to explicitly set the path to himalaya (i.e. `sed
's:%install_dir%:/usr/bin:' assets/himalaya-watch@.service`). This is
done automatically in `install.sh` if `$PREFIX` is `/usr`, Otherwise
the packager should handle it themselves

For `nix` it would be (`sed 's:%install_dir%:$out/bin:'
assets/himalaya-watch@.service`). I don't know where it should be placed
(probably `$out/share/systemd/user` as nix will add that to
`$XDG_DATA_DIRS` which is searched by `systemctl --user`.

I swear I checked the address like 4 times before sending the email, I
have no idea how I managed to mess it up T-T. I was wondering why the
formatting was so messed up in sr.ht.
2024-03-09 13:49:44 +01:00
Jalil David Salamé Messina
8867c99b91
fix(CONTRIBUTING): Patches are requested to the wrong email
I only updated the mailto link in the [last
patch](https://lists.sr.ht/~soywod/pimalaya/patches/50059). This also
updates the text.
2024-03-09 11:08:29 +01:00
Clément DOUIN
a8e6dea162
bump email-lib 2024-03-09 11:06:05 +01:00
Clément DOUIN
46bf3eebfc
improve envelope list query error diagnostics 2024-02-29 10:21:01 +01:00
Clément DOUIN
1e7adc5e0c
add query arg to envelope list command 2024-02-28 09:09:03 +01:00
Clément DOUIN
c28b4c6bb3
fix missing maildir deps for account-sync feature 2024-02-25 14:26:23 +01:00
Clément DOUIN
1f6f2fcc11
update changelog 2024-02-25 11:04:37 +01:00
Clément DOUIN
8e8040e036
bump email-lib@0.22.3 2024-02-25 10:44:58 +01:00
Clément DOUIN
1699a581ce
update flake and cargo 2024-02-25 09:07:40 +01:00
Clément DOUIN
04982a4644
fix cargo features issues 2024-02-24 14:27:05 +01:00
Clément DOUIN
556949a684
bump email lib 2024-02-24 11:23:26 +01:00
Clément DOUIN
e945c4b8e2
replace sqlite by sled for id mapping storing 2024-02-24 09:37:55 +01:00
Clément DOUIN
0e35a0cd64
add account check-up command 2024-02-24 07:55:37 +01:00
Clément DOUIN
79da9404f3
fix smtp discovery wrong config 2024-02-23 08:46:21 +01:00
Clément DOUIN
5cb247169a
fix unit tests 2024-02-23 08:25:15 +01:00
Clément DOUIN
faeda95978
fix flatten account config sample 2024-02-21 22:54:59 +01:00
Clément DOUIN
123224963d
adjust changelog 2024-02-21 22:51:09 +01:00
Clément DOUIN
1907817392
fix envelope issues preventing sync to work properly 2024-02-21 22:16:06 +01:00
Clément DOUIN
3e0cf0cfda
refactor backend system, remove accouts flattening 2024-02-21 11:38:50 +01:00
Clément DOUIN
76ab833a62
fix broken link readme#features 2024-02-09 07:10:39 +01:00
Clément DOUIN
8741508413
removed github issue template
GitHub Issues have been definitely closed, so the template does not
make sense anymore.
2024-02-04 21:03:16 +01:00
Clément DOUIN
dd7e1a02be
improve pre and post edit choices interaction 2024-02-04 12:13:14 +01:00
Clément DOUIN
35c1453863
added wizard warning about google passwords 2024-02-04 11:36:11 +01:00
Clément DOUIN
a945e1bf2f
make watch hooks cumulative 2024-02-03 22:30:15 +01:00
Clément DOUIN
83306d5f6a
fix pgp unit test 2024-01-28 08:42:18 +01:00
Clément DOUIN
e5cf39b351
release v1.0.0-beta.2 2024-01-27 22:49:29 +01:00
Clément DOUIN
34a0978588
fix dead links and config sample 2024-01-27 22:47:06 +01:00
Clément DOUIN
7cdfecd7dd
fix changelog and contributing guide typos 2024-01-27 11:28:26 +01:00
Clément DOUIN
b1cc03d2c7
fix readme typo (part 2) 2024-01-27 11:24:23 +01:00
Clément DOUIN
72c3e55bba
fix readme typo 2024-01-27 11:21:59 +01:00
Clément DOUIN
4f9705952a
refactor new backend api 2024-01-27 11:15:03 +01:00
Clément DOUIN
16266dbc0b
fix message save and send prevented due to clap help 2024-01-22 12:03:33 +01:00
Clément DOUIN
39d2dec9e8
fix readme typo 2024-01-22 10:42:26 +01:00
Clément DOUIN
4d288b9d51
fix missing notmuch backend features, improve docs 2024-01-22 10:39:06 +01:00
Clément DOUIN
8cebdf9e90
remove account config from context builder new fn 2024-01-21 22:09:14 +01:00
Clément DOUIN
3137e1e851
add back notmuch features (part 1) 2024-01-21 15:59:03 +01:00
Clément DOUIN
a700f358fb
clean autoconfig discovery 2024-01-18 22:01:22 +01:00
Clément DOUIN
7d4ad9c1d9
replaced autoconfig by custom email-lib account discovery module 2024-01-18 11:59:27 +01:00
Clément DOUIN
2342a83d0d
deny unknown fields on toml account config 2024-01-15 22:34:30 +01:00
Clément DOUIN
7eba3a5186
generate one autoconfig per email address 2024-01-15 15:27:14 +01:00
Clément DOUIN
1246be8a5b
fix wizard serialization issues 2024-01-12 10:16:43 +01:00
Perma Alesheikh
a15e2c0442
allow module inception
Reasons:
- The containing module is already reexported, so repitition in
  namespace is unnecessary.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:13:31 +01:00
Perma Alesheikh
fc59757a9d
remove another unnecessary conversion
Reasons:
- Avoid unnecessary conversion, since into is called on an String value
  when String is expected, anyway.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:12:57 +01:00
Perma Alesheikh
87eac50eb7
remove comparison with boolean value
Reasons:
- The bool value itself is enough for the filter expression.
- Simplifies the expression.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:12:46 +01:00
Perma Alesheikh
0b066b7529
remove unnessary conversions to itself
Reasons:
- Remove unnecessary steps. into() is called on String when the expected
  type is already String.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:12:31 +01:00
Perma Alesheikh
a6440aaa27
remove unnecessary into_owned
Reasons:
- Remove unnecessary step.
- Avoid allocation when not needed.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:12:09 +01:00
Perma Alesheikh
2af1936ef8
use map_while to count for always err case.
Reasons:
- Filter_map will run forever if iterator only returns Err with lines.
  This is a possibility for "lines" iterators.
- Map_while will break the mapping the moment the iterator returns error.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:11:23 +01:00
Perma Alesheikh
b417ad11a0
use if let instead of a map with only sideeffects
Reasons:
- Map is usually intended for transforming a value, and as is not
  idiomatically used for only doing side-effects and control flow.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:10:28 +01:00
Perma Alesheikh
0f097fe293
remove double referencing
Reasons:
- The compiler will immediately dereference the referenced reference.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:09:02 +01:00
Perma Alesheikh
945c567f35
remove reference over trait implemented type
Reasons:
- String already implement the AsRef<str>.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:08:41 +01:00
Perma Alesheikh
2ef477c225
remove needless update using default
Reasons:
- Every field is either turned-off entirely or assigned a value when it
  needs one.
- Avoids the situation when a new field is introduced and is assigned a
  default value when it is not desired.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:07:37 +01:00
Perma Alesheikh
54287d40b8
replace into implementation with from
Reasons:
- From Implementation also implements Into trait.
- Adhere to the recommendation by the Into trait's comments.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 22:04:26 +01:00
Clément DOUIN
bd1ac45a58
remove empty string from println call 2024-01-09 22:02:09 +01:00
Perma Alesheikh
0ff940871b
use char when replacing a single character
Reasons:
- More idiomatic use of string.

Considering that they are constants, I don't anticipate any performance
gains related to heap-allocation.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 21:55:20 +01:00
Perma Alesheikh
f7a7937cb1
use as_deref instead of as_ref for account.name
Reasons:
- More concise.
- Avoids the need for map(String::str).

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 21:54:21 +01:00
Perma Alesheikh
59fefd7c78
use or instead of or_else
Reasons:
- Closure is not needed.
- Makes it more concise.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 21:49:33 +01:00
Perma Alesheikh
8016ecb5a0
define SendmailConfig once
Reasons:
- Makes the declaration more concicse.
- Avoids the mutation.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-09 21:42:28 +01:00
Clément DOUIN
6fcdf7ea10
fix bad prompt_passwd for pgp config 2024-01-09 21:37:53 +01:00
Clément DOUIN
6f9f75cfd2
plug autoconfig to imap and smtp wizards 2024-01-09 21:36:17 +01:00
Clément DOUIN
b0d7e773dc
renamed sync feature to account-sync, put wizard stuff under feature 2024-01-09 09:28:45 +01:00
Perma Alesheikh
921194da5c
remove empty str inside println
Reasons:
- Functionally it has the same result.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-08 23:02:03 +01:00
Perma Alesheikh
95eed65193
use empty ok instead of wrapping empty expression
Reasons:
- It is more readable since the evaluated result is more explicit.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-08 23:01:52 +01:00
Perma Alesheikh
3cca9ac9e8
use static instead of const for lazy values
Reasons:
- Every time a const is referenced, a new instance of the Cell, Mutex,
  or AtomicXxxx is created, negating the purpose of using these types.
  To address this issue, the const value should be stored within
  a static item.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-08 23:01:41 +01:00
Perma Alesheikh
d2ad386eaa
use as as_deref instead of as_ref and mapping on str
Reasons:
- Make the code more direct and concise.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-08 23:01:26 +01:00
Perma Alesheikh
6173495cb6
use iter instead of into_iter
Reasons:
- This is functionally similar to into_iter since it is reference.
- It does not consume the list.

Signed-off-by: Perma Alesheikh <me@prma.dev>
2024-01-08 23:01:03 +01:00
Clément DOUIN
42226abc9c
improve contributing section 2024-01-08 22:47:08 +01:00
Clément DOUIN
161f35d20e
clean cargo features 2024-01-08 10:34:37 +01:00
Clément DOUIN
819bdc84b3
fix features warns and save sent message copy option 2024-01-08 00:33:07 +01:00
Clément DOUIN
a6b863759c
add one cargo feature per backend feature 2024-01-07 23:48:45 +01:00
Clément DOUIN
9ffac16e05
adjust readme matrix workspace name 2024-01-06 08:51:51 +01:00
Clément DOUIN
95c078c327
improve readme contributing section 2024-01-06 08:41:11 +01:00
Clément DOUIN
45ce05ec4d
fix typos 2024-01-05 21:59:41 +01:00
prma
38c8a67ddd
fix: remove printer message from completions command
this way the output command can be used to source completion
2024-01-05 21:56:47 +01:00
Clément DOUIN
89fbb8a9db
update screenshot readme 2024-01-05 11:50:16 +01:00
Clément DOUIN
70fad9b1fd
fix default command 2024-01-03 22:49:39 +01:00
Clément DOUIN
0352e91e36
improve backend features management for every command 2024-01-03 12:58:44 +01:00
Clément DOUIN
a8c6756f56
fix envelope listing 2024-01-02 12:21:12 +01:00
Clément DOUIN
37c352ea7f
Merge pull request #463 from w3irdrobot/sendmail-fix
allow account.sendmail when sendmail feature is on
2024-01-01 22:32:51 +01:00
Clément DOUIN
6af2342316
Merge pull request #464 from w3irdrobot/fix-ambiguous-email
fix ambiguous email export
2024-01-01 22:30:45 +01:00
w3irdrobot
6b6e5cb1fa
fix ambiguous email export 2024-01-01 15:57:30 -05:00
w3irdrobot
77206b2326
allow account.sendmail when sendmail feature is on 2024-01-01 13:49:31 -05:00
Clément DOUIN
12e71a5ba8
improve release process 2024-01-01 16:22:30 +01:00
Clément DOUIN
ce2b292d2b
update license year 2024-01-01 00:31:19 +01:00
Clément DOUIN
be877f0b3e
fix linux, macos and windows release builds 2024-01-01 00:27:45 +01:00
Clément DOUIN
131acd6230
update flake deps, fix gh release interpreter 2023-12-31 11:05:08 +01:00
Clément DOUIN
fa2c6c44bc
prepare v1.0.0-beta 2023-12-31 09:24:42 +01:00
Clément DOUIN
a59d1ca2c6
refactor imap and smtp encryption options 2023-12-30 22:38:25 +01:00
Clément DOUIN
eee17f9173
fix oauth2 serde config namings 2023-12-29 22:33:15 +01:00
Clément DOUIN
bc36ce1255
add imap.watch.timeout example in config sample 2023-12-29 21:00:48 +01:00
Clément DOUIN
8d12528da6
add imap.watch.timeout option 2023-12-29 20:52:15 +01:00
Clément DOUIN
5ede53476b
change watch imap envelope idle algorithm 2023-12-26 16:45:32 +01:00
Clément DOUIN
cdf0a9a846
bump email-lib@0.18.5 2023-12-24 15:11:21 +01:00
Clément DOUIN
2351cfdd28
fix redundant copy saved 2023-12-20 16:43:33 +01:00
Clément DOUIN
92a94c8ff1
fix missing serde rename 2023-12-20 15:03:03 +01:00
Clément DOUIN
cd7cecca6e
fix message send save copy 2023-12-20 14:55:09 +01:00
Clément DOUIN
3f2f691e85
add missing v1 changes to changelog 2023-12-20 11:13:24 +01:00
Clément DOUIN
4ab81c0fe9
add readme warning about docs mismatch 2023-12-20 09:11:06 +01:00
Clément DOUIN
9838854ec0
remove obsolete unit tests 2023-12-20 08:17:06 +01:00
Clément DOUIN
9632508dc7
init changelog 2023-12-19 17:44:16 +01:00
Clément DOUIN
77f5e590b8
bump all deps 2023-12-19 15:38:24 +01:00
Clément DOUIN
f398eb0d30
turn folder arg into flag for message copy, move and delete cmds 2023-12-19 15:37:23 +01:00
Clément DOUIN
c11f00d791
fix message and template send stdin issues 2023-12-19 15:36:56 +01:00
Clément DOUIN
73e1824a0d
bump email-lib@0.18.0 2023-12-16 10:20:29 +01:00
Clément DOUIN
6942c59097
improve folder alias management 2023-12-15 22:54:13 +01:00
Clément DOUIN
bcef05a54c
replace folder arg by flag for watch cmd 2023-12-15 08:52:17 +01:00
Clément DOUIN
d542b2496e
bump lib, fix config sample typo 2023-12-14 22:27:33 +01:00
Clément DOUIN
d6bf407653
move watch command from folder to envelope 2023-12-14 14:12:25 +01:00
Clément DOUIN
7fccdd822a
init folder watch command 2023-12-14 12:13:08 +01:00
Clément DOUIN
a68d297366
fix typo readme 2023-12-12 15:27:00 +01:00
Clément DOUIN
24bb6f10d7
fix broken link in readme 2023-12-12 15:25:49 +01:00
Clément DOUIN
b623468d15
Merge pull request #462 from soywod/backend-features
`v1.0.0-beta` ready for testing 🎉
2023-12-12 14:49:44 +01:00
Clément DOUIN
3e3f111d3b
fix typos 2023-12-11 22:01:48 +01:00
Clément DOUIN
2e0ec913cf
refactor configs to match new nested api from lib 2023-12-11 18:38:00 +01:00
Clément DOUIN
8e05be7f77
apply pr #461 due to conflicts, bump pimalaya crates 2023-12-10 22:01:49 +01:00
Clément DOUIN
203ed2f917
fix editor command hanging, add --preview flag for msg read cmd 2023-12-09 22:06:08 +01:00
Clément DOUIN
04e721d591
adjust api, test commands with a greenmail instance 2023-12-09 09:38:33 +01:00
Clément DOUIN
ef3214f36f
clean doc 2023-12-08 12:18:18 +01:00
Clément DOUIN
fff11fbe20
refactor template with clap derive api 2023-12-07 22:37:28 +01:00
Clément DOUIN
b28f12c367
refactor attachment with clap derive api 2023-12-07 21:59:12 +01:00
Clément DOUIN
b8ef771614
refactor message with clap derive api (part 2) 2023-12-07 18:50:46 +01:00
Clément DOUIN
a47902af7d
refactor message with clap derive api (part 1) 2023-12-07 12:19:45 +01:00
Clément DOUIN
5e1a03e3c1
refactor flag with clap derive api 2023-12-07 10:10:18 +01:00
Clément DOUIN
2c33dd2f9f
refactor envelope with clap derive api 2023-12-06 23:12:06 +01:00
Clément DOUIN
4a77253c1d
refactor folder with clap derive api 2023-12-06 22:13:50 +01:00
Clément DOUIN
abe4c7f4ea
refactor account with clap derive api 2023-12-06 18:09:49 +01:00
Clément DOUIN
d2308221d7
refactor man and completion with clap derive api 2023-12-05 22:38:08 +01:00
Clément DOUIN
7a10a7fc25
reorganize folder and cli structure 2023-12-05 15:06:26 +01:00
Clément DOUIN
8b1a289f4d
rename existing cargo features, fix imports 2023-12-04 22:26:49 +01:00
Clément DOUIN
ea9c28b9d7
fix config and oauth2 2023-12-04 16:25:56 +01:00
Clément DOUIN
c54ada730b
fix wizard 2023-12-03 22:31:43 +01:00
Clément DOUIN
f24a0475cc
fix imap credentials and pgp 2023-12-03 13:03:36 +01:00
Clément DOUIN
a5cacb3f67
build only used backends 2023-11-29 11:04:25 +01:00
Clément DOUIN
41a2f02699
rename config and account config 2023-11-29 07:52:08 +01:00
Clément DOUIN
fb8f356e8c
fix id mapper 2023-11-28 22:28:28 +01:00
Clément DOUIN
a0888067da
fix sync cache 2023-11-28 12:30:50 +01:00
Clément DOUIN
7629a66c9c
use email-lib git instead of path 2023-11-27 17:15:34 +01:00
Clément DOUIN
20f6973c55
plug missing other backend features 2023-11-27 15:49:55 +01:00
Clément DOUIN
9f6a9a1333
plug folder features 2023-11-26 14:16:55 +01:00
Clément DOUIN
1f88b27468
init backend override with list envelopes and send message 2023-11-26 12:16:07 +01:00
Clément DOUIN
cec658aff4
bump lib with backend features 2023-11-25 12:37:00 +01:00
Clément DOUIN
56fc31b367
bump mml-lib@v0.5.0 and email-lib@v0.15.3 2023-09-25 15:32:29 +02:00
Clément DOUIN
4b60379070
try fixing #132 by using shellexpand-utils
https://todo.sr.ht/~soywod/pimalaya/132
2023-08-29 11:28:20 +02:00
Clément DOUIN
606162452e
Merge pull request #454 from tim77/tim77-patch
add fedora/centos installation instructions
2023-08-28 10:32:44 +02:00
Artem Polishchuk
70fe936e3b docs: Add Fedora/CentOS installation instructions 2023-08-28 10:38:27 +03:00
Clément DOUIN
7ad1772c83
update pimalaya libs, prepare v0.9.0 2023-08-28 09:05:14 +02:00
Hugo Osvaldo Barrera
f61a1f6669
make sendmail-cmd optional
Use the common /usr/sbin/sendmail the default. This is a common default
hardcoded in many applications.

Fixes: https://todo.sr.ht/~soywod/pimalaya/126
2023-08-27 21:39:27 +02:00
Clément DOUIN
43c270bd44
update changelogs 2023-08-06 14:31:15 +02:00
Clément DOUIN
2b0f378a31
bump pimalaya libs 2023-08-06 09:23:23 +02:00
Clément DOUIN
176da9eeeb
add pgp commands support back 2023-08-05 22:53:39 +02:00
Clément DOUIN
0eed8f355d
add gpg support 2023-08-05 12:10:25 +02:00
Clément DOUIN
183c0272cc
fix pgp exports 2023-08-04 21:33:15 +02:00
Clément DOUIN
1ecceca1e6
bump libs 2023-08-03 10:15:02 +02:00
Clément DOUIN
99ec7c6d97
add pgp support 2023-08-02 18:03:47 +02:00
Clément DOUIN
183aa2f306
bump pimalaya-email a912fa 2023-07-20 12:54:08 +02:00
Clément DOUIN
a8bd265181
bump pimalaya-oauth2 0.0.4 with async 2023-07-20 11:43:28 +02:00
Clément DOUIN
fff82498ba
release v0.8.4 2023-07-18 17:48:19 +02:00
Clément DOUIN
5c360da80b
release v0.8.3 2023-07-18 17:24:52 +02:00
Clément DOUIN
34ad1add65
fix releases 2023-07-18 17:23:24 +02:00
Clément DOUIN
cb1178ee9d
release v0.8.2 2023-07-18 16:40:08 +02:00
Clément DOUIN
679007ba64
resolve folder aliases from backend implems instead #95 2023-07-17 11:31:28 +02:00
Clément DOUIN
4e43b97513
fix notmuch feature 2023-07-16 21:45:50 +02:00
Clément DOUIN
2f4bbcb1db
set up coredump 2023-07-13 15:39:41 +02:00
Clément DOUIN
e821fe06d9
update lib versions and changelog 2023-07-09 22:31:39 +02:00
Clément DOUIN
cac8280c8c
use tokio async runtime
last fixes before merge
2023-07-05 09:04:40 +02:00
Clément DOUIN
f8ca248bce
release v0.8.1 2023-06-15 16:51:03 +02:00
Clément DOUIN
7a6ebc0cd0
add new datetime options 2023-06-15 16:11:08 +02:00
Clément DOUIN
5599a1f5d0
Merge pull request #450 from kianmeng/fix-typos
fix typos
2023-06-13 16:21:36 +02:00
Kian-Meng Ang
5a17ae7316 fix typos
Found via `typos --format brief`
2023-06-13 21:48:15 +08:00
Clément DOUIN
7d96ca52fa
fix tests 2023-06-13 11:03:47 +02:00
Clément DOUIN
0f6f3439fb
update changelog 2023-06-13 10:24:15 +02:00
Clément DOUIN
c254f64569
refactor builders and sync 2023-06-13 10:14:20 +02:00
Clément DOUIN
ab1e8b7e45
fix dead links readme 2023-06-05 08:12:38 +02:00
Clément DOUIN
b5a5b0d42f
Merge pull request #449 from icp1994/sample-config
update sample config to v0.8
2023-06-04 15:30:27 +02:00
icp
ff004f0c2a
update sample config to v0.8 2023-06-04 14:25:41 +05:30
Clément DOUIN
696834c8dc
fix rust toolchain hash 2023-06-04 00:06:11 +02:00
Clément DOUIN
308805db17
update changelog 2023-06-03 23:43:07 +02:00
Clément DOUIN
dfff9064d7
release v0.8.0 2023-06-03 23:38:43 +02:00
Clément DOUIN
7aff3bbf9d
fix imap list envelopes pagination 2023-06-01 12:47:02 +02:00
Clément DOUIN
b800d6e6fc
fix empty plain taken instead of html 2023-05-31 21:49:52 +02:00
Clément DOUIN
d557d9e1df
prepare v0.8.0 2023-05-31 16:12:18 +02:00
Clément DOUIN
32b31db175
fix back read -t html 2023-05-30 23:07:10 +02:00
Clément DOUIN
65ac0c7702
improve tpl builders api 2023-05-30 00:34:15 +02:00
Clément DOUIN
5a2d842cbe
plug option email-sending-save-copy 2023-05-19 16:00:13 +02:00
Clément DOUIN
5da1148dc9
refactor wizard to handle password and oauth2 configuration 2023-05-19 15:26:53 +02:00
Clément DOUIN
d814ae904a
drastically simplified configs
Also started to refactor wizard (WIP).
2023-05-16 00:11:37 +02:00
Clément DOUIN
0ff77b5179
move id mapper from lib to CLI 2023-05-14 21:41:31 +02:00
Clément DOUIN
53538e36f9
fix sync deadlocks 2023-05-12 21:59:53 +02:00
Clément DOUIN
f8eed6ad14
fix smtp cmd password issue 2023-05-08 16:07:52 +02:00
Clément DOUIN
f4facd1761
fix config unit tests 2023-05-08 14:31:36 +02:00
Clément DOUIN
54ea9a3302
improve imap oauth2 api, add smtp oauth2 support 2023-05-07 22:10:48 +02:00
Clément DOUIN
441ce40e09
make secrets have default implem
Secrets use by default the keyring, and the entry is based on the name
of the current account to avoid conflicts.
2023-05-07 20:52:13 +02:00
Clément DOUIN
728f2555d7
improve oauth2 config reset 2023-05-07 10:18:58 +02:00
Clément DOUIN
5749bc3a82
update changelog with secret changes and oauth2 support 2023-05-07 00:16:47 +02:00
Clément DOUIN
5d21433816
fix smtp default config, fix cargo imports 2023-05-06 21:23:16 +02:00
Clément DOUIN
b478c545ad
refactor imap oauth2 and password config using sub crates from lib 2023-05-06 15:04:55 +02:00
Clément DOUIN
9dfdebb396
fix config unit tests 2023-05-05 00:28:50 +02:00
Clément DOUIN
e6c9a6e90e
init imap oauth2 support 2023-05-05 00:08:01 +02:00
Clément DOUIN
f026e48733
improve oauth2 config deserialization and configuration 2023-05-05 00:07:27 +02:00
Clément DOUIN
21f67bc7f5
set up imap oauth2 config 2023-05-04 12:17:43 +02:00
Clément DOUIN
e271ca4293
fix different toolchain channel between shells and packages 2023-05-01 16:13:31 +02:00
Clément DOUIN
ae6fe9a7c1
fix github actions
Fixed wrong nixpkgs channel, fixed windows zip archive, replaced
.tar.gz by .tgz.
2023-05-01 11:42:22 +02:00
Clément DOUIN
7844a79009
fix release and test 2023-05-01 11:41:39 +02:00
Clément DOUIN
30599931bc
add test zip workflow 2023-05-01 11:21:23 +02:00
Clément DOUIN
a7e9c560c2
release v0.7.3 2023-05-01 01:28:31 +02:00
Clément DOUIN
9453f83c94
fix unit tests 2023-05-01 00:44:59 +02:00
Clément DOUIN
5d00e0bef0
fix windows releases 2023-05-01 00:36:55 +02:00
Clément DOUIN
f66679318d
Merge pull request #447 from soywod/develop
Release v0.7.2
2023-05-01 00:25:23 +02:00
Clément DOUIN
84003f951a
improve cross compilation, prepare v0.7.2 2023-05-01 00:19:59 +02:00
Clément DOUIN
9cf5003697
update changelog 2023-04-25 09:39:49 +02:00
Clément DOUIN
072f488d89
replace himalaya-lib by pimalaya-email 2023-04-20 12:13:27 +02:00
Clément DOUIN
7b3a9e4cc7
improve cargo features naming and organization 2023-03-29 18:09:39 +02:00
Clément DOUIN
69590f6986
added vendored cargo feature 2023-03-11 17:33:57 +01:00
Clément DOUIN
d5efd03bcd
bump himalaya lib 3aa70c4b 2023-02-28 02:16:24 +01:00
Clément DOUIN
501c7f18f5
add flag seen by default for send and save commands 2023-02-23 18:23:15 +01:00
Clément DOUIN
55f5de1803
replace reply all -a by -A 2023-02-22 14:36:31 +01:00
Clément DOUIN
fb324878fa
improve global options, add account config sync-folders-strategy 2023-02-22 13:20:58 +01:00
Clément DOUIN
22fb1b8dee
add completions and man pages to release archives #43 2023-02-21 16:07:16 +01:00
Clément DOUIN
21d8d57f72
add create and delete folder commands #54 2023-02-20 18:26:10 +01:00
Clément DOUIN
5734b30fd1
fix config unit tests 2023-02-20 17:47:40 +01:00
Clément DOUIN
3631ca714b
fix flags case sensitivity 2023-02-20 17:41:26 +01:00
Clément DOUIN
0ab652b4b6
bump himalaya-lib 8c53868 2023-02-20 11:57:41 +01:00
Clément DOUIN
4b3280cbbb
bump himalaya-lib b1a1834d 2023-02-20 10:03:55 +01:00
Clément DOUIN
bfb572acbd
fix config deserialization issues 2023-02-19 08:51:19 +01:00
Clément DOUIN
efd24251e0
bump himalaya-lib version 2023-02-18 10:08:26 +01:00
Clément DOUIN
beba35d57e
use develop branch of himalaya-lib 2023-02-16 16:15:30 +01:00
Clément DOUIN
19d8296324
add Scoop install method 2023-02-16 15:45:10 +01:00
Rashil Gandhi
7fbe39b8fb
Add Scoop install method 2023-02-16 19:44:26 +05:30
Clément DOUIN
3cc1ed7583
release v0.7.1
### Added

- Added command `folders expunge` that deletes all emails marked for
  deletion.
  
### Changed

- Changed the location of the
  [documentation](https://pimalaya.org/himalaya/docs/).

### Fixed

- Fixed broken links in README.md.

### Removed

- Removed the `maildir-backend` cargo feature, it is now included by
  default.
- Removed issues section on GitHub, now issues need to be opened by
  sending an email at
  [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
2023-02-14 17:40:27 +01:00
Clément DOUIN
509b09d533
prepare v0.7.1
Those commits have been stashed then applied due to merge issue:

add ability to sync specific folders f7585eb
add expunge command 1c0b7fb
update readme links to documentation e1c8cf5
fix other doc typos 9c27165
reword title of the project 1eaac7d
reword title of the project bis a7419d6
fix broken links in changelog 26b0311
prepare v0.7.1 2b5e58e
2023-02-14 16:47:02 +01:00
Clément DOUIN
694173b534
release v0.7.0 (#433)
* update codebase with email lib changes (#431)

update himalaya-lib, rename remaining mbox vars

add missing methods from lib

update changelog

* fixed missing folder aliases #430

* improve README links

* fix README repology link

* fix README repology table

* fix README repology table 2

* center README repology table

* fix README cosmetic issues

* fix README cosmetic issues 2

* fix README title

* fix README wiki links

* fix lock file

* prepare v0.6.2

* fix ci

* try some musl builds #356

* add musl build to artifact #356

* add musl build to deployment pipeline #356

* migrate clap v4, add man command #419

* add option to choose color manually #407

* update links and badges

* update matrix badge

* add github release version badge

* update badges links

* fix code bloc type

* fix tests

* fix cargo lock

* generate all man pages for all subcommands #419

* fix query and headers arg parsers

* fix invalid flags and options due to clap v4 migration

* fix tests

* remove -l|--log-level option

* refactor contributing guide

* update lib

* fix flags string printer

* make commands read, attachments, copy, move and delete accept multiple ids

* fix ids arg parser

* fix flags subcommands conflicts between ids and flags

* flip back copy and move arguments

* add issue template (#439)

* update lib, prepare for sync feature

* update himalaya lib, fix senders and config

* update lock file himalaya lib

* fix sync enabling issues

* fix wrong imap backend init in main file

* fix notmuch backend post sync feature

* configuration wizard (#432)

* make DeserializedConfig::path more robust

With this change, himalaya uses the crate `dirs` in order to follow XDG
specifications on Unix, Known Folder on Windows and Standard Directories
on MacOS. This gives us much smoother cross-platform support. It still
has the same fallbacks (`$HOME/.config/himalaya/config.toml` and
`$HOME/.himalayarc`.)

Additionally, this commit removes a bit of in-house code-bloat.

* add wizard entrypoint and basic structure

* wip

* feat: impl Serialize for all DeserializedConfigs

* feat: select default account and write to file

* feat: add SMTP part of wizard

* build: update lockfile

* refactor: separate out multiple files for wizard

* style: friendlier and prettier messages

* feat: add maildir part of wizard

* feat: add notmuch part of wizard

* chore: clippy lints and reorder prompts

* fix: contrived solution to serializing None values

* fix: allow empty Option field when deserializing

* style: address PR review comments

* fix: utilize notmuch lib in finding database path

* fix notmuch wizard

---------

Co-authored-by: Clément DOUIN <clement.douin@posteo.net>

* add account sync progress bar

* improve sync spinner

* make the sync dry run flag show patches without applying them

* update himalaya lib, increase imap session pool size

* add disable cache flag

* add nlnet logo in readme

* update himalaya lib deps, make use of sync reports

* prepare v0.7.0

* bump rustc v1.67.0 and clap v4.1.4

* bump himalaya lib v0.5.1, fix flake lock file

---------

Co-authored-by: janabhumi <dmitriy@ideascup.me>
Co-authored-by: Knut Magnus Aasrud <km@aasrud.com>
2023-02-08 16:03:45 +01:00
Clément DOUIN
bda37ca0ed
Release v0.6.1
### Added

* Added `-s|--sanitize` flag for the `read` command.
  
### Changed

* Changed the behaviour of the `-t|--mime-type` argument of the `read`
  command. It is less strict now: if no part is found for the given
  MIME type, it will fallback to the other one. For example, giving
  `-t html` will show in priority HTML parts, but if none of them are
  found it will show plain parts instead (and vice versa).

* Sanitization is not done by default when using the `read` command,
  the flag `-s|--sanitize` needs to be explicitly provided.

### Fixed

* Fixed empty text bodies when reading html part on plain text email
  [#352].

## himalaya-lib [[v0.4.0](2425269e6d)]

### Added

* Added pipe support for `(imap|smtp)-passwd-cmd` [#373].
* Added `imap-ssl` and `smtp-ssl` options to be able to disable
  encryption [#347].
* Implemented sendmail sender [#351].
* Fixed `process` module for `MINGW*` [#254].

### Changed

* Moved `Email::fold_text_plain_parts` to `Parts::to_readable`. It
  take now a `PartsReaderOptions` as parameter:
  * `plain_first`: shows plain texts first, switch to html if empty.
  * `sanitize`: sanitizes or not text bodies (both plain and html).

### Fixed

* Fixed long subject decoding issue [#380].
* Fixed bad mailbox name encoding from UTF7-IMAP [#370].
2022-10-12 17:00:03 +02:00
Clément DOUIN
d29b227c4b
prepare v0.6.1 2022-10-12 16:47:44 +02:00
Clément DOUIN
6a15b742b0
add sanitize flag for the read command, fix #352 2022-10-12 15:36:36 +02:00
Clément DOUIN
bb8f63e4b0
update code for sendmail sender lib feature 2022-10-12 13:59:20 +02:00
Clément DOUIN
98929d687b
update himalaya-lib for smtp and imap ssl option 2022-10-12 00:23:53 +02:00
Clément DOUIN
29f2bdd931
replace badges by repology big one 2022-10-11 16:43:55 +02:00
Clément DOUIN
9630a6f108
update changelog 2022-10-11 16:37:59 +02:00
Michael Vetter
285d9d0521
add repology badge (#414)
As more distributions start to package himalaya I think this information
might be useful.

Co-authored-by: Clément DOUIN <clement.douin@posteo.net>
2022-10-11 14:50:25 +02:00
Clément DOUIN
cd4575eb5e
Merge branch 'develop' 2022-10-10 21:33:59 +02:00
Clément DOUIN
15e8a0f08f
fix changelog typos 2022-10-10 21:31:38 +02:00
Clément DOUIN
82133b30d9
update lib v0.3.1, use MIT license 2022-10-10 18:14:56 +02:00
Clément DOUIN
8125a55bbe
use himalaya-lib from develop branch instead 2022-10-07 10:24:25 +02:00
Clément DOUIN
4fe5d246f1
fix notmuch backend feature 2022-09-29 00:44:31 +02:00
Clément DOUIN
cdc0e0aa6a
remove notmuch backend from default features 2022-09-29 00:01:21 +02:00
Clément DOUIN
29aa383147
bump himalaya lib v0.2.0 2022-09-28 23:28:05 +02:00
Clément DOUIN
1dcdfa8afa
update nix flake 2022-09-28 22:28:08 +02:00
Clément DOUIN
7777eca667
remove himalaya lib path from cargo config 2022-09-28 22:09:41 +02:00
Clément DOUIN
dda90809cb
fix folder source not taken into consideration 2022-09-28 11:36:14 +02:00
Clément DOUIN
abb9f4172b
fix args typos 2022-09-27 21:42:13 +02:00
Clément DOUIN
329af51534
improve args management 2022-09-27 17:37:08 +02:00
Clément DOUIN
3feccc3225
update readme 2022-09-26 00:08:46 +02:00
Clément DOUIN
44b980c329
fix changelog typos 2022-09-22 18:16:16 +02:00
Clément DOUIN
a3686c1c44
clean config refactor 2022-09-22 16:38:38 +02:00
Clément DOUIN
82b7dfb97f
Merge branch 'development' 2022-07-14 12:24:43 +02:00
Clément DOUIN
672666734b
doc: remove announcement header 2022-07-14 12:23:39 +02:00
Clément DOUIN
ceebf643c4
doc: improve announcement about financial support 2022-07-14 12:22:21 +02:00
Clément DOUIN
7b9cfc4512
Merge branch 'development' 2022-07-14 12:15:19 +02:00
Clément DOUIN
29c731336f
doc: add announcement about financial support 2022-07-14 12:14:37 +02:00
Clément DOUIN
9bcd659af2
clean mbox lib module 2022-06-27 20:55:22 +02:00
Clément DOUIN
1e4dc0cb5a
add missing deserialized config errors 2022-06-27 20:51:12 +02:00
Clément DOUIN
c0e002ea1b
clean process and account modules (#340) 2022-06-27 01:13:55 +02:00
Clément DOUIN
a5c4fdaac6
move backend to lib folder (#340) 2022-06-26 21:47:04 +02:00
TornaxO7
3b2991ae56
bumping lettre to 0.10.0-rc.7 (#391)
* bumping lettre to 0.10.0-rc.7

* executed `cargo build`
2022-06-05 17:51:34 +02:00
Clément DOUIN
3c5379b24d
fix tests 2022-06-05 17:14:57 +02:00
Clément DOUIN
8f667def0c
move envelopes and flags to lib
refactor maildir envelopes/flags

refactor notmuch envelopes
2022-06-05 13:55:40 +02:00
TornaxO7
ca67780341
updating the version of lettre (#389) 2022-06-04 11:17:29 +02:00
Clément DOUIN
e1c92d3f57
make Backend::add_msg return String instead of trait (#340)
This step was necessary to move logic from CLI to lib.
2022-05-29 14:06:54 +02:00
Clément DOUIN
7c01f88006
make Backend::get_mboxes return struct instead of trait (#340)
This step was necessary to move logic from CLI to lib. Indeed, the
trait returned by get_mboxes needed to implement Table, which is
related to the CLI module only.
2022-05-29 12:36:10 +02:00
Clément DOUIN
a0461d84ba
use default rust toolchain components 2022-05-28 20:03:27 +02:00
Clément DOUIN
3f5feed0ff
extract account and config from cli to lib (#340) 2022-05-28 17:07:29 +02:00
Clément DOUIN
0e98def513
msg: add imap flag aliases 2022-05-28 17:02:45 +02:00
Clément DOUIN
cc918e0eee
fix license and readme file path in cargo.toml 2022-05-28 17:02:45 +02:00
TornaxO7
b6643be03f
add rust toolchain.toml (#386)
* modified gitignore

* fixing gitignore

* reomving the himalaya.iml file

* applied cargo fmt

* adding rust-toolchain

* restoring the .gitignore file

* make nix use rust-toolchain.toml

* add back rustfmt and rust-analyzer to buildInputs

I opened an issue to see if its the correct behaviour from the
overlay: https://github.com/oxalica/rust-overlay/issues/88.

* adding clippy to rust-toolchain.toml

Co-authored-by: Clément DOUIN <clement.douin@posteo.net>
2022-05-28 14:36:32 +02:00
TornaxO7
bed5a3856b
improve gitignore files (#385)
* modified gitignore

* fixing gitignore

* reomving the himalaya.iml file

* applied cargo fmt

* fixed typo in .gitignore and removed an entry in it

* adding gitignore to cli/

* reducing .gitignore in cli to one line
2022-05-28 14:10:38 +02:00
Robert Günzler
0696f36f05
vim: msg_id is a uuid string (#383)
change the id parser according to the uuid spec:
https://datatracker.ietf.org/doc/html/rfc4122#section-3

and I get this error when using the vim plugin:

	Error: cannot find maildir message by short hash "0" at "/path/to/my/INBOX"
	Caused by:
	0: cannot find maildir message id from short hash "0"
	1: the short hash "0" matches more than one hash: 030598120934103c456ce08338886728, 06edb10a55efb89de45d8560aee33c8e

Signed-off-by: Robert Günzler <r@gnzler.io>
2022-05-28 09:49:40 +02:00
Dmitriy Pleshevskiy
ba8ef9adf6
fix(config/imap): get first line for password (#374)
* fix(config/imap): get first line for password

Fixes #373

* fix(config/smtp): get first line password

Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-05-24 00:41:29 +02:00
Robert Günzler
5a2d7fa6b5
always reset colors settings on the output stream after writing (#375)
This is according to:
https://docs.rs/termcolor/1.1.2/termcolor/#example-using-standardstream
Not resetting the color settings on the stream will leak the style to
the shell otherwise.
This can be observed when listing mailboxes prior to this patch.

Signed-off-by: Robert Günzler <r@gnzler.io>

Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-05-07 22:54:49 +02:00
Dmitriy Pleshevskiy
4d91a5d74e
table: reset color after cell (#372)
Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-05-07 22:29:09 +02:00
fabrixxm
b7157573f2
default Content-Type to text/plain for not multipart messages (#357)
* Default Content-Type to text/plain for not multipart messages

Parse body of messages without subparts as text/plain if Content-Type
header is not set.

* narrow check for defaulting to `text/plain`

take message body as `text/plain` only if message has only one part
and has no `Content-Type` header

Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-05-07 22:13:08 +02:00
ugla
6d154abcb5
add tpl_args for write subcommand (#361)
Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-04-16 13:50:49 +02:00
João Capucho
0ddcce22e6
check the global config for notify-cmd (#362)
This brings it in line with how notify-query works and how the wiki
defines it to also be a global option.

Co-authored-by: Clément DOUIN <soywod@users.noreply.github.com>
2022-04-15 21:50:05 +02:00
186 changed files with 11005 additions and 11804 deletions

5
.github/FUNDING.yml vendored
View file

@ -1 +1,6 @@
github: soywod
ko_fi: soywod
buy_me_a_coffee: soywod
liberapay: soywod
thanks_dev: soywod
custom: https://www.paypal.com/paypalme/soywod

View file

@ -1,85 +0,0 @@
name: deployment
on:
push:
tags:
- v*
jobs:
create_release:
runs-on: ubuntu-latest
outputs:
upload_url: ${{ steps.create_release.outputs.upload_url }}
steps:
- name: Create release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
draft: false
prerelease: false
deploy_github:
runs-on: ${{ matrix.os }}
needs: create_release
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
os_name: linux
- os: macos-latest
os_name: macos
- os: windows-latest
os_name: windows
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- name: Check project
uses: actions-rs/cargo@v1
with:
command: check
- name: Build release
uses: actions-rs/cargo@v1
with:
command: build
args: --release
- name: Compress executable (unix)
if: matrix.os_name == 'linux' || matrix.os_name == 'macos'
run: tar czf himalaya.tar.gz -C target/release himalaya
- name: Compress executable (windows)
if: matrix.os_name == 'windows'
run: tar czf himalaya.tar.gz -C target/release himalaya.exe
- name: Upload release asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ needs.create_release.outputs.upload_url }}
asset_path: himalaya.tar.gz
asset_name: himalaya-${{ matrix.os_name }}.tar.gz
asset_content_type: application/gzip
deploy_crates:
runs-on: ubuntu-latest
needs: create_release
environment: deployment
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: katyo/publish-crates@v1
with:
registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }}

View file

@ -1,22 +0,0 @@
name: nix-build
on:
pull_request:
push:
branches:
- master
jobs:
nix-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2.3.4
- uses: cachix/install-nix-action@v13
with:
install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install
install_options: '--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve'
extra_nix_config: |
experimental-features = nix-command flakes
- run: nix develop -c rustc --version
- run: nix run . -- --version
- run: nix-build

42
.github/workflows/release-on-demand.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Release on demand
on:
workflow_dispatch:
inputs:
os:
description: Operating system
type: choice
required: true
default: ubuntu-latest
options:
- ubuntu-24.04
- macos-13
- macos-14
target:
description: Architecture
type: choice
required: true
options:
- aarch64-apple-darwin
- aarch64-unknown-linux-musl
- aarch64-unknown-linux-musl
- armv6l-unknown-linux-musleabihf
- armv7l-unknown-linux-musleabihf
- i686-unknown-linux-musl
- x86_64-apple-darwin
- x86_64-unknown-linux-musl
- x86_64-w64-mingw32
features:
description: Cargo features
type: string
required: true
jobs:
release-on-demand:
uses: pimalaya/nix/.github/workflows/release-on-demand.yml@master
secrets: inherit
with:
project: himalaya
os: ${{ inputs.os }}
target: ${{ inputs.target }}
features: ${{ inputs.features }}

15
.github/workflows/releases.yml vendored Normal file
View file

@ -0,0 +1,15 @@
name: Releases
on:
push:
tags:
- v*
branches:
- master
jobs:
release:
uses: pimalaya/nix/.github/workflows/releases.yml@master
secrets: inherit
with:
project: himalaya

View file

@ -1,38 +0,0 @@
name: tests
on:
pull_request:
push:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Install libnotmuch
run: sudo apt-get install -y libnotmuch-dev
- name: Checkout code
uses: actions/checkout@v2
- name: Start GreenMail testing server
run: |
docker run \
--rm \
-d \
-e GREENMAIL_OPTS='-Dgreenmail.setup.test.all -Dgreenmail.hostname=0.0.0.0 -Dgreenmail.auth.disabled -Dgreenmail.verbose' \
-p 3025:3025 \
-p 3110:3110 \
-p 3143:3143 \
-p 3465:3465 \
-p 3993:3993 \
-p 3995:3995 \
greenmail/standalone:1.6.2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Run tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all-features

45
.gitignore vendored
View file

@ -1,10 +1,49 @@
# Cargo config directory
.cargo/
# Cargo build directory
/target
target/
debug/
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# Nix build directory
/result
/result-lib
result
result-*
# Direnv
/.envrc
/.direnv
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
.idea/
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# SonarLint plugin
.idea/sonarlint/
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
## Others
.metadata/

File diff suppressed because it is too large Load diff

View file

@ -1,42 +1,69 @@
# Himalaya contributing guide
# Contributing guide
Thank you for investing your time in contributing to Himalaya!
Thank you for investing your time in contributing to Himalaya CLI!
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
## Development
## New contributor guide
The development environment is managed by [Nix](https://nixos.org/download.html).
Running `nix-shell` will spawn a shell with everything you need to get started with the lib.
To get an overview of the project, read the [README](README.md). To get more information about the project, read the [wiki](https://github.com/soywod/himalaya/wiki).
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
## Getting started
```text
rustup update
```
### Issues
or install manually the following dependencies:
#### Create a new issue
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
If you spot a problem with the docs, [search if an issue already exists](https://github.com/soywod/himalaya/issues). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/soywod/himalaya/issues/new/choose).
## Build
#### Solve an issue
```text
cargo build
```
Scan through our [existing issues](https://github.com/soywod/himalaya/issues) to find one that interests you. You can narrow down the search using `labels` as filters. If you find an issue to work on, you are welcome to open a PR with a fix.
You can disable default [features](https://doc.rust-lang.org/cargo/reference/features.html) with `--no-default-features` and enable features with `--features feat1,feat2,feat3`.
### Make Changes
Finally, you can build a release with `--release`:
#### Make changes in the UI
```text
cargo build --no-default-features --features imap,smtp,keyring --release
```
Click **Make a contribution** at the bottom of any docs page to make small changes such as a typo, sentence fix, or a broken link. This takes you to the `.md` file where you can make your changes and [create a pull request](#pull-request) for a review.
## Override dependencies
#### Make changes locally
If you want to build Himalaya CLI with a dependency installed locally (for example `email-lib`), then you can [override it](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html) by adding the following lines at the end of `Cargo.toml`:
First, follow the instructions on [how to install Himalaya from sources](https://github.com/soywod/himalaya/wiki/Installation:sources). Then, create a working branch and start with your changes!
```toml
[patch.crates-io]
email-lib = { path = "/path/to/email-lib" }
```
### Commit your update
If you get the following error:
Commit the changes once you are happy with them. Commit messages follow the [Angular Convention](https://gist.github.com/stephenparish/9941e89d80e2bc58a153), but contain only a subject. The subject can be prefixed with a custom context like `msg: `, `mbox: `, `imap: ` etc.
```text
note: perhaps two different versions of crate email are being used?
```
> Use imperative, present tense: “change” not “changed” nor
> “changes”<br>Don't capitalize first letter<br>No dot (.) at the end
### Pull Request
then you may need to override more Pimalaya's sub-dependencies:
When you're finished with the changes, create a pull request, also known as a PR.
```toml
[patch.crates-io]
email-lib.path = "/path/to/core/email"
imap-client.path = "/path/to/imap-client"
keyring-lib.path = "/path/to/core/keyring"
mml-lib.path = "/path/to/core/mml"
oauth-lib.path = "/path/to/core/oauth"
pgp-lib.path = "/path/to/core/pgp"
pimalaya-tui.path = "/path/to/tui"
process-lib.path = "/path/to/core/process"
secret-lib.path = "/path/to/core/secret"
```
*See [pimalaya/core#32](https://github.com/pimalaya/core/issues/32) for more information.*
## Commit style
Starting from the `v1.0.0`, Himalaya CLI tries to adopt the [conventional commits specification](https://www.conventionalcommits.org/en/v1.0.0/#summary).

5161
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,2 +1,70 @@
[workspace]
members = ["lib", "cli"]
[package]
name = "himalaya"
description = "CLI to manage emails"
version = "1.1.0"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021"
license = "MIT"
categories = ["command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "maildir", "smtp"]
homepage = "https://pimalaya.org/"
documentation = "https://github.com/pimalaya/himalaya/"
repository = "https://github.com/pimalaya/himalaya/"
[package.metadata.docs.rs]
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"]
imap = ["email-lib/imap", "pimalaya-tui/imap"]
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
[build-dependencies]
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
[dev-dependencies]
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
[dependencies]
ariadne = "0.2"
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6"
email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
once_cell = "1.16"
open = "5.3"
pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] }
secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shellexpand-utils = "=0.2.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
tracing = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
imap-codec.git = "https://github.com/duesee/imap-codec"
email-lib.git = "https://github.com/pimalaya/core"
imap-client.git = "https://github.com/pimalaya/imap-client"
keyring-lib.git = "https://github.com/pimalaya/core"
mml-lib.git = "https://github.com/pimalaya/core"
oauth-lib.git = "https://github.com/pimalaya/core"
pgp-lib.git = "https://github.com/pimalaya/core"
pimalaya-tui.git = "https://github.com/pimalaya/tui"
process-lib.git = "https://github.com/pimalaya/core"
secret-lib.git = "https://github.com/pimalaya/core"

45
LICENSE
View file

@ -1,32 +1,21 @@
Copyright (c) 2020-2021, soywod (Clément DOUIN) <clement.douin@posteo.net>
MIT License
All rights reserved.
Copyright (c) 2022-2024 soywod <clement.douin@posteo.net>
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. All advertising materials mentioning features or use of this software must
display the following acknowledgement:
This product includes software developed by Clément DOUIN.
4. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

776
README.md
View file

@ -1,106 +1,694 @@
# 📫 Himalaya
<div align="center">
<img src="./logo.svg" alt="Logo" width="128" height="128" />
<h1>📫 Himalaya</h1>
<p>CLI to manage emails, based on <a href="https://crates.io/crates/email-lib"><code>email-lib</code></a></p>
<p>
<a href="https://github.com/pimalaya/himalaya/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/pimalaya/himalaya?color=success"/></a>
<a href="https://repology.org/project/himalaya/versions"><img alt="Repology" src="https://img.shields.io/repology/repositories/himalaya?color=success"></a>
<a href="https://matrix.to/#/#pimalaya:matrix.org"><img alt="Matrix" src="https://img.shields.io/matrix/pimalaya:matrix.org?color=success&label=chat"/></a>
</p>
</div>
Command-line interface for email management
*The project is under active development. Do not use in production
before the `v1.0.0`.*
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
## Motivation
Bringing emails to the terminal is a *pain*. First, because they are
sensitive data. Secondly, the existing TUIs
([Mutt](http://www.mutt.org/), [NeoMutt](https://neomutt.org/),
[Alpine](https://alpine.x10host.com/),
[aerc](https://aerc-mail.org/)…) are really hard to configure. They
require time and patience.
The aim of Himalaya is to extract the email logic into a simple (yet
solid) CLI API that can be used directly from the terminal, from
scripts, from UIs… Possibilities are endless!
## Installation
[![homebrew](https://img.shields.io/homebrew/v/himalaya?color=success&style=flat-square)](https://formulae.brew.sh/formula/himalaya)
[![crates](https://img.shields.io/crates/v/himalaya?color=success&style=flat-square)](https://crates.io/crates/himalaya)
```sh
curl -sSL https://raw.githubusercontent.com/soywod/himalaya/master/install.sh | PREFIX=~/.local sh
```
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
```
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Installation:binary)
for other installation methods.*
## Configuration
```toml
# ~/.config/himalaya/config.toml
name = "Your full name"
downloads-dir = "/abs/path/to/downloads"
signature = """
Cordialement,
Regards,
"""
[gmail]
default = true
email = "your.email@gmail.com"
imap-host = "imap.gmail.com"
imap-port = 993
imap-login = "your.email@gmail.com"
imap-passwd-cmd = "pass show gmail"
smtp-host = "smtp.gmail.com"
smtp-port = 465
smtp-login = "your.email@gmail.com"
smtp-passwd-cmd = "security find-internet-password -gs gmail -w"
```
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Configuration:config-file)
for all the options.*
![screenshot](./screenshot.jpeg)
## Features
- Mailbox listing
- Email listing and searching
- Email composition based on `$EDITOR`
- Email manipulation (copy/move/delete)
- Multi-accounting
- Account listing
- IMAP, Maildir and Notmuch support
- IMAP IDLE mode for real-time notifications
- PGP end-to-end encryption
- Vim and Emacs plugins
- Completions for various shells
- JSON output
- …
- Multi-accounting configuration:
- interactive via **wizard** (requires `wizard` feature)
- manual via **TOML**-based configuration file (see [`./config.sample.toml`](./config.sample.toml))
- Message composition based on `$EDITOR`
- **IMAP** backend (requires `imap` feature)
- **Maildir** backend (requires `maildir` feature)
- **Notmuch** backend (requires `notmuch` feature)
- **SMTP** backend (requires `smtp` feature)
- **Sendmail** backend (requires `sendmail` feature)
- Global system **keyring** for secret management (requires `keyring` feature)
- **OAuth 2.0** authorization flow (requires `oauth2` feature)
- **JSON** output via `--output json`
- **PGP** encryption:
- via shell commands (requires `pgp-commands` feature)
- via [GPG](https://www.gnupg.org/) bindings (requires `pgp-gpg` feature)
- via native implementation (requires `pgp-native` feature)
*See the
[wiki](https://github.com/soywod/himalaya/wiki/Usage:msg:list) for all
the features.*
*Himalaya CLI is written in [Rust](https://www.rust-lang.org/), and relies on [cargo features](https://doc.rust-lang.org/cargo/reference/features.html) to enable or disable functionalities. Default features can be found in the `features` section of the [`Cargo.toml`](./Cargo.toml#L18), or on [docs.rs](https://docs.rs/crate/himalaya/latest/features).*
## Installation
<details>
<summary>Pre-built binary</summary>
Himalaya CLI can be installed with the installer:
*As root:*
```
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo sh
```
*As a regular user:*
```
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh
```
These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section.
If you want a more up-to-date version than the latest release, check out the [`releases`](https://github.com/pimalaya/himalaya/actions/workflows/releases.yml) GitHub workflow and look for the *Artifacts* section. You should find a pre-built binary matching your OS. These pre-built binaries are built from the `master` branch.
*Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
</details>
<details>
<summary>Cargo</summary>
Himalaya CLI can be installed with [cargo](https://doc.rust-lang.org/cargo/):
```
cargo install himalaya
```
*With only IMAP support:*
```
cargo install himalaya --no-default-features --features imap
```
You can also use the git repository for a more up-to-date (but less stable) version:
```
cargo install --frozen --force --git https://github.com/pimalaya/himalaya.git
```
</details>
<details>
<summary>Arch Linux</summary>
Himalaya CLI can be installed on [Arch Linux](https://archlinux.org/) with either the community repository:
```
pacman -S himalaya
```
or the [user repository](https://aur.archlinux.org/):
```
git clone https://aur.archlinux.org/himalaya-git.git
cd himalaya-git
makepkg -isc
```
If you use [yay](https://github.com/Jguer/yay), it is even simplier:
```
yay -S himalaya-git
```
</details>
<details>
<summary>Homebrew</summary>
Himalaya CLI can be installed with [Homebrew](https://brew.sh/):
```
brew install himalaya
```
</details>
<details>
<summary>Scoop</summary>
Himalaya CLI can be installed with [Scoop](https://scoop.sh/):
```
scoop install himalaya
```
</details>
<details>
<summary>Fedora Linux/CentOS/RHEL</summary>
Himalaya CLI can be installed on [Fedora Linux](https://fedoraproject.org/)/CentOS/RHEL via [COPR](https://copr.fedorainfracloud.org/coprs/atim/himalaya/) repo:
```
dnf copr enable atim/himalaya
dnf install himalaya
```
</details>
<details>
<summary>Nix</summary>
Himalaya CLI can be installed with [Nix](https://serokell.io/blog/what-is-nix):
```
nix-env -i himalaya
```
You can also use the git repository for a more up-to-date (but less stable) version:
```
nix-env -if https://github.com/pimalaya/himalaya/archive/master.tar.gz
```
*Or, from within the source tree checkout:*
```
nix-env -if .
```
If you have the [Flakes](https://nixos.wiki/wiki/Flakes) feature enabled:
```
nix profile install himalaya
```
*Or, from within the source tree checkout:*
```
nix profile install
```
*You can also run Himalaya directly without installing it:*
```
nix run himalaya
```
</details>
<details>
<summary>Sources</summary>
Himalaya CLI can be installed from sources.
First you need to install the Rust development environment (see the [rust installation documentation](https://doc.rust-lang.org/cargo/getting-started/installation.html)):
```
curl https://sh.rustup.rs -sSf | sh
```
Then, you need to clone the repository and install dependencies:
```
git clone https://github.com/pimalaya/himalaya.git
cd himalaya
cargo check
```
Now, you can build Himalaya:
```
cargo build --release
```
*Binaries are available under the `target/release` folder.*
</details>
## Configuration
Just run `himalaya`, the wizard will help you to configure your default account.
Accounts can be (re)configured via the wizard using the command `himalaya account configure <name>`.
You can also manually edit your own configuration, from scratch:
- Copy the content of the documented [`./config.sample.toml`](./config.sample.toml)
- Paste it in a new file `~/.config/himalaya/config.toml`
- Edit, then comment or uncomment the options you want
<details>
<summary>Proton Mail (Bridge)</summary>
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
- Id order may be reversed or shuffled, but envelopes will still be sorted by date.
- SSL/TLS needs to be deactivated manually.
- The password to use is the one generated by Proton Bridge, not the one from your Proton Mail account.
```toml
[accounts.proton]
email = "example@proton.me"
backend.type = "imap"
backend.host = "127.0.0.1"
backend.port = 1143
backend.encryption.type = "none"
backend.login = "example@proton.me"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "127.0.0.1"
message.send.backend.port = 1025
message.send.backend.encryption.type = "none"
message.send.backend.login = "example@proton.me"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show proton"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "proton-example"
```
Running `himalaya configure -a proton` will ask for your IMAP password, just paste the one generated previously.
</details>
<details>
<summary>Gmail</summary>
Google passwords cannot be used directly. There is two ways to authenticate yourself:
### Using [App Passwords](https://support.google.com/mail/answer/185833)
This option is the simplest and the fastest. First, be sure that:
- IMAP is enabled
- Two-step authentication is enabled
- Less secure app access is enabled
First create a [dedicated password](https://myaccount.google.com/apppasswords) for Himalaya.
```toml
[accounts.gmail]
email = "example@gmail.com"
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/Sent Mail"
folder.aliases.drafts = "[Gmail]/Drafts"
folder.aliases.trash = "[Gmail]/Trash"
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.login = "example@gmail.com"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 465
message.send.backend.login = "example@gmail.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show gmail"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "gmail-example"
```
Running `himalaya configure -a gmail` will ask for your IMAP password, just paste the one generated previously.
### Using OAuth 2.0
This option is the most secure but the hardest to configure. It requires the `oauth2` and `keyring` cargo features.
First, you need to get your OAuth 2.0 credentials by following [this guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.). Once you get your client id and your client secret, you can configure your Himalaya account this way:
```toml
[accounts.gmail]
email = "example@gmail.com"
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/Sent Mail"
folder.aliases.drafts = "[Gmail]/Drafts"
folder.aliases.trash = "[Gmail]/Trash"
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.login = "example@gmail.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "*****"
backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
backend.auth.access-token.keyring = "gmail-oauth2-access-token"
backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
backend.auth.pkce = true
backend.auth.scope = "https://mail.google.com/"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 465
message.send.backend.login = "example@gmail.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "*****"
message.send.backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "gmail-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
message.send.backend.auth.pkce = true
message.send.backend.auth.scope = "https://mail.google.com/"
```
Running `himalaya configure -a gmail` will complete your OAuth 2.0 setup and ask for your client secret.
</details>
<details>
<summary>Outlook</summary>
```toml
[accounts.outlook]
email = "example@outlook.com"
backend.type = "imap"
backend.host = "outlook.office365.com"
backend.port = 993
backend.login = "example@outlook.com"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp-mail.outlook.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "example@outlook.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show outlook"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "outlook-example"
```
Running `himalaya configure -a outlook` will ask for your IMAP password, just paste the one generated previously.
### Using OAuth 2.0
This option is the most secure but the hardest to configure. First, you need to get your OAuth 2.0 credentials by following [this guide](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth). Once you get your client id and your client secret, you can configure your Himalaya account this way:
```toml
[accounts.outlook]
email = "example@outlook.com"
backend.type = "imap"
backend.host = "outlook.office365.com"
backend.port = 993
backend.login = "example@outlook.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "*****"
backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
backend.auth.access-token.keyring = "outlook-oauth2-access-token"
backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
backend.auth.pkce = true
backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.mail.outlook.com"
message.send.backend.port = 587
message.send.backend.starttls = true
message.send.backend.login = "example@outlook.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "*****"
message.send.backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "outlook-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
message.send.backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
message.send.backend.auth.pkce = true
message.send.backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
```
Running `himalaya configure -a outlook` will complete your OAuth 2.0 setup and ask for your client secret.
</details>
<details>
<summary>iCloud Mail</summary>
From the [iCloud Mail](https://support.apple.com/en-us/HT202304) support page:
- IMAP port = `993`.
- IMAP login = name of your iCloud Mail email address (for example, `johnappleseed`, not `johnappleseed@icloud.com`)
- SMTP port = `587` with `STARTTLS`
- SMTP login = full iCloud Mail email address (for example, `johnappleseed@icloud.com`, not `johnappleseed`)
```toml
[accounts.icloud]
email = "johnappleseed@icloud.com"
backend.type = "imap"
backend.host = "imap.mail.me.com"
backend.port = 993
backend.login = "johnappleseed"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.mail.me.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "johnappleseed@icloud.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show icloud"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "icloud-example"
```
Running `himalaya configure -a icloud` will ask for your IMAP password, just paste the one generated previously.
</details>
## Other interfaces
- [pimalaya/himalaya-vim](https://github.com/pimalaya/himalaya-vim), a Vim plugin sitting at the top of Himalaya CLI
- [dantecatalfamo/himalaya-emacs](https://github.com/dantecatalfamo/himalaya-emacs), an Emacs plugin sitting at the top of Himalaya CLI
- [jns/himalaya-raycast](https://www.raycast.com/jns/himalaya), a Raycast extension for Himalaya CLI
- [pimalaya/himalaya-repl](https://github.com/pimalaya/himalaya-repl), an experimental Read-Eval-Print-Loop variant of Himalaya CLI
## FAQ
<details>
<summary>How different is it from aerc, mutt or alpine?</summary>
Aerc, mutt and alpine can be categorized as Terminal User Interfaces (TUI). When the program is executed, your terminal is locked into an event loop and you interact with your emails using keybinds.
Himalaya is also a TUI, but more specifically a Command-Line Interface (CLI). There is no event loop: you interact with your emails using shell commands, in a stateless way.
Additionaly, Himalaya CLI is based on `email-lib`, which is also part of the Pimalaya project. The aim is not just to propose a new terminal interface, but also to expose Rust tools to deal with emails. Anyone who knows Rust language can build his own email interface, without re-inventing the wheel.
</details>
<details>
<summary>How to compose a message?</summary>
An email message is a list of **headers** (`key: val`) followed by a **body**. They form together a template:
```eml
Header: value
Header: value
Header: value
Body
```
***Headers and body must be separated by an empty line.***
### Headers
Here a non-exhaustive list of valid email message template headers:
- `Message-ID`: represents the message identifier (you usually do not need to set up it manually)
- `In-Reply-To`: represents the identifier of the replied message
- `Date`: represents the date of the message
- `Subject`: represents the subject of the message
- `From`: represents the address of the sender
- `To`: represents the addresses of the receivers
- `Reply-To`: represents the address the receiver should reply to instead of the `From` header
- `Cc`: represents the addresses of the other receivers (carbon copy)
- `Bcc`: represents the addresses of the other hidden receivers (blind carbon copy)
An address can be:
- a single email address `user@domain`
- a named address `Name <user@domain>`
- a quoted named address `"Name" <user@domain>`
Multiple address are separated by a coma `,`: `user@domain, Name <user@domain>, "Name" <user@domain>`.
### Plain text body
Email message template body can be written in plain text. The result will be compiled into a single `text/plain` MIME part:
```eml
From: alice@localhost
To: Bob <bob@localhost>
Subject: Hello from Himalaya
Hello, world!
```
### MML body
Email message template body can also be written in MML. The MIME Meta Language was introduced by the Emacs [`mml`](https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/Composing.html) ELisp module. Pimalaya [ported it](https://github.com/pimalaya/core/tree/master/mml) in Rust.
A raw email message is structured according to the [MIME](https://www.rfc-editor.org/rfc/rfc2045) standard. This standard produces verbose, non-friendly messages. Here comes MML: it simplifies the way email message body are structured. Thanks to its simple XML-based syntax, it allows you to easily add multiple parts, attach a binary file, or attach inline image to your body without dealing with the MIME standard.
For instance, this MML template:
```eml
From: alice@localhost
To: bob@localhost
Subject: MML simple
<#multipart type=alternative>
This is a plain text part.
<#part type=text/enriched>
<center>This is a centered enriched part</center>
<#/multipart>
```
compiles into the following MIME Message:
```eml
Subject: MML simple
To: bob@localhost
From: alice@localhost
MIME-Version: 1.0
Date: Tue, 29 Nov 2022 13:07:01 +0000
Content-Type: multipart/alternative;
boundary="4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe"
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
This is a plain text part.
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
Content-Type: text/enriched
Content-Transfer-Encoding: 7bit
<center>This is a centered enriched part</center>
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe--
```
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
</details>
<details>
<summary>How to add attachments to a message?</summary>
*Read first about the FAQ: How to compose a message?*.
```eml
From: alice@localhost
To: bob@localhost
Subject: How to attach stuff
Regular binary attachment:
<#part filename=/path/to/file.pdf><#/part>
Custom file name:
<#part filename=/path/to/file.pdf name=custom.pdf><#/part>
Inline image:
<#part disposition=inline filename=/path/to/image.png><#/part>
```
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
</details>
<details>
<summary>How to debug Himalaya CLI?</summary>
The simplest way is to use `--debug` and `--trace` arguments.
The advanced way is based on environment variables:
- `RUST_LOG=<level>`: determines the log level filter, can be one of `off`, `error`, `warn`, `info`, `debug` and `trace`.
- `RUST_SPANTRACE=1`: enables the spantrace (a span represent periods of time in which a program was executing in a particular context).
- `RUST_BACKTRACE=1`: enables the error backtrace.
- `RUST_BACKTRACE=full`: enables the full error backtrace, which include source lines where the error originated from.
Logs are written to the `stderr`, which means that you can redirect them easily to a file:
```
RUST_LOG=debug himalaya 2>/tmp/himalaya.log
```
</details>
<details>
<summary>How the wizard discovers IMAP/SMTP configs?</summary>
All the lookup mechanisms use the email address domain as base for the lookup. It is heavily inspired from the Thunderbird [Autoconfiguration](https://udn.realityripple.com/docs/Mozilla/Thunderbird/Autoconfiguration) protocol. For example, for the email address `test@example.com`, the lookup is performed as (in this order):
1. check for `autoconfig.example.com`
2. look up of `example.com` in the ISPDB (the Thunderbird central database)
3. look up `MX example.com` in DNS, and for `mx1.mail.hoster.com`, look up `hoster.com` in the ISPDB
4. look up `SRV example.com` in DNS
5. try to guess (`imap.example.com`, `smtp.example.com`…)
</details>
<details>
<summary>How to disable color output?</summary>
Simply set the environment variable NO_COLOR=1
</details>
## Sponsoring
[![github](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod)
[![paypal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod)
[![ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff)](https://ko-fi.com/soywod)
[![buy-me-a-coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000)](https://www.buymeacoffee.com/soywod)
[![liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222)](https://liberapay.com/soywod)
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/)
## Credits
Special thanks to the [NLnet foundation](https://nlnet.nl/) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from various programs:
- [IMAP RFC3501](https://tools.ietf.org/html/rfc3501)
- [Iris](https://github.com/soywod/iris.vim), the himalaya predecessor
- [isync](https://isync.sourceforge.io/), an email synchronizer for
offline usage
- [NeoMutt](https://neomutt.org/), an email terminal user interface
- [Alpine](http://alpine.x10host.com/alpine/alpine-info/), an other
email terminal user interface
- [mutt-wizard](https://github.com/LukeSmithxyz/mutt-wizard), a tool
over NeoMutt and isync
- [rust-imap](https://github.com/jonhoo/rust-imap), a rust IMAP lib
- [NGI Assure](https://nlnet.nl/project/Himalaya/) in 2022
- [NGI Zero Entrust](https://nlnet.nl/project/Pimalaya/) in 2023
- [NGI Zero Core](https://nlnet.nl/project/Pimalaya-PIM/) in 2024 *(still ongoing)*
If you appreciate the project, feel free to donate using one of the following providers:
[![GitHub](https://img.shields.io/badge/-GitHub%20Sponsors-fafbfc?logo=GitHub%20Sponsors)](https://github.com/sponsors/soywod)
[![Ko-fi](https://img.shields.io/badge/-Ko--fi-ff5e5a?logo=Ko-fi&logoColor=ffffff)](https://ko-fi.com/soywod)
[![Buy Me a Coffee](https://img.shields.io/badge/-Buy%20Me%20a%20Coffee-ffdd00?logo=Buy%20Me%20A%20Coffee&logoColor=000000)](https://www.buymeacoffee.com/soywod)
[![Liberapay](https://img.shields.io/badge/-Liberapay-f6c915?logo=Liberapay&logoColor=222222)](https://liberapay.com/soywod)
[![thanks.dev](https://img.shields.io/badge/-thanks.dev-000000?logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQuMDk3IiBoZWlnaHQ9IjE3LjU5NyIgY2xhc3M9InctMzYgbWwtMiBsZzpteC0wIHByaW50Om14LTAgcHJpbnQ6aW52ZXJ0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik05Ljc4MyAxNy41OTdINy4zOThjLTEuMTY4IDAtMi4wOTItLjI5Ny0yLjc3My0uODktLjY4LS41OTMtMS4wMi0xLjQ2Mi0xLjAyLTIuNjA2di0xLjM0NmMwLTEuMDE4LS4yMjctMS43NS0uNjc4LTIuMTk1LS40NTItLjQ0Ni0xLjIzMi0uNjY5LTIuMzQtLjY2OUgwVjcuNzA1aC41ODdjMS4xMDggMCAxLjg4OC0uMjIyIDIuMzQtLjY2OC40NTEtLjQ0Ni42NzctMS4xNzcuNjc3LTIuMTk1VjMuNDk2YzAtMS4xNDQuMzQtMi4wMTMgMS4wMjEtMi42MDZDNS4zMDUuMjk3IDYuMjMgMCA3LjM5OCAwaDIuMzg1djEuOTg3aC0uOTg1Yy0uMzYxIDAtLjY4OC4wMjctLjk4LjA4MmExLjcxOSAxLjcxOSAwIDAgMC0uNzM2LjMwN2MtLjIwNS4xNTYtLjM1OC4zODQtLjQ2LjY4Mi0uMTAzLjI5OC0uMTU0LjY4Mi0uMTU0IDEuMTUxVjUuMjNjMCAuODY3LS4yNDkgMS41ODYtLjc0NSAyLjE1NS0uNDk3LjU2OS0xLjE1OCAxLjAwNC0xLjk4MyAxLjMwNXYuMjE3Yy44MjUuMyAxLjQ4Ni43MzYgMS45ODMgMS4zMDUuNDk2LjU3Ljc0NSAxLjI4Ny43NDUgMi4xNTR2MS4wMjFjMCAuNDcuMDUxLjg1NC4xNTMgMS4xNTIuMTAzLjI5OC4yNTYuNTI1LjQ2MS42ODIuMTkzLjE1Ny40MzcuMjYuNzMyLjMxMi4yOTUuMDUuNjIzLjA3Ni45ODQuMDc2aC45ODVabTE0LjMxNC03LjcwNmgtLjU4OGMtMS4xMDggMC0xLjg4OC4yMjMtMi4zNC42NjktLjQ1LjQ0NS0uNjc3IDEuMTc3LS42NzcgMi4xOTVWMTQuMWMwIDEuMTQ0LS4zNCAyLjAxMy0xLjAyIDIuNjA2LS42OC41OTMtMS42MDUuODktMi43NzQuODloLTIuMzg0di0xLjk4OGguOTg0Yy4zNjIgMCAuNjg4LS4wMjcuOTgtLjA4LjI5Mi0uMDU1LjUzOC0uMTU3LjczNy0uMzA4LjIwNC0uMTU3LjM1OC0uMzg0LjQ2LS42ODIuMTAzLS4yOTguMTU0LS42ODIuMTU0LTEuMTUydi0xLjAyYzAtLjg2OC4yNDgtMS41ODYuNzQ1LTIuMTU1LjQ5Ny0uNTcgMS4xNTgtMS4wMDQgMS45ODMtMS4zMDV2LS4yMTdjLS44MjUtLjMwMS0xLjQ4Ni0uNzM2LTEuOTgzLTEuMzA1LS40OTctLjU3LS43NDUtMS4yODgtLjc0NS0yLjE1NXYtMS4wMmMwLS40Ny0uMDUxLS44NTQtLjE1NC0xLjE1Mi0uMTAyLS4yOTgtLjI1Ni0uNTI2LS40Ni0uNjgyYTEuNzE5IDEuNzE5IDAgMCAwLS43MzctLjMwNyA1LjM5NSA1LjM5NSAwIDAgMC0uOTgtLjA4MmgtLjk4NFYwaDIuMzg0YzEuMTY5IDAgMi4wOTMuMjk3IDIuNzc0Ljg5LjY4LjU5MyAxLjAyIDEuNDYyIDEuMDIgMi42MDZ2MS4zNDZjMCAxLjAxOC4yMjYgMS43NS42NzggMi4xOTUuNDUxLjQ0NiAxLjIzMS42NjggMi4zNC42NjhoLjU4N3oiIGZpbGw9IiNmZmYiLz48L3N2Zz4=)](https://thanks.dev/soywod)
[![PayPal](https://img.shields.io/badge/-PayPal-0079c1?logo=PayPal&logoColor=ffffff)](https://www.paypal.com/paypalme/soywod)

View file

@ -3,7 +3,7 @@ Type=Application
Name=himalaya
DesktopName=Himalaya
GenericName=Mail Reader
Comment=Command-line interface for email management
Comment=CLI to manage emails
Terminal=true
Exec=himalaya %U
Categories=Application;Network
@ -13,4 +13,4 @@ Actions=Compose
[Desktop Action Compose]
Name=Compose
Exec=himalaya write %U
Exec=himalaya message write %U

7
build.rs Normal file
View file

@ -0,0 +1,7 @@
use pimalaya_tui::build::{features_env, git_envs, target_envs};
fn main() {
features_env(include_str!("./Cargo.toml"));
target_envs();
git_envs();
}

View file

@ -1,58 +0,0 @@
[package]
name = "himalaya"
description = "Command-line interface for email management"
version = "0.5.10"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2018"
license-file = "LICENSE"
readme = "README.md"
categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "mail", "email", "client", "imap"]
homepage = "https://github.com/soywod/himalaya/wiki"
documentation = "https://github.com/soywod/himalaya/wiki"
repository = "https://github.com/soywod/himalaya"
[package.metadata.deb]
priority = "optional"
section = "mail"
[features]
imap-backend = ["imap", "imap-proto"]
maildir-backend = ["maildir", "md5"]
notmuch-backend = ["notmuch", "maildir-backend"]
default = ["imap-backend", "maildir-backend"]
[dependencies]
ammonia = "3.1.2"
anyhow = "1.0.44"
atty = "0.2.14"
chrono = "0.4.19"
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
convert_case = "0.5.0"
env_logger = "0.8.3"
erased-serde = "0.3.18"
html-escape = "0.2.9"
lettre = { version = "0.10.0-rc.1", features = ["serde"] }
log = "0.4.14"
mailparse = "0.13.6"
native-tls = "0.2.8"
regex = "1.5.4"
rfc2047-decoder = "0.1.2"
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61"
shellexpand = "2.1.0"
termcolor = "1.1"
terminal_size = "0.1.15"
toml = "0.5.8"
tree_magic = "0.2.3"
unicode-width = "0.1.7"
url = "2.2.2"
uuid = { version = "0.8", features = ["v4"] }
# Optional dependencies:
imap = { version = "=3.0.0-alpha.4", optional = true }
imap-proto = { version = "0.14.3", optional = true }
maildir = { version = "0.6.1", optional = true }
md5 = { version = "0.7.0", optional = true }
notmuch = { version = "0.7.1", optional = true }

View file

@ -1,47 +0,0 @@
//! Backend module.
//!
//! This module exposes the backend trait, which can be used to create
//! custom backend implementations.
use anyhow::Result;
use crate::{
mbox::Mboxes,
msg::{Envelopes, Msg},
};
pub trait Backend<'a> {
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>>;
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
fn get_envelopes(
&mut self,
mbox: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
fn search_envelopes(
&mut self,
mbox: &str,
query: &str,
sort: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>>;
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>>;
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>;
fn del_msg(&mut self, mbox: &str, ids: &str) -> Result<()>;
fn add_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn set_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn del_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>;
fn disconnect(&mut self) -> Result<()> {
Ok(())
}
}

View file

@ -1,126 +0,0 @@
use anyhow::{anyhow, Context, Result};
use std::{
collections::HashMap,
fs::OpenOptions,
io::{BufRead, BufReader, Write},
ops::{Deref, DerefMut},
path::{Path, PathBuf},
};
#[derive(Debug, Default)]
pub struct IdMapper {
path: PathBuf,
map: HashMap<String, String>,
short_hash_len: usize,
}
impl IdMapper {
pub fn new(dir: &Path) -> Result<Self> {
let mut mapper = Self::default();
mapper.path = dir.join(".himalaya-id-map");
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.open(&mapper.path)
.context("cannot open id hash map file")?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line =
line.context("cannot read line from maildir envelopes id mapper cache file")?;
if mapper.short_hash_len == 0 {
mapper.short_hash_len = 2.max(line.parse().unwrap_or(2));
} else {
let (hash, id) = line.split_once(' ').ok_or_else(|| {
anyhow!(
"cannot parse line {:?} from maildir envelopes id mapper cache file",
line
)
})?;
mapper.insert(hash.to_owned(), id.to_owned());
}
}
Ok(mapper)
}
pub fn find(&self, short_hash: &str) -> Result<String> {
let matching_hashes: Vec<_> = self
.keys()
.filter(|hash| hash.starts_with(short_hash))
.collect();
if matching_hashes.len() == 0 {
Err(anyhow!(
"cannot find maildir message id from short hash {:?}",
short_hash,
))
} else if matching_hashes.len() > 1 {
Err(anyhow!(
"the short hash {:?} matches more than one hash: {}",
short_hash,
matching_hashes
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.join(", ")
)
.context(format!(
"cannot find maildir message id from short hash {:?}",
short_hash
)))
} else {
Ok(self.get(matching_hashes[0]).unwrap().to_owned())
}
}
pub fn append(&mut self, lines: Vec<(String, String)>) -> Result<usize> {
self.extend(lines);
let mut entries = String::new();
let mut short_hash_len = self.short_hash_len;
for (hash, id) in self.iter() {
loop {
let short_hash = &hash[0..short_hash_len];
let conflict_found = self
.map
.keys()
.find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash)
.is_some();
if short_hash_len > 32 || !conflict_found {
break;
}
short_hash_len += 1;
}
entries.push_str(&format!("{} {}\n", hash, id));
}
self.short_hash_len = short_hash_len;
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&self.path)
.context("cannot open maildir id hash map cache")?
.write(format!("{}\n{}", short_hash_len, entries).as_bytes())
.context("cannot write maildir id hash map cache")?;
Ok(short_hash_len)
}
}
impl Deref for IdMapper {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl DerefMut for IdMapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}

View file

@ -1,66 +0,0 @@
//! Module related to IMAP CLI.
//!
//! This module provides subcommands and a command matcher related to IMAP.
use anyhow::Result;
use clap::{App, ArgMatches};
use log::{debug, info};
type Keepalive = u64;
/// IMAP commands.
pub enum Command {
/// Start the IMAP notify mode with the give keepalive duration.
Notify(Keepalive),
/// Start the IMAP watch mode with the give keepalive duration.
Watch(Keepalive),
}
/// IMAP command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Command>> {
info!("entering imap command matcher");
if let Some(m) = m.subcommand_matches("notify") {
info!("notify command matched");
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Notify(keepalive)));
}
if let Some(m) = m.subcommand_matches("watch") {
info!("watch command matched");
let keepalive = clap::value_t_or_exit!(m.value_of("keepalive"), u64);
debug!("keepalive: {}", keepalive);
return Ok(Some(Command::Watch(keepalive)));
}
Ok(None)
}
/// IMAP subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![
clap::SubCommand::with_name("notify")
.about("Notifies when new messages arrive in the given mailbox")
.aliases(&["idle"])
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
clap::SubCommand::with_name("watch")
.about("Watches IMAP server changes")
.arg(
clap::Arg::with_name("keepalive")
.help("Specifies the keepalive duration")
.short("k")
.long("keepalive")
.value_name("SECS")
.default_value("500"),
),
]
}

View file

@ -1,415 +0,0 @@
//! IMAP backend module.
//!
//! This module contains the definition of the IMAP backend.
use anyhow::{anyhow, Context, Result};
use log::{debug, log_enabled, trace, Level};
use native_tls::{TlsConnector, TlsStream};
use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
net::TcpStream,
thread,
};
use crate::{
backends::{
imap::msg_sort_criterion::SortCriteria, Backend, ImapEnvelope, ImapEnvelopes, ImapMboxes,
},
config::{AccountConfig, ImapBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::run_cmd,
};
use super::ImapFlags;
type ImapSess = imap::Session<TlsStream<TcpStream>>;
pub struct ImapBackend<'a> {
account_config: &'a AccountConfig,
imap_config: &'a ImapBackendConfig,
sess: Option<ImapSess>,
}
impl<'a> ImapBackend<'a> {
pub fn new(account_config: &'a AccountConfig, imap_config: &'a ImapBackendConfig) -> Self {
Self {
account_config,
imap_config,
sess: None,
}
}
fn sess(&mut self) -> Result<&mut ImapSess> {
if self.sess.is_none() {
debug!("create TLS builder");
debug!("insecure: {}", self.imap_config.imap_insecure);
let builder = TlsConnector::builder()
.danger_accept_invalid_certs(self.imap_config.imap_insecure)
.danger_accept_invalid_hostnames(self.imap_config.imap_insecure)
.build()
.context("cannot create TLS connector")?;
debug!("create client");
debug!("host: {}", self.imap_config.imap_host);
debug!("port: {}", self.imap_config.imap_port);
debug!("starttls: {}", self.imap_config.imap_starttls);
let mut client_builder =
imap::ClientBuilder::new(&self.imap_config.imap_host, self.imap_config.imap_port);
if self.imap_config.imap_starttls {
client_builder.starttls();
}
let client = client_builder
.connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?))
.context("cannot connect to IMAP server")?;
debug!("create session");
debug!("login: {}", self.imap_config.imap_login);
debug!("passwd cmd: {}", self.imap_config.imap_passwd_cmd);
let mut sess = client
.login(
&self.imap_config.imap_login,
&self.imap_config.imap_passwd()?,
)
.map_err(|res| res.0)
.context("cannot login to IMAP server")?;
sess.debug = log_enabled!(Level::Trace);
self.sess = Some(sess);
}
match self.sess {
Some(ref mut sess) => Ok(sess),
None => Err(anyhow!("cannot get IMAP session")),
}
}
fn search_new_msgs(&mut self, query: &str) -> Result<Vec<u32>> {
let uids: Vec<u32> = self
.sess()?
.uid_search(query)
.context("cannot search new messages")?
.into_iter()
.collect();
debug!("found {} new messages", uids.len());
trace!("uids: {:?}", uids);
Ok(uids)
}
pub fn notify(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("notify");
debug!("examine mailbox {:?}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox {}", mbox))?;
debug!("init messages hashset");
let mut msgs_set: HashSet<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.iter()
.cloned()
.collect::<HashSet<_>>();
trace!("messages hashset: {:?}", msgs_set);
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let uids: Vec<u32> = self
.search_new_msgs(&self.account_config.notify_query)?
.into_iter()
.filter(|uid| -> bool { msgs_set.get(uid).is_none() })
.collect();
debug!("found {} new messages not in hashset", uids.len());
trace!("messages hashet: {:?}", msgs_set);
if !uids.is_empty() {
let uids = uids
.iter()
.map(|uid| uid.to_string())
.collect::<Vec<_>>()
.join(",");
let fetches = self
.sess()?
.uid_fetch(uids, "(UID ENVELOPE)")
.context("cannot fetch new messages enveloppe")?;
for fetch in fetches.iter() {
let msg = ImapEnvelope::try_from(fetch)?;
let uid = fetch.uid.ok_or_else(|| {
anyhow!("cannot retrieve message {}'s UID", fetch.message)
})?;
let from = msg.sender.to_owned().into();
self.account_config.run_notify_cmd(&msg.subject, &from)?;
debug!("notify message: {}", uid);
trace!("message: {:?}", msg);
debug!("insert message {} in hashset", uid);
msgs_set.insert(uid);
trace!("messages hashset: {:?}", msgs_set);
}
}
debug!("end loop");
}
}
pub fn watch(&mut self, keepalive: u64, mbox: &str) -> Result<()> {
debug!("examine mailbox: {}", mbox);
self.sess()?
.examine(mbox)
.context(format!("cannot examine mailbox `{}`", mbox))?;
loop {
debug!("begin loop");
self.sess()?
.idle()
.and_then(|mut idle| {
idle.set_keepalive(std::time::Duration::new(keepalive, 0));
idle.wait_keepalive_while(|res| {
// TODO: handle response
trace!("idle response: {:?}", res);
false
})
})
.context("cannot start the idle mode")?;
let cmds = self.account_config.watch_cmds.clone();
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);
})
});
debug!("end loop");
}
}
}
impl<'a> Backend<'a> for ImapBackend<'a> {
fn add_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.create(mbox)
.context(format!("cannot create imap mailbox {:?}", mbox))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
let mboxes: ImapMboxes = self
.sess()?
.list(Some(""), Some("*"))
.context("cannot list mailboxes")?
.into();
Ok(Box::new(mboxes))
}
fn del_mbox(&mut self, mbox: &str) -> Result<()> {
self.sess()?
.delete(mbox)
.context(format!("cannot delete imap mailbox {:?}", mbox))
}
fn get_envelopes(
&mut self,
mbox: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists as usize;
debug!("last sequence number: {:?}", last_seq);
if last_seq == 0 {
return Ok(Box::new(ImapEnvelopes::default()));
}
let range = if page_size > 0 {
let cursor = page * page_size;
let begin = 1.max(last_seq - cursor);
let end = begin - begin.min(page_size) + 1;
format!("{}:{}", end, begin)
} else {
String::from("1:*")
};
debug!("range: {:?}", range);
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!("cannot fetch messages within range {:?}", range))?;
let envelopes: ImapEnvelopes = fetches.try_into()?;
Ok(Box::new(envelopes))
}
fn search_envelopes(
&mut self,
mbox: &str,
query: &str,
sort: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
debug!("last sequence number: {:?}", last_seq);
if last_seq == 0 {
return Ok(Box::new(ImapEnvelopes::default()));
}
let begin = page * page_size;
let end = begin + (page_size - 1);
let seqs: Vec<String> = if sort.is_empty() {
self.sess()?
.search(query)
.context(format!(
"cannot find envelopes in {:?} with query {:?}",
mbox, query
))?
.iter()
.map(|seq| seq.to_string())
.collect()
} else {
let sort: SortCriteria = sort.try_into()?;
let charset = imap::extensions::sort::SortCharset::Utf8;
self.sess()?
.sort(&sort, charset, query)
.context(format!(
"cannot find envelopes in {:?} with query {:?}",
mbox, query
))?
.iter()
.map(|seq| seq.to_string())
.collect()
};
if seqs.is_empty() {
return Ok(Box::new(ImapEnvelopes::default()));
}
let range = seqs[begin..end.min(seqs.len())].join(",");
let fetches = self
.sess()?
.fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)")
.context(format!("cannot fetch messages within range {:?}", range))?;
let envelopes: ImapEnvelopes = fetches.try_into()?;
Ok(Box::new(envelopes))
}
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
let flags: ImapFlags = flags.into();
self.sess()?
.append(mbox, msg)
.flags(<ImapFlags as Into<Vec<imap::types::Flag<'a>>>>::into(flags))
.finish()
.context(format!("cannot append message to {:?}", mbox))?;
let last_seq = self
.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?
.exists;
Ok(Box::new(last_seq))
}
fn get_msg(&mut self, mbox: &str, seq: &str) -> Result<Msg> {
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
let fetches = self
.sess()?
.fetch(seq, "(FLAGS INTERNALDATE BODY[])")
.context(format!("cannot fetch messages {:?}", seq))?;
let fetch = fetches
.first()
.ok_or_else(|| anyhow!("cannot find message {:?}", seq))?;
let msg_raw = fetch.body().unwrap_or_default().to_owned();
let mut msg = Msg::from_parsed_mail(
mailparse::parse_mail(&msg_raw).context("cannot parse message")?,
self.account_config,
)?;
msg.raw = msg_raw;
Ok(msg)
}
fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(&mbox_src, seq)?.raw;
println!("raw: {:?}", String::from_utf8(msg.to_vec()).unwrap());
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> {
let msg = self.get_msg(mbox_src, seq)?.raw;
self.add_flags(mbox_src, seq, "seen deleted")?;
self.add_msg(&mbox_dst, &msg, "seen")?;
Ok(())
}
fn del_msg(&mut self, mbox: &str, seq: &str) -> Result<()> {
self.add_flags(mbox, seq, "deleted")
}
fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("+FLAGS ({})", flags))
.context(format!("cannot add flags {:?}", &flags))?;
self.sess()?
.expunge()
.context(format!("cannot expunge mailbox {:?}", mbox))?;
Ok(())
}
fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("FLAGS ({})", flags))
.context(format!("cannot set flags {:?}", &flags))?;
Ok(())
}
fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> {
let flags: ImapFlags = flags.into();
self.sess()?
.select(mbox)
.context(format!("cannot select mailbox {:?}", mbox))?;
self.sess()?
.store(seq_range, format!("-FLAGS ({})", flags))
.context(format!("cannot remove flags {:?}", &flags))?;
Ok(())
}
fn disconnect(&mut self) -> Result<()> {
if let Some(ref mut sess) = self.sess {
debug!("logout from IMAP server");
sess.logout().context("cannot logout from IMAP server")?;
}
Ok(())
}
}

View file

@ -1,187 +0,0 @@
//! IMAP envelope module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the envelope.
use anyhow::{anyhow, Context, Error, Result};
use std::{convert::TryFrom, ops::Deref};
use crate::{
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use super::{ImapFlag, ImapFlags};
/// Represents a list of IMAP envelopes.
#[derive(Debug, Default, serde::Serialize)]
pub struct ImapEnvelopes {
#[serde(rename = "response")]
pub envelopes: Vec<ImapEnvelope>,
}
impl Deref for ImapEnvelopes {
type Target = Vec<ImapEnvelope>;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}
impl PrintTable for ImapEnvelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
// impl Envelopes for ImapEnvelopes {
// //
// }
/// Represents the IMAP envelope. The envelope is just a message
/// subset, and is mostly used for listings.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct ImapEnvelope {
/// Represents the sequence number of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
pub id: u32,
/// Represents the flags attached to the message.
pub flags: ImapFlags,
/// Represents the subject of the message.
pub subject: String,
/// Represents the first sender of the message.
pub sender: String,
/// Represents the internal date of the message.
///
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
pub date: Option<String>,
}
impl Table for ImapEnvelope {
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(&ImapFlag::Seen);
let subject = &self.subject;
let sender = &self.sender;
let date = self.date.as_deref().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())
}
}
/// Represents a list of raw envelopes returned by the `imap` crate.
pub type RawImapEnvelopes = imap::types::ZeroCopy<Vec<RawImapEnvelope>>;
impl TryFrom<RawImapEnvelopes> for ImapEnvelopes {
type Error = Error;
fn try_from(raw_envelopes: RawImapEnvelopes) -> Result<Self, Self::Error> {
let mut envelopes = vec![];
for raw_envelope in raw_envelopes.iter().rev() {
envelopes.push(ImapEnvelope::try_from(raw_envelope).context("cannot parse envelope")?);
}
Ok(Self { envelopes })
}
}
/// Represents the raw envelope returned by the `imap` crate.
pub type RawImapEnvelope = imap::types::Fetch;
impl TryFrom<&RawImapEnvelope> for ImapEnvelope {
type Error = Error;
fn try_from(fetch: &RawImapEnvelope) -> Result<ImapEnvelope> {
let envelope = fetch
.envelope()
.ok_or_else(|| anyhow!("cannot get envelope of message {}", fetch.message))?;
// Get the sequence number
let id = fetch.message;
// Get the flags
let flags = ImapFlags::try_from(fetch.flags())?;
// Get the subject
let subject = envelope
.subject
.as_ref()
.map(|subj| {
rfc2047_decoder::decode(subj).context(format!(
"cannot decode subject of message {}",
fetch.message
))
})
.unwrap_or_else(|| Ok(String::default()))?;
// 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_else(|| 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_else(|| 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_else(|| 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,
})
}
}

View file

@ -1,151 +0,0 @@
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
fmt,
ops::Deref,
};
/// Represents the imap flag variants.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum ImapFlag {
Seen,
Answered,
Flagged,
Deleted,
Draft,
Recent,
MayCreate,
Custom(String),
}
impl From<&str> for ImapFlag {
fn from(flag_str: &str) -> Self {
match flag_str {
"seen" => ImapFlag::Seen,
"answered" => ImapFlag::Answered,
"flagged" => ImapFlag::Flagged,
"deleted" => ImapFlag::Deleted,
"draft" => ImapFlag::Draft,
"recent" => ImapFlag::Recent,
"maycreate" | "may-create" => ImapFlag::MayCreate,
flag_str => ImapFlag::Custom(flag_str.into()),
}
}
}
impl TryFrom<&imap::types::Flag<'_>> for ImapFlag {
type Error = Error;
fn try_from(flag: &imap::types::Flag<'_>) -> Result<Self, Self::Error> {
Ok(match flag {
imap::types::Flag::Seen => ImapFlag::Seen,
imap::types::Flag::Answered => ImapFlag::Answered,
imap::types::Flag::Flagged => ImapFlag::Flagged,
imap::types::Flag::Deleted => ImapFlag::Deleted,
imap::types::Flag::Draft => ImapFlag::Draft,
imap::types::Flag::Recent => ImapFlag::Recent,
imap::types::Flag::MayCreate => ImapFlag::MayCreate,
imap::types::Flag::Custom(custom) => ImapFlag::Custom(custom.to_string()),
_ => return Err(anyhow!("cannot parse imap flag")),
})
}
}
/// Represents the imap flags.
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
pub struct ImapFlags(pub Vec<ImapFlag>);
impl ImapFlags {
/// Builds a symbols string
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&ImapFlag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&ImapFlag::Answered) {
""
} else {
" "
});
flags.push_str(if self.contains(&ImapFlag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Deref for ImapFlags {
type Target = Vec<ImapFlag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for ImapFlags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut glue = "";
for flag in &self.0 {
write!(f, "{}", glue)?;
match flag {
ImapFlag::Seen => write!(f, "\\Seen")?,
ImapFlag::Answered => write!(f, "\\Answered")?,
ImapFlag::Flagged => write!(f, "\\Flagged")?,
ImapFlag::Deleted => write!(f, "\\Deleted")?,
ImapFlag::Draft => write!(f, "\\Draft")?,
ImapFlag::Recent => write!(f, "\\Recent")?,
ImapFlag::MayCreate => write!(f, "\\MayCreate")?,
ImapFlag::Custom(custom) => write!(f, "{}", custom)?,
}
glue = " ";
}
Ok(())
}
}
impl<'a> Into<Vec<imap::types::Flag<'a>>> for ImapFlags {
fn into(self) -> Vec<imap::types::Flag<'a>> {
self.0
.into_iter()
.map(|flag| match flag {
ImapFlag::Seen => imap::types::Flag::Seen,
ImapFlag::Answered => imap::types::Flag::Answered,
ImapFlag::Flagged => imap::types::Flag::Flagged,
ImapFlag::Deleted => imap::types::Flag::Deleted,
ImapFlag::Draft => imap::types::Flag::Draft,
ImapFlag::Recent => imap::types::Flag::Recent,
ImapFlag::MayCreate => imap::types::Flag::MayCreate,
ImapFlag::Custom(custom) => imap::types::Flag::Custom(custom.into()),
})
.collect()
}
}
impl From<&str> for ImapFlags {
fn from(flags_str: &str) -> Self {
ImapFlags(
flags_str
.split_whitespace()
.map(|flag_str| flag_str.trim().into())
.collect(),
)
}
}
impl TryFrom<&[imap::types::Flag<'_>]> for ImapFlags {
type Error = Error;
fn try_from(flags: &[imap::types::Flag<'_>]) -> Result<Self, Self::Error> {
let mut f = vec![];
for flag in flags {
f.push(flag.try_into()?);
}
Ok(Self(f))
}
}

View file

@ -1,15 +0,0 @@
//! Module related to IMAP handling.
//!
//! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::Result;
use crate::backends::ImapBackend;
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.notify(keepalive, mbox)
}
pub fn watch(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
imap.watch(keepalive, mbox)
}

View file

@ -1,154 +0,0 @@
//! IMAP mailbox module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox.
use anyhow::Result;
use serde::Serialize;
use std::fmt::{self, Display};
use std::ops::Deref;
use crate::mbox::Mboxes;
use crate::{
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
use super::ImapMboxAttrs;
/// Represents a list of IMAP mailboxes.
#[derive(Debug, Default, Serialize)]
pub struct ImapMboxes {
#[serde(rename = "response")]
pub mboxes: Vec<ImapMbox>,
}
impl Deref for ImapMboxes {
type Target = Vec<ImapMbox>;
fn deref(&self) -> &Self::Target {
&self.mboxes
}
}
impl PrintTable for ImapMboxes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
impl Mboxes for ImapMboxes {
//
}
/// Represents the IMAP mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMbox {
/// Represents the mailbox hierarchie delimiter.
pub delim: String,
/// Represents the mailbox name.
pub name: String,
/// Represents the mailbox attributes.
pub attrs: ImapMboxAttrs,
}
impl ImapMbox {
pub fn new(name: &str) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
}
impl Display for ImapMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for ImapMbox {
fn head() -> Row {
Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(
Cell::new("ATTRIBUTES")
.shrinkable()
.bold()
.underline()
.white(),
)
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.delim).white())
.cell(Cell::new(&self.name).green())
.cell(Cell::new(&self.attrs.to_string()).shrinkable().blue())
}
}
#[cfg(test)]
mod tests {
use crate::backends::ImapMboxAttr;
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(ImapMbox::default(), ImapMbox::new(""));
assert_eq!(
ImapMbox {
name: "INBOX".into(),
..ImapMbox::default()
},
ImapMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = ImapMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = ImapMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = ImapMbox {
delim: ".".into(),
name: "Sent".into(),
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `imap` crate.
pub type RawImapMboxes = imap::types::ZeroCopy<Vec<RawImapMbox>>;
impl<'a> From<RawImapMboxes> for ImapMboxes {
fn from(raw_mboxes: RawImapMboxes) -> Self {
Self {
mboxes: raw_mboxes.iter().map(ImapMbox::from).collect(),
}
}
}
/// Represents the raw mailbox returned by the `imap` crate.
pub type RawImapMbox = imap::types::Name;
impl<'a> From<&'a RawImapMbox> for ImapMbox {
fn from(raw_mbox: &'a RawImapMbox) -> Self {
Self {
delim: raw_mbox.delimiter().unwrap_or_default().into(),
name: raw_mbox.name().into(),
attrs: raw_mbox.attributes().into(),
}
}
}

View file

@ -1,119 +0,0 @@
//! IMAP mailbox attribute module.
//!
//! This module provides IMAP types and conversion utilities related
//! to the mailbox attribute.
/// Represents the raw mailbox attribute returned by the `imap` crate.
pub use imap::types::NameAttribute as RawImapMboxAttr;
use std::{
fmt::{self, Display},
ops::Deref,
};
/// Represents the attributes of the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct ImapMboxAttrs(pub Vec<ImapMboxAttr>);
impl Deref for ImapMboxAttrs {
type Target = Vec<ImapMboxAttr>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for ImapMboxAttrs {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut glue = "";
for attr in self.iter() {
write!(f, "{}{}", glue, attr)?;
glue = ", ";
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize)]
pub enum ImapMboxAttr {
NoInferiors,
NoSelect,
Marked,
Unmarked,
Custom(String),
}
/// Makes the attribute displayable.
impl Display for ImapMboxAttr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ImapMboxAttr::NoInferiors => write!(f, "NoInferiors"),
ImapMboxAttr::NoSelect => write!(f, "NoSelect"),
ImapMboxAttr::Marked => write!(f, "Marked"),
ImapMboxAttr::Unmarked => write!(f, "Unmarked"),
ImapMboxAttr::Custom(custom) => write!(f, "{}", custom),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_display_attrs() {
macro_rules! attrs_from {
($($attr:expr),*) => {
ImapMboxAttrs(vec![$($attr,)*]).to_string()
};
}
let empty_attr = attrs_from![];
let single_attr = attrs_from![ImapMboxAttr::NoInferiors];
let multiple_attrs = attrs_from![
ImapMboxAttr::Custom("AttrCustom".into()),
ImapMboxAttr::NoInferiors
];
assert_eq!("", empty_attr);
assert_eq!("NoInferiors", single_attr);
assert!(multiple_attrs.contains("NoInferiors"));
assert!(multiple_attrs.contains("AttrCustom"));
assert!(multiple_attrs.contains(","));
}
#[test]
fn it_should_display_attr() {
macro_rules! attr_from {
($attr:ident) => {
ImapMboxAttr::$attr.to_string()
};
($custom:literal) => {
ImapMboxAttr::Custom($custom.into()).to_string()
};
}
assert_eq!("NoInferiors", attr_from![NoInferiors]);
assert_eq!("NoSelect", attr_from![NoSelect]);
assert_eq!("Marked", attr_from![Marked]);
assert_eq!("Unmarked", attr_from![Unmarked]);
assert_eq!("CustomAttr", attr_from!["CustomAttr"]);
}
}
impl<'a> From<&'a [RawImapMboxAttr<'a>]> for ImapMboxAttrs {
fn from(raw_attrs: &'a [RawImapMboxAttr<'a>]) -> Self {
Self(raw_attrs.iter().map(ImapMboxAttr::from).collect())
}
}
impl<'a> From<&'a RawImapMboxAttr<'a>> for ImapMboxAttr {
fn from(attr: &'a RawImapMboxAttr<'a>) -> Self {
match attr {
RawImapMboxAttr::NoInferiors => Self::NoInferiors,
RawImapMboxAttr::NoSelect => Self::NoSelect,
RawImapMboxAttr::Marked => Self::Marked,
RawImapMboxAttr::Unmarked => Self::Unmarked,
RawImapMboxAttr::Custom(cow) => Self::Custom(cow.to_string()),
}
}
}

View file

@ -1,61 +0,0 @@
//! Message sort criteria module.
//!
//! This module regroups everything related to deserialization of
//! message sort criteria.
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, ops::Deref};
/// Represents the message sort criteria. It is just a wrapper around
/// the `imap::extensions::sort::SortCriterion`.
pub struct SortCriteria<'a>(Vec<imap::extensions::sort::SortCriterion<'a>>);
impl<'a> Deref for SortCriteria<'a> {
type Target = Vec<imap::extensions::sort::SortCriterion<'a>>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> TryFrom<&'a str> for SortCriteria<'a> {
type Error = Error;
fn try_from(criteria_str: &'a str) -> Result<Self, Self::Error> {
let mut criteria = vec![];
for criterion_str in criteria_str.split(" ") {
criteria.push(match criterion_str.trim() {
"arrival:asc" | "arrival" => Ok(imap::extensions::sort::SortCriterion::Arrival),
"arrival:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Arrival,
)),
"cc:asc" | "cc" => Ok(imap::extensions::sort::SortCriterion::Cc),
"cc:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Cc,
)),
"date:asc" | "date" => Ok(imap::extensions::sort::SortCriterion::Date),
"date:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Date,
)),
"from:asc" | "from" => Ok(imap::extensions::sort::SortCriterion::From),
"from:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::From,
)),
"size:asc" | "size" => Ok(imap::extensions::sort::SortCriterion::Size),
"size:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Size,
)),
"subject:asc" | "subject" => Ok(imap::extensions::sort::SortCriterion::Subject),
"subject:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::Subject,
)),
"to:asc" | "to" => Ok(imap::extensions::sort::SortCriterion::To),
"to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse(
&imap::extensions::sort::SortCriterion::To,
)),
_ => Err(anyhow!("cannot parse sort criterion {:?}", criterion_str)),
}?);
}
Ok(Self(criteria))
}
}

View file

@ -1,493 +0,0 @@
//! Maildir backend module.
//!
//! This module contains the definition of the maildir backend and its
//! traits implementation.
use anyhow::{anyhow, Context, Result};
use log::{debug, info, trace};
use std::{convert::TryInto, env, fs, path::PathBuf};
use crate::{
backends::{Backend, IdMapper, MaildirEnvelopes, MaildirFlags, MaildirMboxes},
config::{AccountConfig, MaildirBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
/// Represents the maildir backend.
pub struct MaildirBackend<'a> {
account_config: &'a AccountConfig,
mdir: maildir::Maildir,
}
impl<'a> MaildirBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
maildir_config: &'a MaildirBackendConfig,
) -> Self {
Self {
account_config,
mdir: maildir_config.maildir_dir.clone().into(),
}
}
fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result<PathBuf> {
if mdir_path.is_dir() {
Ok(mdir_path)
} else {
Err(anyhow!("cannot read maildir directory {:?}", mdir_path))
}
}
/// Creates a maildir instance from a string slice.
pub fn get_mdir_from_dir(&self, dir: &str) -> Result<maildir::Maildir> {
let dir = self.account_config.get_mbox_alias(dir)?;
// If the dir points to the inbox folder, creates a maildir
// instance from the root folder.
if &dir == "inbox" {
return self
.validate_mdir_path(self.mdir.path().to_owned())
.map(maildir::Maildir::from);
}
// If the dir is a valid maildir path, creates a maildir
// instance from it. First checks for absolute path,
self.validate_mdir_path((&dir).into())
// then for relative path to `maildir-dir`,
.or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir)))
// and finally for relative path to the current directory.
.or_else(|_| self.validate_mdir_path(env::current_dir()?.join(&dir)))
.or_else(|_| {
// Otherwise creates a maildir instance from a maildir
// subdirectory by adding a "." in front of the name
// as described in the [spec].
//
// [spec]: http://www.courier-mta.org/imap/README.maildirquota.html
self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir)))
})
.map(maildir::Maildir::from)
}
}
impl<'a> Backend<'a> for MaildirBackend<'a> {
fn add_mbox(&mut self, subdir: &str) -> Result<()> {
info!(">> add maildir subdir");
debug!("subdir: {:?}", subdir);
let path = self.mdir.path().join(format!(".{}", subdir));
trace!("subdir path: {:?}", path);
fs::create_dir(&path)
.with_context(|| format!("cannot create maildir subdir {:?} at {:?}", subdir, path))?;
info!("<< add maildir subdir");
Ok(())
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
info!(">> get maildir dirs");
let dirs: MaildirMboxes =
self.mdir.list_subdirs().try_into().with_context(|| {
format!("cannot parse maildir dirs from {:?}", self.mdir.path())
})?;
trace!("dirs: {:?}", dirs);
info!("<< get maildir dirs");
Ok(Box::new(dirs))
}
fn del_mbox(&mut self, dir: &str) -> Result<()> {
info!(">> delete maildir dir");
debug!("dir: {:?}", dir);
let path = self.mdir.path().join(format!(".{}", dir));
trace!("dir path: {:?}", path);
fs::remove_dir_all(&path)
.with_context(|| format!("cannot delete maildir {:?} from {:?}", dir, path))?;
info!("<< delete maildir dir");
Ok(())
}
fn get_envelopes(
&mut self,
dir: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
info!(">> get maildir envelopes");
debug!("dir: {:?}", dir);
debug!("page size: {:?}", page_size);
debug!("page: {:?}", page);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
// Reads envelopes from the "cur" folder of the selected
// maildir.
let mut envelopes: MaildirEnvelopes = mdir.list_cur().try_into().with_context(|| {
format!("cannot parse maildir envelopes from {:?}", self.mdir.path())
})?;
debug!("envelopes len: {:?}", envelopes.len());
trace!("envelopes: {:?}", envelopes);
// Calculates pagination boundaries.
let page_begin = page * page_size;
debug!("page begin: {:?}", page_begin);
if page_begin > envelopes.len() {
return Err(anyhow!(
"cannot get maildir envelopes at page {:?} (out of bounds)",
page_begin + 1,
));
}
let page_end = envelopes.len().min(page_begin + page_size);
debug!("page end: {:?}", page_end);
// Sorts envelopes by most recent date.
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
// Applies pagination boundaries.
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
// Appends envelopes hash to the id mapper cache file and
// calculates the new short hash length. The short hash length
// represents the minimum hash length possible to avoid
// conflicts.
let short_hash_len = {
let mut mapper = IdMapper::new(mdir.path())?;
let entries = envelopes
.iter()
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
.collect();
mapper.append(entries)?
};
debug!("short hash length: {:?}", short_hash_len);
// Shorten envelopes hash.
envelopes
.iter_mut()
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
info!("<< get maildir envelopes");
Ok(Box::new(envelopes))
}
fn search_envelopes(
&mut self,
_dir: &str,
_query: &str,
_sort: &str,
_page_size: usize,
_page: usize,
) -> Result<Box<dyn Envelopes>> {
info!(">> search maildir envelopes");
info!("<< search maildir envelopes");
Err(anyhow!(
"cannot find maildir envelopes: feature not implemented"
))
}
fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result<Box<dyn ToString>> {
info!(">> add maildir message");
debug!("dir: {:?}", dir);
debug!("flags: {:?}", flags);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let flags: MaildirFlags = flags
.try_into()
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
let id = mdir
.store_cur_with_flags(msg, &flags.to_string())
.with_context(|| format!("cannot add maildir message to {:?}", mdir.path()))?;
debug!("id: {:?}", id);
let hash = format!("{:x}", md5::compute(&id));
debug!("hash: {:?}", hash);
// Appends hash entry to the id mapper cache file.
let mut mapper = IdMapper::new(mdir.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?;
mapper
.append(vec![(hash.clone(), id.clone())])
.with_context(|| {
format!(
"cannot append hash {:?} with id {:?} to id mapper",
hash, id
)
})?;
info!("<< add maildir message");
Ok(Box::new(hash))
}
fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result<Msg> {
info!(">> get maildir message");
debug!("dir: {:?}", dir);
debug!("short hash: {:?}", short_hash);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let id = IdMapper::new(mdir.path())?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir.path()
)
})?;
debug!("id: {:?}", id);
let mut mail_entry = mdir.find(&id).ok_or_else(|| {
anyhow!(
"cannot find maildir message by id {:?} at {:?}",
id,
mdir.path()
)
})?;
let parsed_mail = mail_entry.parsed().with_context(|| {
format!("cannot parse maildir message {:?} at {:?}", id, mdir.path())
})?;
let msg = Msg::from_parsed_mail(parsed_mail, self.account_config).with_context(|| {
format!("cannot parse maildir message {:?} at {:?}", id, mdir.path())
})?;
trace!("message: {:?}", msg);
info!("<< get maildir message");
Ok(msg)
}
fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
info!(">> copy maildir message");
debug!("source dir: {:?}", dir_src);
debug!("destination dir: {:?}", dir_dst);
let mdir_src = self
.get_mdir_from_dir(dir_src)
.with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?;
let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| {
format!("cannot get destination maildir instance from {:?}", dir_dst)
})?;
let id = IdMapper::new(mdir_src.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir_src.path()
)
})?;
debug!("id: {:?}", id);
mdir_src.copy_to(&id, &mdir_dst).with_context(|| {
format!(
"cannot copy message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
)
})?;
// Appends hash entry to the id mapper cache file.
let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| {
format!("cannot create id mapper instance for {:?}", mdir_dst.path())
})?;
let hash = format!("{:x}", md5::compute(&id));
mapper
.append(vec![(hash.clone(), id.clone())])
.with_context(|| {
format!(
"cannot append hash {:?} with id {:?} to id mapper",
hash, id
)
})?;
info!("<< copy maildir message");
Ok(())
}
fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> {
info!(">> move maildir message");
debug!("source dir: {:?}", dir_src);
debug!("destination dir: {:?}", dir_dst);
let mdir_src = self
.get_mdir_from_dir(dir_src)
.with_context(|| format!("cannot get source maildir instance from {:?}", dir_src))?;
let mdir_dst = self.get_mdir_from_dir(dir_dst).with_context(|| {
format!("cannot get destination maildir instance from {:?}", dir_dst)
})?;
let id = IdMapper::new(mdir_src.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir_src.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir_src.path()
)
})?;
debug!("id: {:?}", id);
mdir_src.move_to(&id, &mdir_dst).with_context(|| {
format!(
"cannot move message {:?} from maildir {:?} to maildir {:?}",
id,
mdir_src.path(),
mdir_dst.path()
)
})?;
// Appends hash entry to the id mapper cache file.
let mut mapper = IdMapper::new(mdir_dst.path()).with_context(|| {
format!("cannot create id mapper instance for {:?}", mdir_dst.path())
})?;
let hash = format!("{:x}", md5::compute(&id));
mapper
.append(vec![(hash.clone(), id.clone())])
.with_context(|| {
format!(
"cannot append hash {:?} with id {:?} to id mapper",
hash, id
)
})?;
info!("<< move maildir message");
Ok(())
}
fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> {
info!(">> delete maildir message");
debug!("dir: {:?}", dir);
debug!("short hash: {:?}", short_hash);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let id = IdMapper::new(mdir.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir.path()
)
})?;
debug!("id: {:?}", id);
mdir.delete(&id).with_context(|| {
format!(
"cannot delete message {:?} from maildir {:?}",
id,
mdir.path()
)
})?;
info!("<< delete maildir message");
Ok(())
}
fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
info!(">> add maildir message flags");
debug!("dir: {:?}", dir);
debug!("short hash: {:?}", short_hash);
debug!("flags: {:?}", flags);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let flags: MaildirFlags = flags
.try_into()
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
debug!("flags: {:?}", flags);
let id = IdMapper::new(mdir.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir.path()
)
})?;
debug!("id: {:?}", id);
mdir.add_flags(&id, &flags.to_string())
.with_context(|| format!("cannot add flags {:?} to maildir message {:?}", flags, id))?;
info!("<< add maildir message flags");
Ok(())
}
fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
info!(">> set maildir message flags");
debug!("dir: {:?}", dir);
debug!("short hash: {:?}", short_hash);
debug!("flags: {:?}", flags);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let flags: MaildirFlags = flags
.try_into()
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
debug!("flags: {:?}", flags);
let id = IdMapper::new(mdir.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir.path()
)
})?;
debug!("id: {:?}", id);
mdir.set_flags(&id, &flags.to_string())
.with_context(|| format!("cannot set flags {:?} to maildir message {:?}", flags, id))?;
info!("<< set maildir message flags");
Ok(())
}
fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> {
info!(">> delete maildir message flags");
debug!("dir: {:?}", dir);
debug!("short hash: {:?}", short_hash);
debug!("flags: {:?}", flags);
let mdir = self
.get_mdir_from_dir(dir)
.with_context(|| format!("cannot get maildir instance from {:?}", dir))?;
let flags: MaildirFlags = flags
.try_into()
.with_context(|| format!("cannot parse maildir flags {:?}", flags))?;
debug!("flags: {:?}", flags);
let id = IdMapper::new(mdir.path())
.with_context(|| format!("cannot create id mapper instance for {:?}", mdir.path()))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find maildir message by short hash {:?} at {:?}",
short_hash,
mdir.path()
)
})?;
debug!("id: {:?}", id);
mdir.remove_flags(&id, &flags.to_string())
.with_context(|| {
format!(
"cannot delete flags {:?} to maildir message {:?}",
flags, id
)
})?;
info!("<< delete maildir message flags");
Ok(())
}
}

View file

@ -1,194 +0,0 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the envelope
use anyhow::{anyhow, Context, Error, Result};
use chrono::DateTime;
use log::trace;
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use crate::{
backends::{MaildirFlag, MaildirFlags},
msg::{from_slice_to_addrs, Addr},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of envelopes.
#[derive(Debug, Default, serde::Serialize)]
pub struct MaildirEnvelopes {
#[serde(rename = "response")]
pub envelopes: Vec<MaildirEnvelope>,
}
impl Deref for MaildirEnvelopes {
type Target = Vec<MaildirEnvelope>;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}
impl DerefMut for MaildirEnvelopes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.envelopes
}
}
impl PrintTable for MaildirEnvelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
// impl Envelopes for MaildirEnvelopes {
// //
// }
/// Represents the envelope. The envelope is just a message subset,
/// and is mostly used for listings.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct MaildirEnvelope {
/// Represents the id of the message.
pub id: String,
/// Represents the MD5 hash of the message id.
pub hash: String,
/// Represents the flags of the message.
pub flags: MaildirFlags,
/// Represents the subject of the message.
pub subject: String,
/// Represents the first sender of the message.
pub sender: String,
/// Represents the date of the message.
pub date: String,
}
impl Table for MaildirEnvelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("HASH").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 hash = self.hash.clone();
let unseen = !self.flags.contains(&MaildirFlag::Seen);
let flags = self.flags.to_symbols_string();
let subject = &self.subject;
let sender = &self.sender;
let date = &self.date;
Row::new()
.cell(Cell::new(hash).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())
}
}
/// Represents a list of raw envelopees returned by the `maildir` crate.
pub type RawMaildirEnvelopes = maildir::MailEntries;
impl<'a> TryFrom<RawMaildirEnvelopes> for MaildirEnvelopes {
type Error = Error;
fn try_from(mail_entries: RawMaildirEnvelopes) -> Result<Self, Self::Error> {
let mut envelopes = vec![];
for entry in mail_entries {
let envelope: MaildirEnvelope = entry
.context("cannot decode maildir mail entry")?
.try_into()
.context("cannot parse maildir mail entry")?;
envelopes.push(envelope);
}
Ok(MaildirEnvelopes { envelopes })
}
}
/// Represents the raw envelope returned by the `maildir` crate.
pub type RawMaildirEnvelope = maildir::MailEntry;
impl<'a> TryFrom<RawMaildirEnvelope> for MaildirEnvelope {
type Error = Error;
fn try_from(mut mail_entry: RawMaildirEnvelope) -> Result<Self, Self::Error> {
trace!(">> build envelope from maildir parsed mail");
let mut envelope = Self::default();
envelope.id = mail_entry.id().into();
envelope.hash = format!("{:x}", md5::compute(&envelope.id));
envelope.flags = (&mail_entry)
.try_into()
.context("cannot parse maildir flags")?;
let parsed_mail = mail_entry
.parsed()
.context("cannot parse maildir mail entry")?;
trace!(">> parse headers");
for h in parsed_mail.get_headers() {
let k = h.get_key();
trace!("header key: {:?}", k);
let v = rfc2047_decoder::decode(h.get_value_raw())
.context(format!("cannot decode value from header {:?}", k))?;
trace!("header value: {:?}", v);
match k.to_lowercase().as_str() {
"date" => {
envelope.date =
DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0)
.context(format!("cannot parse maildir message date {:?}", v))?
.naive_local()
.to_string();
}
"subject" => {
envelope.subject = v.into();
}
"from" => {
envelope.sender = from_slice_to_addrs(v)
.context(format!("cannot parse header {:?}", k))?
.and_then(|senders| {
if senders.is_empty() {
None
} else {
Some(senders)
}
})
.map(|senders| match &senders[0] {
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
}
Addr::Group(mailparse::GroupInfo { group_name, .. }) => {
group_name.to_owned()
}
})
.ok_or_else(|| anyhow!("cannot find sender"))?;
}
_ => (),
}
}
trace!("<< parse headers");
trace!("envelope: {:?}", envelope);
trace!("<< build envelope from maildir parsed mail");
Ok(envelope)
}
}

View file

@ -1,129 +0,0 @@
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ops::Deref,
};
/// Represents the maildir flag variants.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub enum MaildirFlag {
Passed,
Replied,
Seen,
Trashed,
Draft,
Flagged,
Custom(char),
}
/// Represents the maildir flags.
#[derive(Debug, Default, Clone, PartialEq, Eq, serde::Serialize)]
pub struct MaildirFlags(pub Vec<MaildirFlag>);
impl MaildirFlags {
/// Builds a symbols string
pub fn to_symbols_string(&self) -> String {
let mut flags = String::new();
flags.push_str(if self.contains(&MaildirFlag::Seen) {
" "
} else {
""
});
flags.push_str(if self.contains(&MaildirFlag::Replied) {
""
} else {
" "
});
flags.push_str(if self.contains(&MaildirFlag::Passed) {
""
} else {
" "
});
flags.push_str(if self.contains(&MaildirFlag::Flagged) {
""
} else {
" "
});
flags
}
}
impl Deref for MaildirFlags {
type Target = Vec<MaildirFlag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl ToString for MaildirFlags {
fn to_string(&self) -> String {
self.0
.iter()
.map(|flag| {
let flag_char: char = flag.into();
flag_char
})
.collect()
}
}
impl TryFrom<&str> for MaildirFlags {
type Error = Error;
fn try_from(flags_str: &str) -> Result<Self, Self::Error> {
let mut flags = vec![];
for flag_str in flags_str.split_whitespace() {
flags.push(flag_str.trim().try_into()?);
}
Ok(MaildirFlags(flags))
}
}
impl From<&maildir::MailEntry> for MaildirFlags {
fn from(mail_entry: &maildir::MailEntry) -> Self {
let mut flags = vec![];
for c in mail_entry.flags().chars() {
flags.push(match c {
'P' => MaildirFlag::Passed,
'R' => MaildirFlag::Replied,
'S' => MaildirFlag::Seen,
'T' => MaildirFlag::Trashed,
'D' => MaildirFlag::Draft,
'F' => MaildirFlag::Flagged,
custom => MaildirFlag::Custom(custom),
})
}
Self(flags)
}
}
impl Into<char> for &MaildirFlag {
fn into(self) -> char {
match self {
MaildirFlag::Passed => 'P',
MaildirFlag::Replied => 'R',
MaildirFlag::Seen => 'S',
MaildirFlag::Trashed => 'T',
MaildirFlag::Draft => 'D',
MaildirFlag::Flagged => 'F',
MaildirFlag::Custom(custom) => *custom,
}
}
}
impl TryFrom<&str> for MaildirFlag {
type Error = Error;
fn try_from(flag_str: &str) -> Result<Self, Self::Error> {
match flag_str {
"passed" => Ok(MaildirFlag::Passed),
"replied" => Ok(MaildirFlag::Replied),
"seen" => Ok(MaildirFlag::Seen),
"trashed" => Ok(MaildirFlag::Trashed),
"draft" => Ok(MaildirFlag::Draft),
"flagged" => Ok(MaildirFlag::Flagged),
flag_str => Err(anyhow!("cannot parse maildir flag {:?}", flag_str)),
}
}
}

View file

@ -1,144 +0,0 @@
//! Maildir mailbox module.
//!
//! This module provides Maildir types and conversion utilities
//! related to the mailbox
use anyhow::{anyhow, Error, Result};
use std::{
convert::{TryFrom, TryInto},
ffi::OsStr,
fmt::{self, Display},
ops::Deref,
};
use crate::{
mbox::Mboxes,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of Maildir mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct MaildirMboxes {
#[serde(rename = "response")]
pub mboxes: Vec<MaildirMbox>,
}
impl Deref for MaildirMboxes {
type Target = Vec<MaildirMbox>;
fn deref(&self) -> &Self::Target {
&self.mboxes
}
}
impl PrintTable for MaildirMboxes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
impl Mboxes for MaildirMboxes {
//
}
/// Represents the mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct MaildirMbox {
/// Represents the mailbox name.
pub name: String,
}
impl MaildirMbox {
pub fn new(name: &str) -> Self {
Self { name: name.into() }
}
}
impl Display for MaildirMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for MaildirMbox {
fn head() -> Row {
Row::new().cell(Cell::new("SUBDIR").bold().underline().white())
}
fn row(&self) -> Row {
Row::new().cell(Cell::new(&self.name).green())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_create_new_mbox() {
assert_eq!(MaildirMbox::default(), MaildirMbox::new(""));
assert_eq!(
MaildirMbox {
name: "INBOX".into(),
..MaildirMbox::default()
},
MaildirMbox::new("INBOX")
);
}
#[test]
fn it_should_display_mbox() {
let default_mbox = MaildirMbox::default();
assert_eq!("", default_mbox.to_string());
let new_mbox = MaildirMbox::new("INBOX");
assert_eq!("INBOX", new_mbox.to_string());
let full_mbox = MaildirMbox {
name: "Sent".into(),
};
assert_eq!("Sent", full_mbox.to_string());
}
}
/// Represents a list of raw mailboxes returned by the `maildir` crate.
pub type RawMaildirMboxes = maildir::MaildirEntries;
impl TryFrom<RawMaildirMboxes> for MaildirMboxes {
type Error = Error;
fn try_from(mail_entries: RawMaildirMboxes) -> Result<Self, Self::Error> {
let mut mboxes = vec![];
for entry in mail_entries {
mboxes.push(entry?.try_into()?);
}
Ok(MaildirMboxes { mboxes })
}
}
/// Represents the raw mailbox returned by the `maildir` crate.
pub type RawMaildirMbox = maildir::Maildir;
impl TryFrom<RawMaildirMbox> for MaildirMbox {
type Error = Error;
fn try_from(mail_entry: RawMaildirMbox) -> Result<Self, Self::Error> {
let subdir_name = mail_entry.path().file_name();
Ok(Self {
name: subdir_name
.and_then(OsStr::to_str)
.and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) })
.ok_or_else(|| {
anyhow!(
"cannot parse maildir subdirectory name from path {:?}",
subdir_name,
)
})?
.into(),
})
}
}

View file

@ -1,453 +0,0 @@
use std::{convert::TryInto, fs};
use anyhow::{anyhow, Context, Result};
use log::{debug, info, trace};
use crate::{
backends::{Backend, IdMapper, MaildirBackend, NotmuchEnvelopes, NotmuchMbox, NotmuchMboxes},
config::{AccountConfig, NotmuchBackendConfig},
mbox::Mboxes,
msg::{Envelopes, Msg},
};
/// Represents the Notmuch backend.
pub struct NotmuchBackend<'a> {
account_config: &'a AccountConfig,
notmuch_config: &'a NotmuchBackendConfig,
pub mdir: &'a mut MaildirBackend<'a>,
db: notmuch::Database,
}
impl<'a> NotmuchBackend<'a> {
pub fn new(
account_config: &'a AccountConfig,
notmuch_config: &'a NotmuchBackendConfig,
mdir: &'a mut MaildirBackend<'a>,
) -> Result<NotmuchBackend<'a>> {
info!(">> create new notmuch backend");
let backend = Self {
account_config,
notmuch_config,
mdir,
db: notmuch::Database::open(
notmuch_config.notmuch_database_dir.clone(),
notmuch::DatabaseMode::ReadWrite,
)
.with_context(|| {
format!(
"cannot open notmuch database at {:?}",
notmuch_config.notmuch_database_dir
)
})?,
};
info!("<< create new notmuch backend");
Ok(backend)
}
fn _search_envelopes(
&mut self,
query: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
// Gets envelopes matching the given Notmuch query.
let query_builder = self
.db
.create_query(query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let mut envelopes: NotmuchEnvelopes = query_builder
.search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?
.try_into()
.with_context(|| format!("cannot parse notmuch envelopes from query {:?}", query))?;
debug!("envelopes len: {:?}", envelopes.len());
trace!("envelopes: {:?}", envelopes);
// Calculates pagination boundaries.
let page_begin = page * page_size;
debug!("page begin: {:?}", page_begin);
if page_begin > envelopes.len() {
return Err(anyhow!(
"cannot get notmuch envelopes at page {:?} (out of bounds)",
page_begin + 1,
));
}
let page_end = envelopes.len().min(page_begin + page_size);
debug!("page end: {:?}", page_end);
// Sorts envelopes by most recent date.
envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap());
// Applies pagination boundaries.
envelopes.envelopes = envelopes[page_begin..page_end].to_owned();
// Appends envelopes hash to the id mapper cache file and
// calculates the new short hash length. The short hash length
// represents the minimum hash length possible to avoid
// conflicts.
let short_hash_len = {
let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?;
let entries = envelopes
.iter()
.map(|env| (env.hash.to_owned(), env.id.to_owned()))
.collect();
mapper.append(entries)?
};
debug!("short hash length: {:?}", short_hash_len);
// Shorten envelopes hash.
envelopes
.iter_mut()
.for_each(|env| env.hash = env.hash[0..short_hash_len].to_owned());
Ok(Box::new(envelopes))
}
}
impl<'a> Backend<'a> for NotmuchBackend<'a> {
fn add_mbox(&mut self, _mbox: &str) -> Result<()> {
info!(">> add notmuch mailbox");
info!("<< add notmuch mailbox");
Err(anyhow!(
"cannot add notmuch mailbox: feature not implemented"
))
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
info!(">> get notmuch virtual mailboxes");
let mut mboxes: Vec<_> = self
.account_config
.mailboxes
.iter()
.map(|(k, v)| NotmuchMbox::new(k, v))
.collect();
trace!("virtual mailboxes: {:?}", mboxes);
mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
info!("<< get notmuch virtual mailboxes");
Ok(Box::new(NotmuchMboxes { mboxes }))
}
fn del_mbox(&mut self, _mbox: &str) -> Result<()> {
info!(">> delete notmuch mailbox");
info!("<< delete notmuch mailbox");
Err(anyhow!(
"cannot delete notmuch mailbox: feature not implemented"
))
}
fn get_envelopes(
&mut self,
virt_mbox: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
info!(">> get notmuch envelopes");
debug!("virtual mailbox: {:?}", virt_mbox);
debug!("page size: {:?}", page_size);
debug!("page: {:?}", page);
let query = self
.account_config
.mailboxes
.get(virt_mbox)
.map(|s| s.as_str())
.unwrap_or("all");
debug!("query: {:?}", query);
let envelopes = self._search_envelopes(query, page_size, page)?;
info!("<< get notmuch envelopes");
Ok(envelopes)
}
fn search_envelopes(
&mut self,
virt_mbox: &str,
query: &str,
_sort: &str,
page_size: usize,
page: usize,
) -> Result<Box<dyn Envelopes>> {
info!(">> search notmuch envelopes");
debug!("virtual mailbox: {:?}", virt_mbox);
debug!("query: {:?}", query);
debug!("page size: {:?}", page_size);
debug!("page: {:?}", page);
let query = if query.is_empty() {
self.account_config
.mailboxes
.get(virt_mbox)
.map(|s| s.as_str())
.unwrap_or("all")
} else {
query
};
debug!("final query: {:?}", query);
let envelopes = self._search_envelopes(query, page_size, page)?;
info!("<< search notmuch envelopes");
Ok(envelopes)
}
fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result<Box<dyn ToString>> {
info!(">> add notmuch envelopes");
debug!("tags: {:?}", tags);
let dir = &self.notmuch_config.notmuch_database_dir;
// Adds the message to the maildir folder and gets its hash.
let hash = self
.mdir
.add_msg("", msg, "seen")
.with_context(|| {
format!(
"cannot add notmuch message to maildir {:?}",
self.notmuch_config.notmuch_database_dir
)
})?
.to_string();
debug!("hash: {:?}", hash);
// Retrieves the file path of the added message by its maildir
// identifier.
let mut mapper = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?;
let id = mapper
.find(&hash)
.with_context(|| format!("cannot find notmuch message from short hash {:?}", hash))?;
debug!("id: {:?}", id);
let file_path = dir.join("cur").join(format!("{}:2,S", id));
debug!("file path: {:?}", file_path);
// Adds the message to the notmuch database by indexing it.
let id = self
.db
.index_file(&file_path, None)
.with_context(|| format!("cannot index notmuch message from file {:?}", file_path))?
.id()
.to_string();
let hash = format!("{:x}", md5::compute(&id));
// Appends hash entry to the id mapper cache file.
mapper
.append(vec![(hash.clone(), id.clone())])
.with_context(|| {
format!(
"cannot append hash {:?} with id {:?} to id mapper",
hash, id
)
})?;
// Attaches tags to the notmuch message.
self.add_flags("", &hash, tags)
.with_context(|| format!("cannot add flags to notmuch message {:?}", id))?;
info!("<< add notmuch envelopes");
Ok(Box::new(hash))
}
fn get_msg(&mut self, _: &str, short_hash: &str) -> Result<Msg> {
info!(">> add notmuch envelopes");
debug!("short hash: {:?}", short_hash);
let dir = &self.notmuch_config.notmuch_database_dir;
let id = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find notmuch message from short hash {:?}",
short_hash
)
})?;
debug!("id: {:?}", id);
let msg_file_path = self
.db
.find_message(&id)
.with_context(|| format!("cannot find notmuch message {:?}", id))?
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
.filename()
.to_owned();
debug!("message file path: {:?}", msg_file_path);
let raw_msg = fs::read(&msg_file_path).with_context(|| {
format!("cannot read notmuch message from file {:?}", msg_file_path)
})?;
let msg = mailparse::parse_mail(&raw_msg)
.with_context(|| format!("cannot parse raw notmuch message {:?}", id))?;
let msg = Msg::from_parsed_mail(msg, &self.account_config)
.with_context(|| format!("cannot parse notmuch message {:?}", id))?;
trace!("message: {:?}", msg);
info!("<< get notmuch message");
Ok(msg)
}
fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
info!(">> copy notmuch message");
info!("<< copy notmuch message");
Err(anyhow!(
"cannot copy notmuch message: feature not implemented"
))
}
fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> {
info!(">> move notmuch message");
info!("<< move notmuch message");
Err(anyhow!(
"cannot move notmuch message: feature not implemented"
))
}
fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> {
info!(">> delete notmuch message");
debug!("short hash: {:?}", short_hash);
let dir = &self.notmuch_config.notmuch_database_dir;
let id = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find notmuch message from short hash {:?}",
short_hash
)
})?;
debug!("id: {:?}", id);
let msg_file_path = self
.db
.find_message(&id)
.with_context(|| format!("cannot find notmuch message {:?}", id))?
.ok_or_else(|| anyhow!("cannot find notmuch message {:?}", id))?
.filename()
.to_owned();
debug!("message file path: {:?}", msg_file_path);
self.db
.remove_message(msg_file_path)
.with_context(|| format!("cannot delete notmuch message {:?}", id))?;
info!("<< delete notmuch message");
Ok(())
}
fn add_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
info!(">> add notmuch message flags");
debug!("tags: {:?}", tags);
let dir = &self.notmuch_config.notmuch_database_dir;
let id = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find notmuch message from short hash {:?}",
short_hash
)
})?;
debug!("id: {:?}", id);
let query = format!("id:{}", id);
debug!("query: {:?}", query);
let tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self
.db
.create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let msgs = query_builder
.search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for msg in msgs {
for tag in tags.iter() {
msg.add_tag(*tag).with_context(|| {
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
})?
}
}
info!("<< add notmuch message flags");
Ok(())
}
fn set_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
info!(">> set notmuch message flags");
debug!("tags: {:?}", tags);
let dir = &self.notmuch_config.notmuch_database_dir;
let id = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find notmuch message from short hash {:?}",
short_hash
)
})?;
debug!("id: {:?}", id);
let query = format!("id:{}", id);
debug!("query: {:?}", query);
let tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self
.db
.create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let msgs = query_builder
.search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for msg in msgs {
msg.remove_all_tags().with_context(|| {
format!("cannot remove all tags from notmuch message {:?}", msg.id())
})?;
for tag in tags.iter() {
msg.add_tag(*tag).with_context(|| {
format!("cannot add tag {:?} to notmuch message {:?}", tag, msg.id())
})?
}
}
info!("<< set notmuch message flags");
Ok(())
}
fn del_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> {
info!(">> delete notmuch message flags");
debug!("tags: {:?}", tags);
let dir = &self.notmuch_config.notmuch_database_dir;
let id = IdMapper::new(dir)
.with_context(|| format!("cannot create id mapper instance for {:?}", dir))?
.find(short_hash)
.with_context(|| {
format!(
"cannot find notmuch message from short hash {:?}",
short_hash
)
})?;
debug!("id: {:?}", id);
let query = format!("id:{}", id);
debug!("query: {:?}", query);
let tags: Vec<_> = tags.split_whitespace().collect();
let query_builder = self
.db
.create_query(&query)
.with_context(|| format!("cannot create notmuch query from {:?}", query))?;
let msgs = query_builder
.search_messages()
.with_context(|| format!("cannot find notmuch envelopes from query {:?}", query))?;
for msg in msgs {
for tag in tags.iter() {
msg.remove_tag(*tag).with_context(|| {
format!(
"cannot delete tag {:?} from notmuch message {:?}",
tag,
msg.id()
)
})?
}
}
info!("<< delete notmuch message flags");
Ok(())
}
}

View file

@ -1,180 +0,0 @@
//! Notmuch mailbox module.
//!
//! This module provides Notmuch types and conversion utilities
//! related to the envelope
use anyhow::{anyhow, Context, Error, Result};
use chrono::DateTime;
use log::{info, trace};
use std::{
convert::{TryFrom, TryInto},
ops::{Deref, DerefMut},
};
use crate::{
msg::{from_slice_to_addrs, Addr},
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of envelopes.
#[derive(Debug, Default, serde::Serialize)]
pub struct NotmuchEnvelopes {
#[serde(rename = "response")]
pub envelopes: Vec<NotmuchEnvelope>,
}
impl Deref for NotmuchEnvelopes {
type Target = Vec<NotmuchEnvelope>;
fn deref(&self) -> &Self::Target {
&self.envelopes
}
}
impl DerefMut for NotmuchEnvelopes {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.envelopes
}
}
impl PrintTable for NotmuchEnvelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
/// Represents the envelope. The envelope is just a message subset,
/// and is mostly used for listings.
#[derive(Debug, Default, Clone, serde::Serialize)]
pub struct NotmuchEnvelope {
/// Represents the id of the message.
pub id: String,
/// Represents the MD5 hash of the message id.
pub hash: String,
/// Represents the tags of the message.
pub flags: Vec<String>,
/// Represents the subject of the message.
pub subject: String,
/// Represents the first sender of the message.
pub sender: String,
/// Represents the date of the message.
pub date: String,
}
impl Table for NotmuchEnvelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("HASH").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 hash = self.hash.to_string();
let unseen = !self.flags.contains(&String::from("unread"));
let flags = String::new();
let subject = &self.subject;
let sender = &self.sender;
let date = &self.date;
Row::new()
.cell(Cell::new(hash).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())
}
}
/// Represents a list of raw envelopees returned by the `notmuch` crate.
pub type RawNotmuchEnvelopes = notmuch::Messages;
impl<'a> TryFrom<RawNotmuchEnvelopes> for NotmuchEnvelopes {
type Error = Error;
fn try_from(raw_envelopes: RawNotmuchEnvelopes) -> Result<Self, Self::Error> {
let mut envelopes = vec![];
for raw_envelope in raw_envelopes {
let envelope: NotmuchEnvelope = raw_envelope
.try_into()
.context("cannot parse notmuch mail entry")?;
envelopes.push(envelope);
}
Ok(NotmuchEnvelopes { envelopes })
}
}
/// Represents the raw envelope returned by the `notmuch` crate.
pub type RawNotmuchEnvelope = notmuch::Message;
impl<'a> TryFrom<RawNotmuchEnvelope> for NotmuchEnvelope {
type Error = Error;
fn try_from(raw_envelope: RawNotmuchEnvelope) -> Result<Self, Self::Error> {
info!("begin: try building envelope from notmuch parsed mail");
let id = raw_envelope.id().to_string();
let hash = format!("{:x}", md5::compute(&id));
let subject = raw_envelope
.header("subject")
.context("cannot get header \"Subject\" from notmuch message")?
.unwrap_or_default()
.to_string();
let sender = raw_envelope
.header("from")
.context("cannot get header \"From\" from notmuch message")?
.ok_or_else(|| anyhow!("cannot parse sender from notmuch message {:?}", id))?
.to_string();
let sender = from_slice_to_addrs(sender)?
.and_then(|senders| {
if senders.is_empty() {
None
} else {
Some(senders)
}
})
.map(|senders| match &senders[0] {
Addr::Single(mailparse::SingleInfo { display_name, addr }) => {
display_name.as_ref().unwrap_or_else(|| addr).to_owned()
}
Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(),
})
.ok_or_else(|| anyhow!("cannot find sender"))?;
let date = raw_envelope
.header("date")
.context("cannot get header \"Date\" from notmuch message")?
.ok_or_else(|| anyhow!("cannot parse date of notmuch message {:?}", id))?
.to_string();
let date =
DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0)
.context(format!(
"cannot parse message date {:?} of notmuch message {:?}",
date, id
))?
.naive_local()
.to_string();
let envelope = Self {
id,
hash,
flags: raw_envelope.tags().collect(),
subject,
sender,
date,
};
trace!("envelope: {:?}", envelope);
info!("end: try building envelope from notmuch parsed mail");
Ok(envelope)
}
}

View file

@ -1,83 +0,0 @@
//! Notmuch mailbox module.
//!
//! This module provides Notmuch types and conversion utilities
//! related to the mailbox
use anyhow::Result;
use std::{
fmt::{self, Display},
ops::Deref,
};
use crate::{
mbox::Mboxes,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents a list of Notmuch mailboxes.
#[derive(Debug, Default, serde::Serialize)]
pub struct NotmuchMboxes {
#[serde(rename = "response")]
pub mboxes: Vec<NotmuchMbox>,
}
impl Deref for NotmuchMboxes {
type Target = Vec<NotmuchMbox>;
fn deref(&self) -> &Self::Target {
&self.mboxes
}
}
impl PrintTable for NotmuchMboxes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
impl Mboxes for NotmuchMboxes {
//
}
/// Represents the notmuch virtual mailbox.
#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
pub struct NotmuchMbox {
/// Represents the virtual mailbox name.
pub name: String,
/// Represents the query associated to the virtual mailbox name.
pub query: String,
}
impl NotmuchMbox {
pub fn new(name: &str, query: &str) -> Self {
Self {
name: name.into(),
query: query.into(),
}
}
}
impl Display for NotmuchMbox {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for NotmuchMbox {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").bold().underline().white())
.cell(Cell::new("QUERY").bold().underline().white())
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.name).white())
.cell(Cell::new(&self.query).green())
}
}

View file

@ -1,39 +0,0 @@
//! Module related to completion CLI.
//!
//! This module provides subcommands and a command matcher related to completion.
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, Shell, SubCommand};
use log::{debug, info};
type OptionShell<'a> = Option<&'a str>;
/// Completion commands.
pub enum Command<'a> {
/// Generate completion script for the given shell slice.
Generate(OptionShell<'a>),
}
/// Completion command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Command<'a>>> {
info!("entering completion command matcher");
if let Some(m) = m.subcommand_matches("completion") {
info!("completion command matched");
let shell = m.value_of("shell");
debug!("shell: {:?}", shell);
return Ok(Some(Command::Generate(shell)));
};
Ok(None)
}
/// Completion subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("completion")
.aliases(&["completions", "compl", "compe", "comp"])
.about("Generates the completion script for the given shell")
.args(&[Arg::with_name("shell")
.possible_values(&Shell::variants()[..])
.required(true)])]
}

View file

@ -1,21 +0,0 @@
//! Module related to completion handling.
//!
//! This module gathers all completion commands.
use anyhow::{anyhow, Context, Result};
use clap::{App, Shell};
use log::{debug, info};
use std::{io, str::FromStr};
/// Generates completion script from the given [`clap::App`] for the given shell slice.
pub fn generate<'a>(mut app: App<'a, 'a>, shell: Option<&'a str>) -> Result<()> {
info!("entering generate completion handler");
let shell = Shell::from_str(shell.unwrap_or_default())
.map_err(|err| anyhow!(err))
.context("cannot parse shell")?;
debug!("shell: {}", shell);
app.gen_completions_to("himalaya", shell, &mut io::stdout());
Ok(())
}

View file

@ -1,9 +0,0 @@
//! Module related to shell completion.
//!
//! This module allows users to generate autocompletion scripts for their shells. You can see the
//! list of available shells directly on the [clap's docs.rs website].
//!
//! [clap's docs.rs website]: https://docs.rs/clap/2.33.3/clap/enum.Shell.html
pub mod compl_args;
pub mod compl_handlers;

View file

@ -1,109 +0,0 @@
//! Account module.
//!
//! This module contains the definition of the printable account,
//! which is only used by the "accounts" command to list all available
//! accounts from the config file.
use anyhow::Result;
use serde::Serialize;
use std::{
collections::hash_map::Iter,
fmt::{self, Display},
ops::Deref,
};
use crate::{
config::DeserializedAccountConfig,
output::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
/// Represents the list of printable accounts.
#[derive(Debug, Default, Serialize)]
pub struct Accounts(pub Vec<Account>);
impl Deref for Accounts {
type Target = Vec<Account>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
/// Represents the printable account.
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct Account {
/// Represents the account name.
pub name: String,
/// Represents the backend name of the account.
pub backend: String,
/// Represents the default state of the account.
pub default: bool,
}
impl Account {
pub fn new(name: &str, backend: &str, default: bool) -> Self {
Self {
name: name.into(),
backend: backend.into(),
default,
}
}
}
impl Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKEND").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
}
fn row(&self) -> Row {
let default = if self.default { "yes" } else { "" };
Row::new()
.cell(Cell::new(&self.name).shrinkable().green())
.cell(Cell::new(&self.backend).blue())
.cell(Cell::new(default).white())
}
}
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, config)| match config {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => {
Account::new(name, "imap", config.default.unwrap_or_default())
}
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => {
Account::new(name, "maildir", config.default.unwrap_or_default())
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => {
Account::new(name, "notmuch", config.default.unwrap_or_default())
}
})
.collect();
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
Self(accounts)
}
}

View file

@ -1,56 +0,0 @@
//! This module provides arguments related to the user account config.
use anyhow::Result;
use clap::{App, Arg, ArgMatches, SubCommand};
use log::{debug, info};
use crate::ui::table_arg;
type MaxTableWidth = Option<usize>;
/// Represents the account commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
/// Represents the list accounts command.
List(MaxTableWidth),
}
/// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
info!(">> account command matcher");
let cmd = if let Some(m) = m.subcommand_matches("accounts") {
info!("accounts command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
Some(Cmd::List(max_table_width))
} else {
None
};
info!("<< account command matcher");
Ok(cmd)
}
/// Represents the account subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("accounts")
.aliases(&["account", "acc", "a"])
.about("Lists accounts")
.arg(table_arg::max_width())]
}
/// Represents the user account name argument.
/// This argument allows the user to select a different account than
/// the default one.
pub fn name_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("account")
.long("account")
.short("a")
.help("Selects a specific account")
.value_name("NAME")
}

View file

@ -1,437 +0,0 @@
use anyhow::{anyhow, Context, Result};
use lettre::transport::smtp::authentication::Credentials as SmtpCredentials;
use log::{debug, info, trace};
use mailparse::MailAddr;
use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf};
use crate::{config::*, output::run_cmd};
/// Represents the user account.
#[derive(Debug, Default, Clone)]
pub struct AccountConfig {
/// Represents the name of the user account.
pub name: String,
/// Makes this account the default one.
pub default: bool,
/// Represents the display name of the user account.
pub display_name: String,
/// Represents the email address of the user account.
pub email: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: PathBuf,
/// Represents the signature of the user.
pub sig: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: usize,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: String,
/// Represents the watch commands.
pub watch_cmds: Vec<String>,
/// Represents the text/plain format as defined in the
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
pub format: Format,
/// Overrides the default headers displayed at the top of
/// the read message.
pub read_headers: Vec<String>,
/// Represents mailbox aliases.
pub mailboxes: HashMap<String, String>,
/// Represents hooks.
pub hooks: Hooks,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: bool,
/// Trusts any certificate.
pub smtp_insecure: bool,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
}
impl<'a> AccountConfig {
/// tries to create an account from a config and an optional account name.
pub fn from_config_and_opt_account_name(
config: &'a DeserializedConfig,
account_name: Option<&str>,
) -> Result<(AccountConfig, BackendConfig)> {
info!("begin: parsing account and backend configs from config and account name");
debug!("account name: {:?}", account_name.unwrap_or("default"));
let (name, account) = match account_name.map(|name| name.trim()) {
Some("default") | Some("") | None => config
.accounts
.iter()
.find(|(_, account)| match account {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(account) => account.default.unwrap_or_default(),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(account) => {
account.default.unwrap_or_default()
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(account) => {
account.default.unwrap_or_default()
}
})
.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 base_account = account.to_base();
let downloads_dir = base_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 = base_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 = base_account
.signature_delimiter
.as_ref()
.or_else(|| config.signature_delimiter.as_ref())
.unwrap_or(&default_sig_delim);
let sig = base_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_config = AccountConfig {
name,
display_name: base_account
.name
.as_ref()
.unwrap_or(&config.name)
.to_owned(),
downloads_dir,
sig,
default_page_size,
notify_cmd: base_account.notify_cmd.clone(),
notify_query: base_account
.notify_query
.as_ref()
.or_else(|| config.notify_query.as_ref())
.unwrap_or(&String::from("NEW"))
.to_owned(),
watch_cmds: base_account
.watch_cmds
.as_ref()
.or_else(|| config.watch_cmds.as_ref())
.unwrap_or(&vec![])
.to_owned(),
format: base_account.format.unwrap_or_default(),
read_headers: base_account.read_headers,
mailboxes: base_account.mailboxes.clone(),
hooks: base_account.hooks.unwrap_or_default(),
default: base_account.default.unwrap_or_default(),
email: base_account.email.to_owned(),
smtp_host: base_account.smtp_host.to_owned(),
smtp_port: base_account.smtp_port,
smtp_starttls: base_account.smtp_starttls.unwrap_or_default(),
smtp_insecure: base_account.smtp_insecure.unwrap_or_default(),
smtp_login: base_account.smtp_login.to_owned(),
smtp_passwd_cmd: base_account.smtp_passwd_cmd.to_owned(),
pgp_encrypt_cmd: base_account.pgp_encrypt_cmd.to_owned(),
pgp_decrypt_cmd: base_account.pgp_decrypt_cmd.to_owned(),
};
trace!("account config: {:?}", account_config);
let backend_config = match account {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => BackendConfig::Imap(ImapBackendConfig {
imap_host: config.imap_host.clone(),
imap_port: config.imap_port.clone(),
imap_starttls: config.imap_starttls.unwrap_or_default(),
imap_insecure: config.imap_insecure.unwrap_or_default(),
imap_login: config.imap_login.clone(),
imap_passwd_cmd: config.imap_passwd_cmd.clone(),
}),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => {
BackendConfig::Maildir(MaildirBackendConfig {
maildir_dir: shellexpand::full(&config.maildir_dir)?.to_string().into(),
})
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => {
BackendConfig::Notmuch(NotmuchBackendConfig {
notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir)?
.to_string()
.into(),
})
}
};
trace!("backend config: {:?}", backend_config);
info!("end: parsing account and backend configs from config and account name");
Ok((account_config, backend_config))
}
/// Builds the full RFC822 compliant address of the user account.
pub fn address(&self) -> Result<MailAddr> {
let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c));
let addr = if self.display_name.is_empty() {
self.email.clone()
} else if has_special_chars {
// Wraps the name with double quotes if it contains any special character.
format!("\"{}\" <{}>", self.display_name, self.email)
} else {
format!("{} <{}>", self.display_name, self.email)
};
Ok(mailparse::addrparse(&addr)
.context(format!(
"cannot parse account address {:?}",
self.display_name
))?
.first()
.ok_or_else(|| anyhow!("cannot parse account address {:?}", self.display_name))?
.clone())
}
/// Builds the user account SMTP credentials.
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))
}
/// Encrypts a file.
pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() {
let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path);
run_cmd(&encrypt_file_cmd).map(Some).context(format!(
"cannot run pgp encrypt command {:?}",
encrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Decrypts a file.
pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result<Option<String>> {
if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() {
let decrypt_file_cmd = format!("{} {:?}", cmd, path);
run_cmd(&decrypt_file_cmd).map(Some).context(format!(
"cannot run pgp decrypt command {:?}",
decrypt_file_cmd
))
} else {
Ok(None)
}
}
/// Gets the download path from a file name.
pub fn get_download_file_path<S: AsRef<str>>(&self, file_name: S) -> Result<PathBuf> {
let file_path = self.downloads_dir.join(file_name.as_ref());
self.get_unique_download_file_path(&file_path, |path, _count| path.is_file())
.context(format!(
"cannot get download file path of {:?}",
file_name.as_ref()
))
}
/// Gets the unique download path from a file name by adding suffixes in case of name conflicts.
pub fn get_unique_download_file_path(
&self,
original_file_path: &PathBuf,
is_file: impl Fn(&PathBuf, u8) -> bool,
) -> Result<PathBuf> {
let mut count = 0;
let file_ext = original_file_path
.extension()
.and_then(OsStr::to_str)
.map(|fext| String::from(".") + fext)
.unwrap_or_default();
let mut file_path = original_file_path.clone();
while is_file(&file_path, count) {
count += 1;
file_path.set_file_name(OsStr::new(
&original_file_path
.file_stem()
.and_then(OsStr::to_str)
.map(|fstem| format!("{}_{}{}", fstem, count, file_ext))
.ok_or_else(|| anyhow!("cannot get stem from file {:?}", original_file_path))?,
));
}
Ok(file_path)
}
/// Runs the notify command.
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 "New message from {}" "{}""#, sender, subject);
let cmd = self
.notify_cmd
.as_ref()
.map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender))
.unwrap_or(default_cmd);
debug!("run command: {}", cmd);
run_cmd(&cmd).context("cannot run notify cmd")?;
Ok(())
}
/// Gets the mailbox alias if exists, otherwise returns the
/// mailbox. Also tries to expand shell variables.
pub fn get_mbox_alias(&self, mbox: &str) -> Result<String> {
let mbox = self
.mailboxes
.get(&mbox.trim().to_lowercase())
.map(|s| s.as_str())
.unwrap_or(mbox);
shellexpand::full(mbox)
.map(String::from)
.with_context(|| format!("cannot expand mailbox path {:?}", mbox))
}
}
/// Represents all existing kind of account (backend).
#[derive(Debug, Clone)]
pub enum BackendConfig {
#[cfg(feature = "imap-backend")]
Imap(ImapBackendConfig),
#[cfg(feature = "maildir-backend")]
Maildir(MaildirBackendConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(NotmuchBackendConfig),
}
/// Represents the IMAP backend.
#[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone)]
pub struct ImapBackendConfig {
/// Represents the IMAP host.
pub imap_host: String,
/// Represents the IMAP port.
pub imap_port: u16,
/// Enables StartTLS.
pub imap_starttls: bool,
/// Trusts any certificate.
pub imap_insecure: bool,
/// Represents the IMAP login.
pub imap_login: String,
/// Represents the IMAP password command.
pub imap_passwd_cmd: String,
}
#[cfg(feature = "imap-backend")]
impl ImapBackendConfig {
/// Gets the IMAP password of the user account.
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)
}
}
/// Represents the Maildir backend.
#[cfg(feature = "maildir-backend")]
#[derive(Debug, Default, Clone)]
pub struct MaildirBackendConfig {
/// Represents the Maildir directory path.
pub maildir_dir: PathBuf,
}
/// Represents the Notmuch backend.
#[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone)]
pub struct NotmuchBackendConfig {
/// Represents the Notmuch database path.
pub notmuch_database_dir: PathBuf,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_get_unique_download_file_path() {
let account = AccountConfig::default();
let path = PathBuf::from("downloads/file.ext");
// When file path is unique
assert!(matches!(
account.get_unique_download_file_path(&path, |_, _| false),
Ok(path) if path == PathBuf::from("downloads/file.ext")
));
// When 1 file path already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 1),
Ok(path) if path == PathBuf::from("downloads/file_1.ext")
));
// When 5 file paths already exist
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5.ext")
));
// When file path has no extension
let path = PathBuf::from("downloads/file");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file_5")
));
// When file path has 2 extensions
let path = PathBuf::from("downloads/file.ext.ext2");
assert!(matches!(
account.get_unique_download_file_path(&path, |_, count| count < 5),
Ok(path) if path == PathBuf::from("downloads/file.ext_5.ext2")
));
}
}

View file

@ -1,138 +0,0 @@
//! Account handlers module.
//!
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use log::{info, trace};
use crate::{
config::{AccountConfig, Accounts, DeserializedConfig},
output::{PrintTableOpts, PrinterService},
};
/// Lists all accounts.
pub fn list<'a, P: PrinterService>(
max_width: Option<usize>,
config: &DeserializedConfig,
account_config: &AccountConfig,
printer: &mut P,
) -> Result<()> {
info!(">> account list handler");
let accounts: Accounts = config.accounts.iter().into();
trace!("accounts: {:?}", accounts);
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &account_config.format,
max_width,
},
)?;
info!("<< account list handler");
Ok(())
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
use termcolor::ColorSpec;
use crate::{
config::{DeserializedAccountConfig, DeserializedImapAccountConfig},
output::{Print, PrintTable, WriteColor},
};
use super::*;
#[test]
fn it_should_match_cmds_accounts() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl PrinterService for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn print_struct<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
let config = DeserializedConfig {
accounts: HashMap::from_iter([(
"account-1".into(),
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
default: Some(true),
..DeserializedImapAccountConfig::default()
}),
)]),
..DeserializedConfig::default()
};
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
assert!(list(None, &config, &account_config, &mut printer).is_ok());
assert_eq!(
concat![
"\n",
"NAME │BACKEND │DEFAULT \n",
"account-1 │imap │yes \n",
"\n"
],
printer.writer.content
);
}
}

View file

@ -1,13 +0,0 @@
//! This module provides arguments related to the user config.
use clap::Arg;
/// Represents the config path argument.
/// This argument allows the user to customize the config file path.
pub fn path_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("config")
.long("config")
.short("c")
.help("Forces a specific config path")
.value_name("PATH")
}

View file

@ -1,152 +0,0 @@
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::config::{Format, Hooks};
pub trait ToDeserializedBaseAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig;
}
/// Represents all existing kind of account config.
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum DeserializedAccountConfig {
#[cfg(feature = "imap-backend")]
Imap(DeserializedImapAccountConfig),
#[cfg(feature = "maildir-backend")]
Maildir(DeserializedMaildirAccountConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(DeserializedNotmuchAccountConfig),
}
impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
match self {
#[cfg(feature = "imap-backend")]
Self::Imap(config) => config.to_base(),
#[cfg(feature = "maildir-backend")]
Self::Maildir(config) => config.to_base(),
#[cfg(feature = "notmuch-backend")]
Self::Notmuch(config) => config.to_base(),
}
}
}
macro_rules! make_account_config {
($AccountConfig:ident, $($element: ident: $ty: ty),*) => {
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct $AccountConfig {
/// Overrides the display name of the user for this account.
pub name: Option<String>,
/// Overrides the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Overrides the signature for this account.
pub signature: Option<String>,
/// Overrides the signature delimiter for this account.
pub signature_delimiter: Option<String>,
/// Overrides the default page size for this account.
pub default_page_size: Option<usize>,
/// Overrides the notify command for this account.
pub notify_cmd: Option<String>,
/// Overrides the IMAP query used to fetch new messages for this account.
pub notify_query: Option<String>,
/// Overrides the watch commands for this account.
pub watch_cmds: Option<Vec<String>>,
/// Represents the text/plain format as defined in the
/// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt)
pub format: Option<Format>,
/// Represents the default headers displayed at the top of
/// the read message.
#[serde(default)]
pub read_headers: Vec<String>,
/// Makes this account the default one.
pub default: Option<bool>,
/// Represents the account email address.
pub email: String,
/// Represents the SMTP host.
pub smtp_host: String,
/// Represents the SMTP port.
pub smtp_port: u16,
/// Enables StartTLS.
pub smtp_starttls: Option<bool>,
/// Trusts any certificate.
pub smtp_insecure: Option<bool>,
/// Represents the SMTP login.
pub smtp_login: String,
/// Represents the SMTP password command.
pub smtp_passwd_cmd: String,
/// Represents the command used to encrypt a message.
pub pgp_encrypt_cmd: Option<String>,
/// Represents the command used to decrypt a message.
pub pgp_decrypt_cmd: Option<String>,
/// Represents mailbox aliases.
#[serde(default)]
pub mailboxes: HashMap<String, String>,
/// Represents hooks.
pub hooks: Option<Hooks>,
$(pub $element: $ty),*
}
impl ToDeserializedBaseAccountConfig for $AccountConfig {
fn to_base(&self) -> DeserializedBaseAccountConfig {
DeserializedBaseAccountConfig {
name: self.name.clone(),
downloads_dir: self.downloads_dir.clone(),
signature: self.signature.clone(),
signature_delimiter: self.signature_delimiter.clone(),
default_page_size: self.default_page_size.clone(),
notify_cmd: self.notify_cmd.clone(),
notify_query: self.notify_query.clone(),
watch_cmds: self.watch_cmds.clone(),
format: self.format.clone(),
read_headers: self.read_headers.clone(),
default: self.default.clone(),
email: self.email.clone(),
smtp_host: self.smtp_host.clone(),
smtp_port: self.smtp_port.clone(),
smtp_starttls: self.smtp_starttls.clone(),
smtp_insecure: self.smtp_insecure.clone(),
smtp_login: self.smtp_login.clone(),
smtp_passwd_cmd: self.smtp_passwd_cmd.clone(),
pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(),
pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(),
mailboxes: self.mailboxes.clone(),
hooks: self.hooks.clone(),
}
}
}
}
}
make_account_config!(DeserializedBaseAccountConfig,);
#[cfg(feature = "imap-backend")]
make_account_config!(
DeserializedImapAccountConfig,
imap_host: String,
imap_port: u16,
imap_starttls: Option<bool>,
imap_insecure: Option<bool>,
imap_login: String,
imap_passwd_cmd: String
);
#[cfg(feature = "maildir-backend")]
make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String);
#[cfg(feature = "notmuch-backend")]
make_account_config!(
DeserializedNotmuchAccountConfig,
notmuch_database_dir: String
);

View file

@ -1,97 +0,0 @@
use anyhow::{Context, Result};
use log::{debug, info, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf};
use toml;
use crate::config::DeserializedAccountConfig;
pub const DEFAULT_PAGE_SIZE: usize = 10;
pub const DEFAULT_SIG_DELIM: &str = "-- \n";
pub const DEFAULT_INBOX_FOLDER: &str = "INBOX";
pub const DEFAULT_SENT_FOLDER: &str = "Sent";
pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts";
/// Represents the user config file.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
/// Represents the display name of the user.
pub name: String,
/// Represents the downloads directory (mostly for attachments).
pub downloads_dir: Option<PathBuf>,
/// Represents the signature of the user.
pub signature: Option<String>,
/// Overrides the default signature delimiter "`-- \n`".
pub signature_delimiter: Option<String>,
/// Represents the default page size for listings.
pub default_page_size: Option<usize>,
/// Represents the notify command.
pub notify_cmd: Option<String>,
/// Overrides the default IMAP query "NEW" used to fetch new messages
pub notify_query: Option<String>,
/// Represents the watch commands.
pub watch_cmds: Option<Vec<String>>,
/// Represents all the user accounts.
#[serde(flatten)]
pub accounts: HashMap<String, DeserializedAccountConfig>,
}
impl DeserializedConfig {
/// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
info!("begin: try to parse config from path");
debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config = toml::from_str(&content).context("cannot parse config file")?;
info!("end: try to parse config from path");
trace!("config: {:?}", config);
Ok(config)
}
/// Tries to get the XDG config file path from XDG_CONFIG_HOME environment variable.
fn path_from_xdg() -> Result<PathBuf> {
let path =
env::var("XDG_CONFIG_HOME").context("cannot find \"XDG_CONFIG_HOME\" env var")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path)
}
/// Tries to get the XDG config file path from HOME environment variable.
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path)
.join(".config")
.join("himalaya")
.join("config.toml");
Ok(path)
}
/// Tries to get the .himalayarc config file path from HOME environment variable.
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot find {:?} env var", home_var))?;
let path = PathBuf::from(path).join(".himalayarc");
Ok(path)
}
/// Tries to get the config file path.
pub fn path() -> Result<PathBuf> {
Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
.context("cannot find config path")
}
}

View file

@ -1,23 +0,0 @@
use serde::Deserialize;
/// Represents the text/plain format as defined in the [RFC2646]. The
/// format is then used by the table system to adjust the way it is
/// rendered.
///
/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(tag = "type", content = "width", rename_all = "lowercase")]
pub enum Format {
// Forces the content width with a fixed amount of pixels.
Fixed(usize),
// Makes the content fit the terminal.
Auto,
// Does not restrict the content.
Flowed,
}
impl Default for Format {
fn default() -> Self {
Self::Auto
}
}

View file

@ -1,7 +0,0 @@
use serde::Deserialize;
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Hooks {
pub pre_send: Option<String>,
}

View file

@ -1,136 +0,0 @@
pub mod mbox {
pub mod mbox;
pub use mbox::*;
pub mod mbox_args;
pub mod mbox_handlers;
}
pub mod msg {
pub mod envelope;
pub use envelope::*;
pub mod msg_args;
pub mod msg_handlers;
pub mod msg_utils;
pub mod flag_args;
pub mod flag_handlers;
pub mod tpl_args;
pub use tpl_args::TplOverride;
pub mod tpl_handlers;
pub mod msg_entity;
pub use msg_entity::*;
pub mod parts_entity;
pub use parts_entity::*;
pub mod addr_entity;
pub use addr_entity::*;
}
pub mod backends {
pub mod backend;
pub use backend::*;
pub mod id_mapper;
pub use id_mapper::*;
#[cfg(feature = "imap-backend")]
pub mod imap {
pub mod imap_args;
pub mod imap_backend;
pub use imap_backend::*;
pub mod imap_handlers;
pub mod imap_mbox;
pub use imap_mbox::*;
pub mod imap_mbox_attr;
pub use imap_mbox_attr::*;
pub mod imap_envelope;
pub use imap_envelope::*;
pub mod imap_flag;
pub use imap_flag::*;
pub mod msg_sort_criterion;
}
#[cfg(feature = "imap-backend")]
pub use self::imap::*;
#[cfg(feature = "maildir-backend")]
pub mod maildir {
pub mod maildir_backend;
pub use maildir_backend::*;
pub mod maildir_mbox;
pub use maildir_mbox::*;
pub mod maildir_envelope;
pub use maildir_envelope::*;
pub mod maildir_flag;
pub use maildir_flag::*;
}
#[cfg(feature = "maildir-backend")]
pub use self::maildir::*;
#[cfg(feature = "notmuch-backend")]
pub mod notmuch {
pub mod notmuch_backend;
pub use notmuch_backend::*;
pub mod notmuch_mbox;
pub use notmuch_mbox::*;
pub mod notmuch_envelope;
pub use notmuch_envelope::*;
}
#[cfg(feature = "notmuch-backend")]
pub use self::notmuch::*;
}
pub mod smtp {
pub mod smtp_service;
pub use smtp_service::*;
}
pub mod config {
pub mod deserialized_config;
pub use deserialized_config::*;
pub mod deserialized_account_config;
pub use deserialized_account_config::*;
pub mod config_args;
pub mod account_args;
pub mod account_handlers;
pub mod account;
pub use account::*;
pub mod account_config;
pub use account_config::*;
pub mod format;
pub use format::*;
pub mod hooks;
pub use hooks::*;
}
pub mod compl;
pub mod output;
pub mod ui;

View file

@ -1,347 +0,0 @@
use anyhow::Result;
use std::{convert::TryFrom, env};
use url::Url;
use himalaya::{
backends::Backend,
compl::{compl_args, compl_handlers},
config::{
account_args, account_handlers, config_args, AccountConfig, BackendConfig,
DeserializedConfig, DEFAULT_INBOX_FOLDER,
},
mbox::{mbox_args, mbox_handlers},
msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers},
output::{output_args, OutputFmt, StdoutPrinter},
smtp::LettreService,
};
#[cfg(feature = "imap-backend")]
use himalaya::backends::{imap_args, imap_handlers, ImapBackend};
#[cfg(feature = "maildir-backend")]
use himalaya::backends::MaildirBackend;
#[cfg(feature = "notmuch-backend")]
use himalaya::{backends::NotmuchBackend, config::MaildirBackendConfig};
fn create_app<'a>() -> clap::App<'a, 'a> {
let app = clap::App::new(env!("CARGO_PKG_NAME"))
.version(env!("CARGO_PKG_VERSION"))
.about(env!("CARGO_PKG_DESCRIPTION"))
.author(env!("CARGO_PKG_AUTHORS"))
.global_setting(clap::AppSettings::GlobalVersion)
.arg(&config_args::path_arg())
.arg(&account_args::name_arg())
.args(&output_args::args())
.arg(mbox_args::source_arg())
.subcommands(compl_args::subcmds())
.subcommands(account_args::subcmds())
.subcommands(mbox_args::subcmds())
.subcommands(msg_args::subcmds());
#[cfg(feature = "imap-backend")]
let app = app.subcommands(imap_args::subcmds());
app
}
#[allow(clippy::single_match)]
fn main() -> Result<()> {
let default_env_filter = env_logger::DEFAULT_FILTER_ENV;
env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off"));
// Check mailto command BEFORE app initialization.
let raw_args: Vec<String> = env::args().collect();
if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") {
let config = DeserializedConfig::from_opt_path(None)?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, None)?;
let mut printer = StdoutPrinter::from(OutputFmt::Plain);
let url = Url::parse(&raw_args[1])?;
let mut smtp = LettreService::from(&account_config);
#[cfg(feature = "imap-backend")]
let mut imap;
#[cfg(feature = "maildir-backend")]
let mut maildir;
#[cfg(feature = "notmuch-backend")]
let maildir_config: MaildirBackendConfig;
#[cfg(feature = "notmuch-backend")]
let mut notmuch;
let backend: Box<&mut dyn Backend> = match backend_config {
#[cfg(feature = "imap-backend")]
BackendConfig::Imap(ref imap_config) => {
imap = ImapBackend::new(&account_config, imap_config);
Box::new(&mut imap)
}
#[cfg(feature = "maildir-backend")]
BackendConfig::Maildir(ref maildir_config) => {
maildir = MaildirBackend::new(&account_config, maildir_config);
Box::new(&mut maildir)
}
#[cfg(feature = "notmuch-backend")]
BackendConfig::Notmuch(ref notmuch_config) => {
maildir_config = MaildirBackendConfig {
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
};
maildir = MaildirBackend::new(&account_config, &maildir_config);
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
Box::new(&mut notmuch)
}
};
return msg_handlers::mailto(&url, &account_config, &mut printer, backend, &mut smtp);
}
let app = create_app();
let m = app.get_matches();
// Check completion command BEFORE entities and services initialization.
// Related issue: https://github.com/soywod/himalaya/issues/115.
match compl_args::matches(&m)? {
Some(compl_args::Command::Generate(shell)) => {
return compl_handlers::generate(create_app(), shell);
}
_ => (),
}
// Init entities and services.
let config = DeserializedConfig::from_opt_path(m.value_of("config"))?;
let (account_config, backend_config) =
AccountConfig::from_config_and_opt_account_name(&config, m.value_of("account"))?;
let mbox = m
.value_of("mbox-source")
.or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str()))
.unwrap_or(DEFAULT_INBOX_FOLDER);
let mut printer = StdoutPrinter::try_from(m.value_of("output"))?;
#[cfg(feature = "imap-backend")]
let mut imap;
#[cfg(feature = "maildir-backend")]
let mut maildir;
#[cfg(feature = "notmuch-backend")]
let maildir_config: MaildirBackendConfig;
#[cfg(feature = "notmuch-backend")]
let mut notmuch;
let backend: Box<&mut dyn Backend> = match backend_config {
#[cfg(feature = "imap-backend")]
BackendConfig::Imap(ref imap_config) => {
imap = ImapBackend::new(&account_config, imap_config);
Box::new(&mut imap)
}
#[cfg(feature = "maildir-backend")]
BackendConfig::Maildir(ref maildir_config) => {
maildir = MaildirBackend::new(&account_config, maildir_config);
Box::new(&mut maildir)
}
#[cfg(feature = "notmuch-backend")]
BackendConfig::Notmuch(ref notmuch_config) => {
maildir_config = MaildirBackendConfig {
maildir_dir: notmuch_config.notmuch_database_dir.clone(),
};
maildir = MaildirBackend::new(&account_config, &maildir_config);
notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?;
Box::new(&mut notmuch)
}
};
let mut smtp = LettreService::from(&account_config);
// Check IMAP commands.
#[allow(irrefutable_let_patterns)]
#[cfg(feature = "imap-backend")]
if let BackendConfig::Imap(ref imap_config) = backend_config {
let mut imap = ImapBackend::new(&account_config, imap_config);
match imap_args::matches(&m)? {
Some(imap_args::Command::Notify(keepalive)) => {
return imap_handlers::notify(keepalive, mbox, &mut imap);
}
Some(imap_args::Command::Watch(keepalive)) => {
return imap_handlers::watch(keepalive, mbox, &mut imap);
}
_ => (),
}
}
// Check account commands.
match account_args::matches(&m)? {
Some(account_args::Cmd::List(max_width)) => {
return account_handlers::list(max_width, &config, &account_config, &mut printer);
}
_ => (),
}
// Check mailbox commands.
match mbox_args::matches(&m)? {
Some(mbox_args::Cmd::List(max_width)) => {
return mbox_handlers::list(max_width, &account_config, &mut printer, backend);
}
_ => (),
}
// Check message commands.
match msg_args::matches(&m)? {
Some(msg_args::Cmd::Attachments(seq)) => {
return msg_handlers::attachments(seq, mbox, &account_config, &mut printer, backend);
}
Some(msg_args::Cmd::Copy(seq, mbox_dst)) => {
return msg_handlers::copy(seq, mbox, mbox_dst, &mut printer, backend);
}
Some(msg_args::Cmd::Delete(seq)) => {
return msg_handlers::delete(seq, mbox, &mut printer, backend);
}
Some(msg_args::Cmd::Forward(seq, attachment_paths, encrypt)) => {
return msg_handlers::forward(
seq,
attachment_paths,
encrypt,
mbox,
&account_config,
&mut printer,
backend,
&mut smtp,
);
}
Some(msg_args::Cmd::List(max_width, page_size, page)) => {
return msg_handlers::list(
max_width,
page_size,
page,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(msg_args::Cmd::Move(seq, mbox_dst)) => {
return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend);
}
Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => {
return msg_handlers::read(
seq,
text_mime,
raw,
headers,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => {
return msg_handlers::reply(
seq,
all,
attachment_paths,
encrypt,
mbox,
&account_config,
&mut printer,
backend,
&mut smtp,
);
}
Some(msg_args::Cmd::Save(raw_msg)) => {
return msg_handlers::save(mbox, raw_msg, &mut printer, backend);
}
Some(msg_args::Cmd::Search(query, max_width, page_size, page)) => {
return msg_handlers::search(
query,
max_width,
page_size,
page,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(msg_args::Cmd::Sort(criteria, query, max_width, page_size, page)) => {
return msg_handlers::sort(
criteria,
query,
max_width,
page_size,
page,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(msg_args::Cmd::Send(raw_msg)) => {
return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp);
}
Some(msg_args::Cmd::Write(atts, encrypt)) => {
return msg_handlers::write(
atts,
encrypt,
&account_config,
&mut printer,
backend,
&mut smtp,
);
}
Some(msg_args::Cmd::Flag(m)) => match m {
Some(flag_args::Cmd::Set(seq_range, ref flags)) => {
return flag_handlers::set(seq_range, flags, mbox, &mut printer, backend);
}
Some(flag_args::Cmd::Add(seq_range, ref flags)) => {
return flag_handlers::add(seq_range, flags, mbox, &mut printer, backend);
}
Some(flag_args::Cmd::Remove(seq_range, ref flags)) => {
return flag_handlers::remove(seq_range, flags, mbox, &mut printer, backend);
}
_ => (),
},
Some(msg_args::Cmd::Tpl(m)) => match m {
Some(tpl_args::Cmd::New(tpl)) => {
return tpl_handlers::new(tpl, &account_config, &mut printer);
}
Some(tpl_args::Cmd::Reply(seq, all, tpl)) => {
return tpl_handlers::reply(
seq,
all,
tpl,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(tpl_args::Cmd::Forward(seq, tpl)) => {
return tpl_handlers::forward(
seq,
tpl,
mbox,
&account_config,
&mut printer,
backend,
);
}
Some(tpl_args::Cmd::Save(atts, tpl)) => {
return tpl_handlers::save(mbox, &account_config, atts, tpl, &mut printer, backend);
}
Some(tpl_args::Cmd::Send(atts, tpl)) => {
return tpl_handlers::send(
mbox,
&account_config,
atts,
tpl,
&mut printer,
backend,
&mut smtp,
);
}
_ => (),
},
_ => (),
}
backend.disconnect()
}

View file

@ -1,7 +0,0 @@
use std::fmt;
use crate::output::PrintTable;
pub trait Mboxes: fmt::Debug + erased_serde::Serialize + PrintTable {
//
}

View file

@ -1,136 +0,0 @@
//! Mailbox CLI module.
//!
//! This module provides subcommands, arguments and a command matcher related to the mailbox
//! domain.
use anyhow::Result;
use clap;
use log::{debug, info};
use crate::ui::table_arg;
type MaxTableWidth = Option<usize>;
/// Represents the mailbox commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
/// Represents the list mailboxes command.
List(MaxTableWidth),
}
/// Defines the mailbox command matcher.
pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
info!("entering mailbox command matcher");
if let Some(m) = m.subcommand_matches("mailboxes") {
info!("mailboxes command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
return Ok(Some(Cmd::List(max_table_width)));
}
Ok(None)
}
/// Contains mailbox subcommands.
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
vec![clap::SubCommand::with_name("mailboxes")
.aliases(&["mailbox", "mboxes", "mbox", "mb", "m"])
.about("Lists mailboxes")
.arg(table_arg::max_width())]
}
/// Defines the source mailbox argument.
pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("mbox-source")
.short("m")
.long("mailbox")
.help("Specifies the source mailbox")
.value_name("SOURCE")
}
/// Defines the target mailbox argument.
pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
clap::Arg::with_name("mbox-target")
.help("Specifies the targeted mailbox")
.value_name("TARGET")
.required(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_should_match_cmds() {
let arg = clap::App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", "mailboxes"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
let arg = clap::App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", "mailboxes", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
}
#[test]
fn it_should_match_aliases() {
macro_rules! get_matches_from {
($alias:expr) => {
clap::App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", $alias])
.subcommand_name()
};
}
assert_eq!(Some("mailboxes"), get_matches_from!["mailboxes"]);
assert_eq!(Some("mailboxes"), get_matches_from!["mboxes"]);
assert_eq!(Some("mailboxes"), get_matches_from!["mbox"]);
assert_eq!(Some("mailboxes"), get_matches_from!["mb"]);
assert_eq!(Some("mailboxes"), get_matches_from!["m"]);
}
#[test]
fn it_should_match_source_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
clap::App::new("himalaya")
.arg(source_arg())
.get_matches_from(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(None, app.value_of("mbox-source"));
let app = get_matches_from!["-m", "SOURCE"];
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
let app = get_matches_from!["--mailbox", "SOURCE"];
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
}
#[test]
fn it_should_match_target_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
clap::App::new("himalaya")
.arg(target_arg())
.get_matches_from_safe(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(
clap::ErrorKind::MissingRequiredArgument,
app.unwrap_err().kind
);
let app = get_matches_from!["TARGET"];
assert_eq!(Some("TARGET"), app.unwrap().value_of("mbox-target"));
}
}

View file

@ -1,195 +0,0 @@
//! Mailbox handling module.
//!
//! This module gathers all mailbox actions triggered by the CLI.
use anyhow::Result;
use log::{info, trace};
use crate::{
backends::Backend,
config::AccountConfig,
output::{PrintTableOpts, PrinterService},
};
/// Lists all mailboxes.
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
info!("entering list mailbox handler");
let mboxes = backend.get_mboxes()?;
trace!("mailboxes: {:?}", mboxes);
printer.print_table(
mboxes,
PrintTableOpts {
format: &config.format,
max_width,
},
)
}
#[cfg(test)]
mod tests {
use std::{fmt::Debug, io};
use termcolor::ColorSpec;
use crate::{
backends::{ImapMbox, ImapMboxAttr, ImapMboxAttrs, ImapMboxes},
mbox::Mboxes,
msg::{Envelopes, Msg},
output::{Print, PrintTable, WriteColor},
};
use super::*;
#[test]
fn it_should_list_mboxes() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl PrinterService for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_str<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn print_struct<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
impl<'a> Backend<'a> for TestBackend {
fn add_mbox(&mut self, _: &str) -> Result<()> {
unimplemented!();
}
fn get_mboxes(&mut self) -> Result<Box<dyn Mboxes>> {
Ok(Box::new(ImapMboxes {
mboxes: vec![
ImapMbox {
delim: "/".into(),
name: "INBOX".into(),
attrs: ImapMboxAttrs(vec![ImapMboxAttr::NoSelect]),
},
ImapMbox {
delim: "/".into(),
name: "Sent".into(),
attrs: ImapMboxAttrs(vec![
ImapMboxAttr::NoInferiors,
ImapMboxAttr::Custom("HasNoChildren".into()),
]),
},
],
}))
}
fn del_mbox(&mut self, _: &str) -> Result<()> {
unimplemented!();
}
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> Result<Box<dyn Envelopes>> {
unimplemented!()
}
fn search_envelopes(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> Result<Box<dyn Envelopes>> {
unimplemented!()
}
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> Result<Box<dyn ToString>> {
unimplemented!()
}
fn get_msg(&mut self, _: &str, _: &str) -> Result<Msg> {
unimplemented!()
}
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn del_msg(&mut self, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> Result<()> {
unimplemented!()
}
}
let config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
let backend = Box::new(&mut backend);
assert!(list(None, &config, &mut printer, backend).is_ok());
assert_eq!(
concat![
"\n",
"DELIM │NAME │ATTRIBUTES \n",
"/ │INBOX │NoSelect \n",
"/ │Sent │NoInferiors, HasNoChildren \n",
"\n"
],
printer.writer.content
);
}
}

View file

@ -1,64 +0,0 @@
//! Module related to email addresses.
//!
//! This module regroups email address entities and converters.
use anyhow::Result;
use mailparse;
use std::fmt::Debug;
/// Defines a single email address.
pub type Addr = mailparse::MailAddr;
/// Defines a list of email addresses.
pub type Addrs = mailparse::MailAddrList;
/// Converts a slice into an optional list of addresses.
pub fn from_slice_to_addrs<S: AsRef<str> + Debug>(addrs: S) -> Result<Option<Addrs>> {
let addrs = mailparse::addrparse(addrs.as_ref())?;
Ok(if addrs.is_empty() { None } else { Some(addrs) })
}
/// Converts a list of addresses into a list of [`lettre::message::Mailbox`].
pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result<Vec<lettre::message::Mailbox>> {
let mut sendable_addrs: Vec<lettre::message::Mailbox> = vec![];
for addr in addrs.iter() {
match addr {
Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push(
lettre::message::Mailbox::new(display_name.clone(), addr.parse()?),
),
Addr::Group(mailparse::GroupInfo { group_name, addrs }) => {
for addr in addrs {
sendable_addrs.push(lettre::message::Mailbox::new(
addr.display_name.clone().or(Some(group_name.clone())),
addr.to_string().parse()?,
))
}
}
}
}
Ok(sendable_addrs)
}
/// Converts a list of addresses into a list of [`lettre::Address`].
pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result<Vec<lettre::Address>> {
let mut sendable_addrs = vec![];
for addr in addrs.iter() {
match addr {
mailparse::MailAddr::Single(mailparse::SingleInfo {
display_name: _,
addr,
}) => {
sendable_addrs.push(addr.parse()?);
}
mailparse::MailAddr::Group(mailparse::GroupInfo {
group_name: _,
addrs,
}) => {
for addr in addrs {
sendable_addrs.push(addr.addr.parse()?);
}
}
};
}
Ok(sendable_addrs)
}

View file

@ -1,13 +0,0 @@
use std::{any, fmt};
use crate::output::PrintTable;
pub trait Envelopes: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any {
fn as_any(&self) -> &dyn any::Any;
}
impl<T: fmt::Debug + erased_serde::Serialize + PrintTable + any::Any> Envelopes for T {
fn as_any(&self) -> &dyn any::Any {
self
}
}

View file

@ -1,109 +0,0 @@
//! Message flag CLI module.
//!
//! This module provides subcommands, arguments and a command matcher related to the message flag
//! domain.
use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, info};
use crate::msg::msg_args;
type SeqRange<'a> = &'a str;
type Flags = String;
/// Represents the flag commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
/// Represents the add flags command.
Add(SeqRange<'a>, Flags),
/// Represents the set flags command.
Set(SeqRange<'a>, Flags),
/// Represents the remove flags command.
Remove(SeqRange<'a>, Flags),
}
/// Defines the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message flag command matcher");
if let Some(m) = m.subcommand_matches("add") {
info!("add subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Cmd::Add(seq_range, flags)));
}
if let Some(m) = m.subcommand_matches("set") {
info!("set subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Cmd::Set(seq_range, flags)));
}
if let Some(m) = m.subcommand_matches("remove") {
info!("remove subcommand matched");
let seq_range = m.value_of("seq-range").unwrap();
debug!("seq range: {}", seq_range);
let flags: String = m
.values_of("flags")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("flags: {:?}", flags);
return Ok(Some(Cmd::Remove(seq_range, flags)));
}
Ok(None)
}
/// Defines the flags argument.
fn flags_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("flags")
.help("IMAP flags")
.long_help("IMAP flags. Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
.value_name("FLAGS…")
.multiple(true)
.required(true)
}
/// Contains flag subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("flag")
.aliases(&["flags", "flg"])
.about("Handles flags")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("add")
.aliases(&["a"])
.about("Adds flags to a message")
.arg(msg_args::seq_range_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name("set")
.aliases(&["s", "change", "c"])
.about("Replaces all message flags")
.arg(msg_args::seq_range_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name("remove")
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
.about("Removes flags from a message")
.arg(msg_args::seq_range_arg())
.arg(flags_arg()),
)]
}

View file

@ -1,55 +0,0 @@
//! Message flag handling module.
//!
//! This module gathers all flag actions triggered by the CLI.
use anyhow::Result;
use crate::{backends::Backend, output::PrinterService};
/// Adds flags to all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.add_flags(mbox, seq_range, flags)?;
printer.print_struct(format!(
"Flag(s) {:?} successfully added to message(s) {:?}",
flags, seq_range
))
}
/// Removes flags from all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.del_flags(mbox, seq_range, flags)?;
printer.print_struct(format!(
"Flag(s) {:?} successfully removed from message(s) {:?}",
flags, seq_range
))
}
/// Replaces flags of all messages matching the given sequence range.
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq_range: &'a str,
flags: &'a str,
mbox: &'a str,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.set_flags(mbox, seq_range, flags)?;
printer.print_struct(format!(
"Flag(s) {:?} successfully set for message(s) {:?}",
flags, seq_range
))
}

View file

@ -1,472 +0,0 @@
//! Module related to message CLI.
//!
//! This module provides subcommands, arguments and a command matcher related to message.
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::{debug, info, trace};
use crate::{
mbox::mbox_args,
msg::{flag_args, msg_args, tpl_args},
ui::table_arg,
};
type Seq<'a> = &'a str;
type PageSize = usize;
type Page = usize;
type Mbox<'a> = &'a str;
type TextMime<'a> = &'a str;
type Raw = bool;
type All = bool;
type RawMsg<'a> = &'a str;
type Query = String;
type AttachmentPaths<'a> = Vec<&'a str>;
type MaxTableWidth = Option<usize>;
type Encrypt = bool;
type Criteria = String;
type Headers<'a> = Vec<&'a str>;
/// Message commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Attachments(Seq<'a>),
Copy(Seq<'a>, Mbox<'a>),
Delete(Seq<'a>),
Forward(Seq<'a>, AttachmentPaths<'a>, Encrypt),
List(MaxTableWidth, Option<PageSize>, Page),
Move(Seq<'a>, Mbox<'a>),
Read(Seq<'a>, TextMime<'a>, Raw, Headers<'a>),
Reply(Seq<'a>, All, AttachmentPaths<'a>, Encrypt),
Save(RawMsg<'a>),
Search(Query, MaxTableWidth, Option<PageSize>, Page),
Sort(Criteria, Query, MaxTableWidth, Option<PageSize>, Page),
Send(RawMsg<'a>),
Write(AttachmentPaths<'a>, Encrypt),
Flag(Option<flag_args::Cmd<'a>>),
Tpl(Option<tpl_args::Cmd<'a>>),
}
/// Message command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message command matcher");
if let Some(m) = m.subcommand_matches("attachments") {
info!("attachments command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
return Ok(Some(Cmd::Attachments(seq)));
}
if let Some(m) = m.subcommand_matches("copy") {
info!("copy command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
let mbox = m.value_of("mbox-target").unwrap();
debug!(r#"target mailbox: "{:?}""#, mbox);
return Ok(Some(Cmd::Copy(seq, mbox)));
}
if let Some(m) = m.subcommand_matches("delete") {
info!("copy command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
return Ok(Some(Cmd::Delete(seq)));
}
if let Some(m) = m.subcommand_matches("forward") {
info!("forward command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", paths);
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Cmd::Forward(seq, paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("list") {
info!("list command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: {:?}", page_size);
let page = m
.value_of("page")
.unwrap_or("1")
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default();
debug!("page: {}", page);
return Ok(Some(Cmd::List(max_table_width, page_size, page)));
}
if let Some(m) = m.subcommand_matches("move") {
info!("move command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
let mbox = m.value_of("mbox-target").unwrap();
debug!("target mailbox: {:?}", mbox);
return Ok(Some(Cmd::Move(seq, mbox)));
}
if let Some(m) = m.subcommand_matches("read") {
info!("read command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
let mime = m.value_of("mime-type").unwrap();
debug!("text mime: {}", mime);
let raw = m.is_present("raw");
debug!("raw: {}", raw);
let headers: Vec<&str> = m.values_of("headers").unwrap_or_default().collect();
debug!("headers: {:?}", headers);
return Ok(Some(Cmd::Read(seq, mime, raw, headers)));
}
if let Some(m) = m.subcommand_matches("reply") {
info!("reply command matched");
let seq = m.value_of("seq").unwrap();
debug!("seq: {}", seq);
let all = m.is_present("reply-all");
debug!("reply all: {}", all);
let paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", paths);
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Cmd::Reply(seq, all, paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("save") {
info!("save command matched");
let msg = m.value_of("message").unwrap_or_default();
trace!("message: {}", msg);
return Ok(Some(Cmd::Save(msg)));
}
if let Some(m) = m.subcommand_matches("search") {
info!("search command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: {:?}", page_size);
let page = m
.value_of("page")
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default();
debug!("page: {}", page);
let query = m
.values_of("query")
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd, escape) {
// Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string());
(true, cmds)
}
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ");
debug!("query: {}", query);
return Ok(Some(Cmd::Search(query, max_table_width, page_size, page)));
}
if let Some(m) = m.subcommand_matches("sort") {
info!("sort command matched");
let max_table_width = m
.value_of("max-table-width")
.and_then(|width| width.parse::<usize>().ok());
debug!("max table width: {:?}", max_table_width);
let page_size = m.value_of("page-size").and_then(|s| s.parse().ok());
debug!("page size: {:?}", page_size);
let page = m
.value_of("page")
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default();
debug!("page: {:?}", page);
let criteria = m
.values_of("criterion")
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ");
debug!("criteria: {:?}", criteria);
let query = m
.values_of("query")
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd, escape) {
// Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string());
(true, cmds)
}
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ");
debug!("query: {:?}", query);
return Ok(Some(Cmd::Sort(
criteria,
query,
max_table_width,
page_size,
page,
)));
}
if let Some(m) = m.subcommand_matches("send") {
info!("send command matched");
let msg = m.value_of("message").unwrap_or_default();
trace!("message: {}", msg);
return Ok(Some(Cmd::Send(msg)));
}
if let Some(m) = m.subcommand_matches("write") {
info!("write command matched");
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
debug!("attachments paths: {:?}", attachment_paths);
let encrypt = m.is_present("encrypt");
debug!("encrypt: {}", encrypt);
return Ok(Some(Cmd::Write(attachment_paths, encrypt)));
}
if let Some(m) = m.subcommand_matches("template") {
return Ok(Some(Cmd::Tpl(tpl_args::matches(m)?)));
}
if let Some(m) = m.subcommand_matches("flag") {
return Ok(Some(Cmd::Flag(flag_args::matches(m)?)));
}
info!("default list command matched");
Ok(Some(Cmd::List(None, None, 0)))
}
/// Message sequence number argument.
pub fn seq_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("seq")
.help("Specifies the targetted message")
.value_name("SEQ")
.required(true)
}
/// Message sequence range argument.
pub 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)
}
/// Message reply all argument.
pub fn reply_all_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("reply-all")
.help("Includes all recipients")
.short("A")
.long("all")
}
/// Message page size argument.
fn page_size_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("page-size")
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
}
/// Message page argument.
fn page_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("page")
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value("0")
}
/// Message attachment argument.
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("attachments")
.help("Adds attachment to the message")
.short("a")
.long("attachment")
.value_name("PATH")
.multiple(true)
}
/// Represents the message headers argument.
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("headers")
.help("Shows additional headers with the message")
.short("h")
.long("header")
.value_name("STR")
.multiple(true)
}
/// Message encrypt argument.
pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name("encrypt")
.help("Encrypts the message")
.short("e")
.long("encrypt")
}
/// Message subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![
flag_args::subcmds(),
tpl_args::subcmds(),
vec![
SubCommand::with_name("attachments")
.aliases(&["attachment", "att", "a"])
.about("Downloads all message attachments")
.arg(msg_args::seq_arg()),
SubCommand::with_name("list")
.aliases(&["lst", "l"])
.about("Lists all messages")
.arg(page_size_arg())
.arg(page_arg())
.arg(table_arg::max_width()),
SubCommand::with_name("search")
.aliases(&["s", "query", "q"])
.about("Lists messages matching the given IMAP query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table_arg::max_width())
.arg(
Arg::with_name("query")
.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")
.multiple(true)
.required(true),
),
SubCommand::with_name("sort")
.about("Sorts messages by the given criteria and matching the given IMAP query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table_arg::max_width())
.arg(
Arg::with_name("criterion")
.long("criterion")
.short("c")
.help("Defines the message sorting preferences")
.value_name("CRITERION:ORDER")
.takes_value(true)
.multiple(true)
.required(true)
.possible_values(&[
"arrival", "arrival:asc", "arrival:desc",
"cc", "cc:asc", "cc:desc",
"date", "date:asc", "date:desc",
"from", "from:asc", "from:desc",
"size", "size:asc", "size:desc",
"subject", "subject:asc", "subject:desc",
"to", "to:asc", "to:desc",
]),
)
.arg(
Arg::with_name("query")
.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")
.default_value("ALL")
.raw(true),
),
SubCommand::with_name("write")
.about("Writes a new message")
.arg(attachments_arg())
.arg(encrypt_arg()),
SubCommand::with_name("send")
.about("Sends a raw message")
.arg(Arg::with_name("message").raw(true)),
SubCommand::with_name("save")
.about("Saves a raw message")
.arg(Arg::with_name("message").raw(true)),
SubCommand::with_name("read")
.about("Reads text bodies of a message")
.arg(seq_arg())
.arg(
Arg::with_name("mime-type")
.help("MIME type to use")
.short("t")
.long("mime-type")
.value_name("MIME")
.possible_values(&["plain", "html"])
.default_value("plain"),
)
.arg(
Arg::with_name("raw")
.help("Reads raw message")
.long("raw")
.short("r"),
)
.arg(headers_arg()),
SubCommand::with_name("reply")
.aliases(&["rep", "r"])
.about("Answers to a message")
.arg(seq_arg())
.arg(reply_all_arg())
.arg(attachments_arg())
.arg(encrypt_arg()),
SubCommand::with_name("forward")
.aliases(&["fwd", "f"])
.about("Forwards a message")
.arg(seq_arg())
.arg(attachments_arg())
.arg(encrypt_arg()),
SubCommand::with_name("copy")
.aliases(&["cp", "c"])
.about("Copies a message to the targetted mailbox")
.arg(seq_arg())
.arg(mbox_args::target_arg()),
SubCommand::with_name("move")
.aliases(&["mv"])
.about("Moves a message to the targetted mailbox")
.arg(seq_arg())
.arg(mbox_args::target_arg()),
SubCommand::with_name("delete")
.aliases(&["del", "d", "remove", "rm"])
.about("Deletes a message")
.arg(seq_arg()),
],
]
.concat()
}

File diff suppressed because it is too large Load diff

View file

@ -1,379 +0,0 @@
//! Module related to message handling.
//!
//! This module gathers all message commands.
use anyhow::{Context, Result};
use atty::Stream;
use log::{debug, info, trace};
use mailparse::addrparse;
use std::{
borrow::Cow,
fs,
io::{self, BufRead},
};
use url::Url;
use crate::{
backends::Backend,
config::{AccountConfig, DEFAULT_SENT_FOLDER},
msg::{Msg, Part, Parts, TextPlainPart},
output::{PrintTableOpts, PrinterService},
smtp::SmtpService,
};
/// Downloads all message attachments to the user account downloads directory.
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let attachments = backend.get_msg(mbox, seq)?.attachments();
let attachments_len = attachments.len();
if attachments_len == 0 {
return printer.print_struct(format!("No attachment found for message {:?}", seq));
}
printer.print_str(format!(
"Found {:?} attachment{} for message {:?}",
attachments_len,
if attachments_len > 1 { "s" } else { "" },
seq
))?;
for attachment in attachments {
let file_path = config.get_download_file_path(&attachment.filename)?;
printer.print_str(format!("Downloading {:?}", file_path))?;
fs::write(&file_path, &attachment.content)
.context(format!("cannot download attachment {:?}", file_path))?;
}
printer.print_struct(format!(
"Attachment{} successfully downloaded to {:?}",
if attachments_len > 1 { "s" } else { "" },
config.downloads_dir
))
}
/// Copy a message from a mailbox to another.
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
backend.copy_msg(mbox_src, mbox_dst, seq)?;
printer.print_struct(format!(
r#"Message {} successfully copied to folder "{}""#,
seq, mbox_dst
))
}
/// Delete messages matching the given sequence range.
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox: &str,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.del_msg(mbox, seq)?;
printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq))
}
/// Forward the given message UID from the selected mailbox.
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
seq: &str,
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
backend
.get_msg(mbox, seq)?
.into_forward(config)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}
/// List paginated messages from the selected mailbox.
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
imap: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = imap.get_envelopes(mbox, page_size, page)?;
trace!("envelopes: {:?}", msgs);
printer.print_table(
msgs,
PrintTableOpts {
format: &config.format,
max_width,
},
)
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
url: &Url,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
info!("entering mailto command handler");
let to = addrparse(url.path())?;
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.to_string());
}
b"bcc" => {
bcc.push(val.to_string());
}
b"subject" => {
subject = val;
}
b"body" => {
body = val;
}
_ => (),
}
}
let msg = Msg {
from: Some(vec![config.address()?].into()),
to: if to.is_empty() { None } else { Some(to) },
cc: if cc.is_empty() {
None
} else {
Some(addrparse(&cc.join(","))?)
},
bcc: if bcc.is_empty() {
None
} else {
Some(addrparse(&bcc.join(","))?)
},
subject: subject.into(),
parts: Parts(vec![Part::TextPlain(TextPlainPart {
content: body.into(),
})]),
..Msg::default()
};
trace!("message: {:?}", msg);
msg.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}
/// Move a message from a mailbox to another.
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
mbox_src: &str,
mbox_dst: &str,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
backend.move_msg(mbox_src, mbox_dst, seq)?;
printer.print_struct(format!(
r#"Message {} successfully moved to folder "{}""#,
seq, mbox_dst
))
}
/// Read a message by its sequence number.
pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
text_mime: &str,
raw: bool,
headers: Vec<&str>,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let msg = backend.get_msg(mbox, seq)?;
printer.print_struct(if raw {
// Emails don't always have valid utf8. Using "lossy" to display what we can.
String::from_utf8_lossy(&msg.raw).into_owned()
} else {
msg.to_readable_string(text_mime, headers, config)?
})
}
/// Reply to the given message UID.
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
seq: &str,
all: bool,
attachments_paths: Vec<&str>,
encrypt: bool,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
backend
.get_msg(mbox, seq)?
.into_reply(all, config)?
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?
.add_flags(mbox, seq, "replied")
}
/// Saves a raw message to the targetted mailbox.
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
mbox: &str,
raw_msg: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
info!("entering save message handler");
debug!("mailbox: {}", mbox);
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
Ok(())
}
/// Paginate messages from the selected mailbox matching the specified query.
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
trace!("messages: {:#?}", msgs);
printer.print_table(
msgs,
PrintTableOpts {
format: &config.format,
max_width,
},
)
}
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
sort: String,
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
mbox: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.default_page_size);
debug!("page size: {}", page_size);
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
trace!("envelopes: {:#?}", msgs);
printer.print_table(
msgs,
PrintTableOpts {
format: &config.format,
max_width,
},
)
}
/// Send a raw message.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
raw_msg: &str,
config: &AccountConfig,
printer: &mut P,
backend: Box<&mut B>,
smtp: &mut S,
) -> Result<()> {
info!("entering send message handler");
let is_tty = atty::is(Stream::Stdin);
debug!("is tty: {}", is_tty);
let is_json = printer.is_json();
debug!("is json: {}", is_json);
let sent_folder = config
.mailboxes
.get("sent")
.map(|s| s.as_str())
.unwrap_or(DEFAULT_SENT_FOLDER);
debug!("sent folder: {:?}", sent_folder);
let raw_msg = if is_tty || is_json {
raw_msg.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
trace!("raw message: {:?}", raw_msg);
let msg = Msg::from_tpl(&raw_msg)?;
smtp.send(&config, &msg)?;
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
Ok(())
}
/// Compose a new message.
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
attachments_paths: Vec<&str>,
encrypt: bool,
config: &AccountConfig,
printer: &mut P,
backend: Box<&'a mut B>,
smtp: &mut S,
) -> Result<()> {
Msg::default()
.add_attachments(attachments_paths)?
.encrypt(encrypt)
.edit_with_editor(config, printer, backend, smtp)?;
Ok(())
}

View file

@ -1,15 +0,0 @@
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.eml");
trace!("local draft path: {:?}", path);
path
}
pub fn remove_local_draft() -> Result<()> {
let path = local_draft_path();
debug!("remove draft path at {:?}", path);
fs::remove_file(&path).context(format!("cannot remove local draft at {:?}", path))
}

View file

@ -1,146 +0,0 @@
use anyhow::{anyhow, Context, Result};
use mailparse::MailHeaderMap;
use serde::Serialize;
use std::{
env, fs,
ops::{Deref, DerefMut},
};
use uuid::Uuid;
use crate::config::AccountConfig;
#[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),
}
impl Part {
pub fn new_text_plain(content: String) -> Self {
Self::TextPlain(TextPlainPart { content })
}
}
#[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| !matches!(part, Part::TextPlain(_)));
self.push(Part::TextPlain(part));
}
pub fn from_parsed_mail<'a>(
account: &'a AccountConfig,
part: &'a mailparse::ParsedMail<'a>,
) -> Result<Self> {
let mut parts = vec![];
build_parts_map_rec(account, part, &mut parts)?;
Ok(Self(parts))
}
}
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
}
}
fn build_parts_map_rec(
account: &AccountConfig,
parsed_mail: &mailparse::ParsedMail,
parts: &mut Vec<Part>,
) -> Result<()> {
if parsed_mail.subparts.is_empty() {
let cdisp = parsed_mail.get_content_disposition();
match cdisp.disposition {
mailparse::DispositionType::Attachment => {
let filename = cdisp
.params
.get("filename")
.map(String::from)
.unwrap_or_else(|| String::from("noname"));
let content = parsed_mail.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
_ => {
if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") {
let content = parsed_mail.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 {
let ctype = parsed_mail
.get_headers()
.get_first_value("content-type")
.ok_or_else(|| anyhow!("cannot get content type of multipart"))?;
if ctype.starts_with("multipart/encrypted") {
let decrypted_part = parsed_mail
.subparts
.get(1)
.ok_or_else(|| anyhow!("cannot find encrypted part of multipart"))
.and_then(|part| decrypt_part(account, part))
.context("cannot decrypt part of multipart")?;
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
.context("cannot parse decrypted part of multipart")?;
build_parts_map_rec(account, &parsed_mail, parts)?;
} else {
for part in parsed_mail.subparts.iter() {
build_parts_map_rec(account, part, parts)?;
}
}
}
Ok(())
}
fn decrypt_part(account: &AccountConfig, msg: &mailparse::ParsedMail) -> Result<String> {
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
let msg_body = msg
.get_body()
.context("cannot get body from encrypted part")?;
fs::write(msg_path.clone(), &msg_body)
.context(format!("cannot write encrypted part to temporary file"))?;
account
.pgp_decrypt_file(msg_path.clone())?
.ok_or_else(|| anyhow!("cannot find pgp decrypt command in config"))
}

View file

@ -1,195 +0,0 @@
//! Module related to message template CLI.
//!
//! This module provides subcommands, arguments and a command matcher related to message template.
use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, info, trace};
use crate::msg::msg_args;
type Seq<'a> = &'a str;
type ReplyAll = bool;
type AttachmentPaths<'a> = Vec<&'a str>;
type Tpl<'a> = &'a str;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct TplOverride<'a> {
pub subject: Option<&'a str>,
pub from: Option<Vec<&'a str>>,
pub to: Option<Vec<&'a str>>,
pub cc: Option<Vec<&'a str>>,
pub bcc: Option<Vec<&'a str>>,
pub headers: Option<Vec<&'a str>>,
pub body: Option<&'a str>,
pub sig: Option<&'a str>,
}
impl<'a> From<&'a ArgMatches<'a>> for TplOverride<'a> {
fn from(matches: &'a ArgMatches<'a>) -> Self {
Self {
subject: matches.value_of("subject"),
from: matches.values_of("from").map(|v| v.collect()),
to: matches.values_of("to").map(|v| v.collect()),
cc: matches.values_of("cc").map(|v| v.collect()),
bcc: matches.values_of("bcc").map(|v| v.collect()),
headers: matches.values_of("headers").map(|v| v.collect()),
body: matches.value_of("body"),
sig: matches.value_of("signature"),
}
}
}
/// Message template commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
New(TplOverride<'a>),
Reply(Seq<'a>, ReplyAll, TplOverride<'a>),
Forward(Seq<'a>, TplOverride<'a>),
Save(AttachmentPaths<'a>, Tpl<'a>),
Send(AttachmentPaths<'a>, Tpl<'a>),
}
/// Message template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
info!("entering message template command matcher");
if let Some(m) = m.subcommand_matches("new") {
info!("new subcommand matched");
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Cmd::New(tpl)));
}
if let Some(m) = m.subcommand_matches("reply") {
info!("reply subcommand matched");
let seq = m.value_of("seq").unwrap();
debug!("sequence: {}", seq);
let all = m.is_present("reply-all");
debug!("reply all: {}", all);
let tpl = TplOverride::from(m);
trace!("template override: {:?}", tpl);
return Ok(Some(Cmd::Reply(seq, all, tpl)));
}
if let Some(m) = m.subcommand_matches("forward") {
info!("forward subcommand matched");
let seq = m.value_of("seq").unwrap();
debug!("sequence: {}", seq);
let tpl = TplOverride::from(m);
trace!("template args: {:?}", tpl);
return Ok(Some(Cmd::Forward(seq, tpl)));
}
if let Some(m) = m.subcommand_matches("save") {
info!("save subcommand matched");
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
trace!("attachments paths: {:?}", attachment_paths);
let tpl = m.value_of("template").unwrap_or_default();
trace!("template: {}", tpl);
return Ok(Some(Cmd::Save(attachment_paths, tpl)));
}
if let Some(m) = m.subcommand_matches("send") {
info!("send subcommand matched");
let attachment_paths: Vec<&str> = m.values_of("attachments").unwrap_or_default().collect();
trace!("attachments paths: {:?}", attachment_paths);
let tpl = m.value_of("template").unwrap_or_default();
trace!("template: {}", tpl);
return Ok(Some(Cmd::Send(attachment_paths, tpl)));
}
Ok(None)
}
/// Message template args.
pub fn tpl_args<'a>() -> Vec<Arg<'a, 'a>> {
vec![
Arg::with_name("subject")
.help("Overrides the Subject header")
.short("s")
.long("subject")
.value_name("STRING"),
Arg::with_name("from")
.help("Overrides the From header")
.short("f")
.long("from")
.value_name("ADDR")
.multiple(true),
Arg::with_name("to")
.help("Overrides the To header")
.short("t")
.long("to")
.value_name("ADDR")
.multiple(true),
Arg::with_name("cc")
.help("Overrides the Cc header")
.short("c")
.long("cc")
.value_name("ADDR")
.multiple(true),
Arg::with_name("bcc")
.help("Overrides the Bcc header")
.short("b")
.long("bcc")
.value_name("ADDR")
.multiple(true),
Arg::with_name("header")
.help("Overrides a specific header")
.short("h")
.long("header")
.value_name("KEY: VAL")
.multiple(true),
Arg::with_name("body")
.help("Overrides the body")
.short("B")
.long("body")
.value_name("STRING"),
Arg::with_name("signature")
.help("Overrides the signature")
.short("S")
.long("signature")
.value_name("STRING"),
]
}
/// Message template subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name("template")
.aliases(&["tpl"])
.about("Generates a message template")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name("new")
.aliases(&["n"])
.about("Generates a new message template")
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("reply")
.aliases(&["rep", "re", "r"])
.about("Generates a reply message template")
.arg(msg_args::seq_arg())
.arg(msg_args::reply_all_arg())
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("forward")
.aliases(&["fwd", "fw", "f"])
.about("Generates a forward message template")
.arg(msg_args::seq_arg())
.args(&tpl_args()),
)
.subcommand(
SubCommand::with_name("save")
.about("Saves a message based on the given template")
.arg(&msg_args::attachments_arg())
.arg(Arg::with_name("template").raw(true)),
)
.subcommand(
SubCommand::with_name("send")
.about("Sends a message based on the given template")
.arg(&msg_args::attachments_arg())
.arg(Arg::with_name("template").raw(true)),
)]
}

View file

@ -1,109 +0,0 @@
//! Module related to message template handling.
//!
//! This module gathers all message template commands.
use anyhow::Result;
use atty::Stream;
use std::io::{self, BufRead};
use crate::{
backends::Backend,
config::AccountConfig,
msg::{Msg, TplOverride},
output::PrinterService,
smtp::SmtpService,
};
/// Generate a new message template.
pub fn new<'a, P: PrinterService>(
opts: TplOverride<'a>,
account: &'a AccountConfig,
printer: &'a mut P,
) -> Result<()> {
let tpl = Msg::default().to_tpl(opts, account)?;
printer.print_struct(tpl)
}
/// Generate a reply message template.
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
all: bool,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let tpl = backend
.get_msg(mbox, seq)?
.into_reply(all, config)?
.to_tpl(opts, config)?;
printer.print_struct(tpl)
}
/// Generate a forward message template.
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
seq: &str,
opts: TplOverride<'a>,
mbox: &str,
config: &'a AccountConfig,
printer: &'a mut P,
backend: Box<&'a mut B>,
) -> Result<()> {
let tpl = backend
.get_msg(mbox, seq)?
.into_forward(config)?
.to_tpl(opts, config)?;
printer.print_struct(tpl)
}
/// Saves a message based on a template.
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
mbox: &str,
config: &AccountConfig,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,
backend: Box<&mut B>,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
let raw_msg = msg.into_sendable_msg(config)?.formatted();
backend.add_msg(mbox, &raw_msg, "seen")?;
printer.print_struct("Template successfully saved")
}
/// Sends a message based on a template.
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
mbox: &str,
account: &AccountConfig,
attachments_paths: Vec<&str>,
tpl: &str,
printer: &mut P,
backend: Box<&mut B>,
smtp: &mut S,
) -> Result<()> {
let tpl = if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
};
let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?;
let sent_msg = smtp.send(account, &msg)?;
backend.add_msg(mbox, &sent_msg, "seen")?;
printer.print_struct("Template successfully sent")
}

View file

@ -1,18 +0,0 @@
//! Module related to output formatting and printing.
pub mod output_args;
pub mod output_utils;
pub use output_utils::*;
pub mod output_entity;
pub use output_entity::*;
pub mod print;
pub use print::*;
pub mod print_table;
pub use print_table::*;
pub mod printer_service;
pub use printer_service::*;

View file

@ -1,26 +0,0 @@
//! Module related to output CLI.
//!
//! This module provides arguments related to output.
use clap::Arg;
/// Output arguments.
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
vec![
Arg::with_name("output")
.long("output")
.short("o")
.help("Defines the output format")
.value_name("FMT")
.possible_values(&["plain", "json"])
.default_value("plain"),
Arg::with_name("log-level")
.long("log-level")
.alias("log")
.short("l")
.help("Defines the logs level")
.value_name("LEVEL")
.possible_values(&["error", "warn", "info", "debug", "trace"])
.default_value("info"),
]
}

View file

@ -1,53 +0,0 @@
use anyhow::{anyhow, Error, Result};
use std::{convert::TryFrom, fmt};
/// Represents the available output formats.
#[derive(Debug, PartialEq)]
pub enum OutputFmt {
Plain,
Json,
}
impl From<&str> for OutputFmt {
fn from(fmt: &str) -> Self {
match fmt {
slice if slice.eq_ignore_ascii_case("json") => Self::Json,
_ => Self::Plain,
}
}
}
impl TryFrom<Option<&str>> for OutputFmt {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self, Self::Error> {
match fmt {
Some(fmt) if fmt.eq_ignore_ascii_case("json") => Ok(Self::Json),
Some(fmt) if fmt.eq_ignore_ascii_case("plain") => Ok(Self::Plain),
None => Ok(Self::Plain),
Some(fmt) => Err(anyhow!(r#"cannot parse output format "{}""#, fmt)),
}
}
}
impl fmt::Display for OutputFmt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let fmt = match *self {
OutputFmt::Json => "JSON",
OutputFmt::Plain => "Plain",
};
write!(f, "{}", fmt)
}
}
/// Defines a struct-wrapper to provide a JSON output.
#[derive(Debug, Clone, serde::Serialize)]
pub struct OutputJson<T: serde::Serialize> {
response: T,
}
impl<T: serde::Serialize> OutputJson<T> {
pub fn new(response: T) -> Self {
Self { response }
}
}

View file

@ -1,41 +0,0 @@
use anyhow::{anyhow, Context, Result};
use log::debug;
use std::{
io::prelude::*,
process::{Command, Stdio},
};
/// TODO: move this in a more approriate place.
pub fn run_cmd(cmd: &str) -> Result<String> {
debug!("running command: {}", cmd);
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)?)
}
pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result<Vec<u8>> {
let mut res = Vec::new();
let process = Command::new(cmd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.with_context(|| format!("cannot spawn process from command {:?}", cmd))?;
process
.stdin
.ok_or_else(|| anyhow!("cannot get stdin"))?
.write_all(data)
.with_context(|| "cannot write data to stdin")?;
process
.stdout
.ok_or_else(|| anyhow!("cannot get stdout"))?
.read_to_end(&mut res)
.with_context(|| "cannot read data from stdout")?;
Ok(res)
}

View file

@ -1,19 +0,0 @@
use anyhow::{Context, Result};
use crate::output::WriteColor;
pub trait Print {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()>;
}
impl Print for &str {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
writeln!(writer, "{}", self).context("cannot write string to writer")
}
}
impl Print for String {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
self.as_str().print(writer)
}
}

View file

@ -1,18 +0,0 @@
use anyhow::Result;
use std::io;
use termcolor::{self, StandardStream};
use crate::config::Format;
pub trait WriteColor: io::Write + termcolor::WriteColor {}
impl WriteColor for StandardStream {}
pub trait PrintTable {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()>;
}
pub struct PrintTableOpts<'a> {
pub format: &'a Format,
pub max_width: Option<usize>,
}

View file

@ -1,92 +0,0 @@
use anyhow::{Context, Error, Result};
use atty::Stream;
use std::{
convert::TryFrom,
fmt::{self, Debug},
};
use termcolor::{ColorChoice, StandardStream};
use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor};
pub trait PrinterService {
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()>;
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()>;
fn print_table<T: Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()>;
fn is_json(&self) -> bool;
}
pub struct StdoutPrinter {
pub writer: Box<dyn WriteColor>,
pub fmt: OutputFmt,
}
impl PrinterService for StdoutPrinter {
fn print_str<T: Debug + Print>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => Ok(()),
}
}
fn print_struct<T: Debug + Print + serde::Serialize>(&mut self, data: T) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print(self.writer.as_mut()),
OutputFmt::Json => serde_json::to_writer(self.writer.as_mut(), &OutputJson::new(data))
.context("cannot write JSON to writer"),
}
}
fn print_table<T: fmt::Debug + erased_serde::Serialize + PrintTable + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> Result<()> {
match self.fmt {
OutputFmt::Plain => data.print_table(self.writer.as_mut(), opts),
OutputFmt::Json => {
let json = &mut serde_json::Serializer::new(self.writer.as_mut());
let ser = &mut <dyn erased_serde::Serializer>::erase(json);
data.erased_serialize(ser).unwrap();
Ok(())
}
}
}
fn is_json(&self) -> bool {
self.fmt == OutputFmt::Json
}
}
impl From<OutputFmt> for StdoutPrinter {
fn from(fmt: OutputFmt) -> Self {
let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) {
// Colors should be deactivated if the terminal is not a tty.
ColorChoice::Never
} else {
// Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]:
// - If `NO_COLOR` is set to any value, then colors will be suppressed.
// - If `TERM` is set to dumb, then colors will be suppressed.
// - In non-Windows environments, if `TERM` is not set, then colors will be suppressed.
//
// [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection
ColorChoice::Auto
});
let writer = Box::new(writer);
Self { writer, fmt }
}
}
impl TryFrom<Option<&str>> for StdoutPrinter {
type Error = Error;
fn try_from(fmt: Option<&str>) -> Result<Self> {
Ok(Self {
fmt: OutputFmt::try_from(fmt)?,
..Self::from(OutputFmt::Plain)
})
}
}

View file

@ -1 +0,0 @@
//! Module related to SMTP.

View file

@ -1,85 +0,0 @@
use anyhow::{Context, Result};
use lettre::{
self,
transport::smtp::{
client::{Tls, TlsParameters},
SmtpTransport,
},
Transport,
};
use std::convert::TryInto;
use crate::{config::AccountConfig, msg::Msg, output::pipe_cmd};
pub trait SmtpService {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>>;
}
pub struct LettreService<'a> {
account: &'a AccountConfig,
transport: Option<SmtpTransport>,
}
impl LettreService<'_> {
fn transport(&mut self) -> Result<&SmtpTransport> {
if let Some(ref transport) = self.transport {
Ok(transport)
} else {
let builder = if self.account.smtp_starttls {
SmtpTransport::starttls_relay(&self.account.smtp_host)
} else {
SmtpTransport::relay(&self.account.smtp_host)
}?;
let tls = TlsParameters::builder(self.account.smtp_host.to_owned())
.dangerous_accept_invalid_hostnames(self.account.smtp_insecure)
.dangerous_accept_invalid_certs(self.account.smtp_insecure)
.build()?;
let tls = if self.account.smtp_starttls {
Tls::Required(tls)
} else {
Tls::Wrapper(tls)
};
self.transport = Some(
builder
.tls(tls)
.port(self.account.smtp_port)
.credentials(self.account.smtp_creds()?)
.build(),
);
Ok(self.transport.as_ref().unwrap())
}
}
}
impl SmtpService for LettreService<'_> {
fn send(&mut self, account: &AccountConfig, msg: &Msg) -> Result<Vec<u8>> {
let mut raw_msg = msg.into_sendable_msg(account)?.formatted();
let envelope: lettre::address::Envelope =
if let Some(cmd) = account.hooks.pre_send.as_deref() {
for cmd in cmd.split('|') {
raw_msg = pipe_cmd(cmd.trim(), &raw_msg)
.with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?
}
let parsed_mail = mailparse::parse_mail(&raw_msg)?;
Msg::from_parsed_mail(parsed_mail, account)?.try_into()
} else {
msg.try_into()
}?;
self.transport()?.send_raw(&envelope, &raw_msg)?;
Ok(raw_msg)
}
}
impl<'a> From<&'a AccountConfig> for LettreService<'a> {
fn from(account: &'a AccountConfig) -> Self {
Self {
account,
transport: None,
}
}
}

View file

@ -1,92 +0,0 @@
use anyhow::{anyhow, Context, Result};
use log::{debug, error};
use std::io::{self, Write};
pub enum PreEditChoice {
Edit,
Discard,
Quit,
}
pub fn pre_edit() -> Result<PreEditChoice> {
println!("A draft was found:");
print!("(e)dit, (d)iscard or (q)uit? ");
io::stdout().flush().context("cannot flush stdout")?;
let mut buf = String::new();
io::stdin()
.read_line(&mut buf)
.context("cannot read stdin")?;
match buf.bytes().next().map(|bytes| bytes as char) {
Some('e') => {
debug!("edit choice matched");
Ok(PreEditChoice::Edit)
}
Some('d') => {
debug!("discard choice matched");
Ok(PreEditChoice::Discard)
}
Some('q') => {
debug!("quit choice matched");
Ok(PreEditChoice::Quit)
}
Some(choice) => {
error!(r#"invalid choice "{}""#, choice);
Err(anyhow!(r#"invalid choice "{}""#, choice))
}
None => {
error!("empty choice");
Err(anyhow!("empty choice"))
}
}
}
pub enum PostEditChoice {
Send,
Edit,
LocalDraft,
RemoteDraft,
Discard,
}
pub fn post_edit() -> Result<PostEditChoice> {
print!("(s)end, (e)dit, (l)ocal/(r)emote draft or (d)iscard? ");
io::stdout().flush().context("cannot flush stdout")?;
let mut buf = String::new();
io::stdin()
.read_line(&mut buf)
.context("cannot read stdin")?;
match buf.bytes().next().map(|bytes| bytes as char) {
Some('s') => {
debug!("send choice matched");
Ok(PostEditChoice::Send)
}
Some('l') => {
debug!("save local draft choice matched");
Ok(PostEditChoice::LocalDraft)
}
Some('r') => {
debug!("save remote draft matched");
Ok(PostEditChoice::RemoteDraft)
}
Some('e') => {
debug!("edit choice matched");
Ok(PostEditChoice::Edit)
}
Some('d') => {
debug!("discard choice matched");
Ok(PostEditChoice::Discard)
}
Some(choice) => {
error!(r#"invalid choice "{}""#, choice);
Err(anyhow!(r#"invalid choice "{}""#, choice))
}
None => {
error!("empty choice");
Err(anyhow!("empty choice"))
}
}
}

View file

@ -1,31 +0,0 @@
use anyhow::{Context, Result};
use log::debug;
use std::{env, fs, process::Command};
use crate::msg::msg_utils;
pub fn open_with_tpl(tpl: String) -> Result<String> {
let path = msg_utils::local_draft_path();
debug!("create draft");
fs::write(&path, tpl.as_bytes()).context(format!("cannot write local draft at {:?}", path))?;
debug!("open editor");
Command::new(env::var("EDITOR").context(r#"cannot find "$EDITOR" env var"#)?)
.arg(&path)
.status()
.context("cannot launch editor")?;
debug!("read draft");
let content =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
Ok(content)
}
pub fn open_with_draft() -> Result<String> {
let path = msg_utils::local_draft_path();
let tpl =
fs::read_to_string(&path).context(format!("cannot read local draft at {:?}", path))?;
open_with_tpl(tpl)
}

View file

@ -1,9 +0,0 @@
//! Module related to User Interface.
pub mod table_arg;
pub mod table;
pub use table::*;
pub mod choice;
pub mod editor;

View file

@ -1,445 +0,0 @@
//! Toolbox for building responsive tables.
//! A table is composed of rows, a row is composed of cells.
//! The toolbox uses the [builder design pattern].
//!
//! [builder design pattern]: https://refactoring.guru/design-patterns/builder
use anyhow::{Context, Result};
use log::trace;
use termcolor::{Color, ColorSpec};
use terminal_size;
use unicode_width::UnicodeWidthStr;
use crate::{
config::Format,
output::{Print, PrintTableOpts, WriteColor},
};
/// Defines the default terminal size.
/// This is used when the size cannot be determined by the `terminal_size` crate.
/// TODO: make this customizable.
pub const DEFAULT_TERM_WIDTH: usize = 80;
/// Defines the minimum size of a shrinked cell.
/// TODO: make this customizable.
pub const MAX_SHRINK_WIDTH: usize = 5;
/// Represents a cell in a table.
#[derive(Debug, Default)]
pub struct Cell {
/// Represents the style of the cell.
style: ColorSpec,
/// Represents the content of the cell.
value: String,
/// (Dis)allowes the cell to shrink when the table exceeds the container width.
shrinkable: bool,
}
impl Cell {
pub fn new<T: AsRef<str>>(value: T) -> Self {
Self {
// Removes carriage returns, new line feeds, tabulations
// and [variation selectors].
//
// [variation selectors]: https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)
value: String::from(value.as_ref()).replace(
|c| ['\r', '\n', '\t', '\u{fe0e}', '\u{fe0f}'].contains(&c),
"",
),
..Self::default()
}
}
/// Returns the unicode width of the cell's value.
pub fn unicode_width(&self) -> usize {
UnicodeWidthStr::width(self.value.as_str())
}
/// Makes the cell shrinkable. If the table exceeds the terminal width, this cell will be the
/// one to shrink in order to prevent the table to overflow.
pub fn shrinkable(mut self) -> Self {
self.shrinkable = true;
self
}
/// Returns the shrinkable state of a cell.
pub fn is_shrinkable(&self) -> bool {
self.shrinkable
}
/// Applies the bold style to the cell.
pub fn bold(mut self) -> Self {
self.style.set_bold(true);
self
}
/// Applies the bold style to the cell conditionally.
pub fn bold_if(self, predicate: bool) -> Self {
if predicate {
self.bold()
} else {
self
}
}
/// Applies the underline style to the cell.
pub fn underline(mut self) -> Self {
self.style.set_underline(true);
self
}
/// Applies the red color to the cell.
pub fn red(mut self) -> Self {
self.style.set_fg(Some(Color::Red));
self
}
/// Applies the green color to the cell.
pub fn green(mut self) -> Self {
self.style.set_fg(Some(Color::Green));
self
}
/// Applies the yellow color to the cell.
pub fn yellow(mut self) -> Self {
self.style.set_fg(Some(Color::Yellow));
self
}
/// Applies the blue color to the cell.
pub fn blue(mut self) -> Self {
self.style.set_fg(Some(Color::Blue));
self
}
/// Applies the white color to the cell.
pub fn white(mut self) -> Self {
self.style.set_fg(Some(Color::White));
self
}
/// Applies the custom ansi color to the cell.
pub fn ansi_256(mut self, code: u8) -> Self {
self.style.set_fg(Some(Color::Ansi256(code)));
self
}
}
/// Makes the cell printable.
impl Print for Cell {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
// Applies colors to the cell
writer
.set_color(&self.style)
.context(format!(r#"cannot apply colors to cell "{}""#, self.value))?;
// Writes the colorized cell to stdout
write!(writer, "{}", self.value).context(format!(r#"cannot print cell "{}""#, self.value))
}
}
/// Represents a row in a table.
#[derive(Debug, Default)]
pub struct Row(
/// Represents a list of cells.
pub Vec<Cell>,
);
impl Row {
pub fn new() -> Self {
Self::default()
}
pub fn cell(mut self, cell: Cell) -> Self {
self.0.push(cell);
self
}
}
/// Represents a table abstraction.
pub trait Table
where
Self: Sized,
{
/// Defines the header row.
fn head() -> Row;
/// Defines the row template.
fn row(&self) -> Row;
/// Writes the table to the writer.
fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> {
let is_format_flowed = matches!(opts.format, Format::Flowed);
let max_width = match opts.format {
Format::Fixed(width) => opts.max_width.unwrap_or(*width),
Format::Flowed => 0,
Format::Auto => opts
.max_width
.or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize))
.unwrap_or(DEFAULT_TERM_WIDTH),
};
let mut table = vec![Self::head()];
let mut cell_widths: Vec<usize> =
table[0].0.iter().map(|cell| cell.unicode_width()).collect();
table.extend(
items
.iter()
.map(|item| {
let row = item.row();
row.0.iter().enumerate().for_each(|(i, cell)| {
cell_widths[i] = cell_widths[i].max(cell.unicode_width());
});
row
})
.collect::<Vec<_>>(),
);
trace!("cell widths: {:?}", cell_widths);
let spaces_plus_separators_len = cell_widths.len() * 2 - 1;
let table_width = cell_widths.iter().sum::<usize>() + spaces_plus_separators_len;
trace!("table width: {}", table_width);
for row in table.iter_mut() {
let mut glue = Cell::default();
for (i, cell) in row.0.iter_mut().enumerate() {
glue.print(writer)?;
let table_is_overflowing = table_width > max_width;
if table_is_overflowing && !is_format_flowed && cell.is_shrinkable() {
trace!("table is overflowing and cell is shrinkable");
let shrink_width = table_width - max_width;
trace!("shrink width: {}", shrink_width);
let cell_width = if shrink_width + MAX_SHRINK_WIDTH < cell_widths[i] {
cell_widths[i] - shrink_width
} else {
MAX_SHRINK_WIDTH
};
trace!("cell width: {}", cell_width);
trace!("cell unicode width: {}", cell.unicode_width());
let cell_is_overflowing = cell.unicode_width() > cell_width;
if cell_is_overflowing {
trace!("cell is overflowing");
let mut value = String::new();
let mut chars_width = 0;
for c in cell.value.chars() {
let char_width = UnicodeWidthStr::width(c.to_string().as_str());
if chars_width + char_width >= cell_width {
break;
}
chars_width += char_width;
value.push(c);
}
value.push_str("");
trace!("chars width: {}", chars_width);
trace!("shrinked value: {}", value);
let spaces_count = cell_width - chars_width - 1;
trace!("number of spaces added to shrinked value: {}", spaces_count);
value.push_str(&" ".repeat(spaces_count));
cell.value = value;
} else {
trace!("cell is not overflowing");
let spaces_count = cell_width - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
} else {
trace!("table is not overflowing or cell is not shrinkable");
trace!("cell width: {}", cell_widths[i]);
trace!("cell unicode width: {}", cell.unicode_width());
let spaces_count = cell_widths[i] - cell.unicode_width() + 1;
trace!("number of spaces added to value: {}", spaces_count);
cell.value.push_str(&" ".repeat(spaces_count));
}
cell.print(writer)?;
glue = Cell::new("").ansi_256(8);
}
writeln!(writer)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io;
use super::*;
#[derive(Debug, Default)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
struct Item {
id: u16,
name: String,
desc: String,
}
impl<'a> Item {
pub fn new(id: u16, name: &'a str, desc: &'a str) -> Self {
Self {
id,
name: String::from(name),
desc: String::from(desc),
}
}
}
impl Table for Item {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID"))
.cell(Cell::new("NAME").shrinkable())
.cell(Cell::new("DESC"))
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(self.id.to_string()))
.cell(Cell::new(self.name.as_str()).shrinkable())
.cell(Cell::new(self.desc.as_str()))
}
}
macro_rules! write_items {
($writer:expr, $($item:expr),*) => {
Table::print($writer, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap();
};
}
#[test]
fn row_smaller_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2, "b", "bb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2 │b │bb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn row_bigger_than_head() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "c", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │c │cc \n",
];
assert_eq!(expected, writer.content);
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "a", "aa"),
Item::new(2222, "bbbbb", "bbbbb"),
Item::new(3, "cccccc", "cc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │a │aa \n",
"2222 │bbbbb │bbbbb \n",
"3 │cccccc │cc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn basic_shrink() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1, "", "desc"),
Item::new(2, "short", "desc"),
Item::new(3, "loooooong", "desc"),
Item::new(4, "shriiiiink", "desc"),
Item::new(5, "shriiiiiiiiiink", "desc"),
Item::new(6, "😍😍😍😍", "desc"),
Item::new(7, "😍😍😍😍😍", "desc"),
Item::new(8, "!😍😍😍😍😍", "desc")
];
let expected = concat![
"ID │NAME │DESC \n",
"1 │ │desc \n",
"2 │short │desc \n",
"3 │loooooong │desc \n",
"4 │shriiiii… │desc \n",
"5 │shriiiii… │desc \n",
"6 │😍😍😍😍 │desc \n",
"7 │😍😍😍😍… │desc \n",
"8 │!😍😍😍… │desc \n",
];
assert_eq!(expected, writer.content);
}
#[test]
fn max_shrink_width() {
let mut writer = StringWriter::default();
write_items![
&mut writer,
Item::new(1111, "shriiiiiiiink", "desc very looong"),
Item::new(2222, "shriiiiiiiink", "desc very loooooooooong")
];
let expected = concat![
"ID │NAME │DESC \n",
"1111 │shri… │desc very looong \n",
"2222 │shri… │desc very loooooooooong \n",
];
assert_eq!(expected, writer.content);
}
}

View file

@ -1,10 +0,0 @@
use clap::Arg;
/// Defines the max table width argument.
pub fn max_width<'a>() -> Arg<'a, 'a> {
Arg::with_name("max-table-width")
.help("Defines a maximum width for the table")
.short("w")
.long("max-width")
.value_name("INT")
}

651
config.sample.toml Normal file
View file

@ -0,0 +1,651 @@
################################################################################
###[ Global configuration ]#####################################################
################################################################################
# Default display name for all accounts. It is used to build the full
# email address of an account: "Example" <example@localhost>
#
display-name = "Example"
# Default signature for all accounts. The signature is put at the
# bottom of all messages. It can be a path or a string. Supports TOML
# multilines.
#
#signature = "/path/to/signature/file"
#signature = """
# Thanks you,
# Regards
#"""
signature = "Regards,\n"
# Default signature delimiter for all accounts. It delimits the end of
# the message body from the signature.
#
signature-delim = "-- \n"
# Default downloads directory path for all accounts. It is mostly used
# for downloading attachments. Defaults to the system temporary
# directory.
#
downloads-dir = "~/Downloads"
# Customizes the charset used to build the accounts listing
# table. Defaults to markdown table style.
#
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
#
account.list.table.preset = "|| |-||| "
# Customizes the color of the NAME column of the account listing
# table.
#
account.list.table.name-color = "green"
# Customizes the color of the BACKENDS column of the account listing
# table.
#
account.list.table.backends-color = "blue"
# Customizes the color of the DEFAULT column of the account listing
# table.
#
account.list.table.default-color = "black"
################################################################################
###[ Account configuration ]####################################################
################################################################################
# The account name should be unique.
#
[accounts.example]
# Defaultness of the account. The current account will be used by
# default in all commands.
#
default = true
# The email address associated to the current account.
#
email = "example@localhost"
# The display name of the account. This and the email are used to
# build the full email address: "Example" <example@localhost>
#
display-name = "Example"
# The signature put at the bottom of composed messages. It can be a
# path or a string. Supports TOML multilines.
#
#signature = "/path/to/signature/file"
#signature = """
# Thanks you,
# Regards
#"""
signature = "Regards,\n"
# Signature delimiter. It delimits the end of the message body from
# the signature.
#
signature-delim = "-- \n"
# Downloads directory path. It is mostly used for downloading
# attachments. Defaults to the system temporary directory.
#
downloads-dir = "~/downloads"
# Defines aliases for your mailboxes. There are 4 special aliases used
# by the tool: inbox, sent, drafts and trash. Other aliases can be
# defined as well.
#
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
folder.aliases.a23 = "Archives/2023"
# Customizes the number of folders to show by page.
#
folder.list.page-size = 10
# Customizes the charset used to build the table. Defaults to markdown
# table style.
#
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
#
folder.list.table.preset = "|| |-||| "
# Customizes the color of the NAME column of the folder listing table.
#
folder.list.table.name-color = "blue"
# Customizes the color of the DESC column of the folder listing table.
#
folder.list.table.desc-color = "green"
# Customizes the number of envelopes to show by page.
#
envelope.list.page-size = 10
# Customizes the format of the envelope date.
#
# See supported formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
#
envelope.list.datetime-fmt = "%F %R%:z"
# Transforms envelopes date timezone into the user's local one. For
# example, if the user's local timezone is UTC, the envelope date
# `2023-06-15T09:00:00+02:00` becomes `2023-06-15T07:00:00-00:00`.
#
envelope.list.datetime-local-tz = true
# Customizes the charset used to build the table. Defaults to markdown
# table style.
#
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
#
envelope.list.table.preset = "|| |-||| "
# Customizes the character of the unseen flag of the envelope listing
# table.
#
envelope.list.table.unseen-char = "*"
# Customizes the character of the replied flag of the envelope listing
# table.
#
envelope.list.table.replied-char = "R"
# Customizes the character of the flagged flag of the envelope listing
# table.
#
envelope.list.table.flagged-char = "!"
# Customizes the character of the attachment property of the envelope
# listing table.
#
envelope.list.table.attachment-char = "@"
# Customizes the color of the ID column of the envelope listing table.
#
envelope.list.table.id-color = "red"
# Customizes the color of the FLAGS column of the envelope listing
# table.
#
envelope.list.table.flags-color = "black"
# Customizes the color of the SUBJECT column of the envelope listing
# table.
#
envelope.list.table.subject-color = "green"
# Customizes the color of the SENDER column of the envelope listing
# table.
#
envelope.list.table.sender-color = "blue"
# Customizes the color of the DATE column of the envelope listing
# table.
#
envelope.list.table.date-color = "yellow"
# Defines headers to show at the top of messages when reading them.
#
message.read.headers = ["From", "To", "Cc", "Subject"]
# Represents the message text/plain format as defined in the
# RFC2646.
#
# See <https://www.ietf.org/rfc/rfc2646.txt>.
#
#message.read.format.fixed = 80
#message.read.format = "flowed"
message.read.format = "auto"
# Defines headers to show at the top of messages when writing them.
#
message.write.headers = ["From", "To", "In-Reply-To", "Cc", "Subject"]
# Saves a copy of sent messages to the sent folder. The sent folder is
# taken from folder.aliases, defaults to Sent.
#
message.send.save-copy = true
# Hook called just before sending a message. The command should take a
# raw message as standard input (stdin) and returns the modified raw
# message to the standard output (stdout).
#
message.send.pre-hook = "process-markdown.sh"
# Customizes the message deletion style. Message deletion can be
# performed either by moving messages to the Trash folder or by adding
# the Deleted flag to their respective envelopes.
#
#message.delete.style = "flag"
message.delete.style = "folder"
# Defines how and where the signature should be displayed when writing
# a new message.
#
#template.new.signature-style = "hidden"
#template.new.signature-style = "attached"
template.new.signature-style = "inlined"
# Defines the posting style when replying to a message.
#
# See <https://en.wikipedia.org/wiki/Posting_style>.
#
#template.reply.posting-style = "interleaved"
#template.reply.posting-style = "bottom"
template.reply.posting-style = "top"
# Defines how and where the signature should be displayed when
# repyling to a message.
#
#template.reply.signature-style = "hidden"
#template.reply.signature-style = "attached"
#template.reply.signature-style = "above-quote"
template.reply.signature-style = "below-quote"
# Defines the headline format put at the top of a quote when replying
# to a message.
#
# Available placeholders: {senders}
# See supported date formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
#
template.reply.quote-headline-fmt = "On %d/%m/%Y %H:%M, {senders} wrote:\n"
# Defines the posting style when forwarding a message.
#
# See <https://en.wikipedia.org/wiki/Posting_style>.
#
#template.forward.posting-style = "attached"
template.forward.posting-style = "top"
# Defines how and where the signature should be displayed when
# forwarding a message.
#
#template.forward.signature-style = "hidden"
#template.forward.signature-style = "attached"
template.forward.signature-style = "inlined"
# Defines the headline format put at the top of the quote when
# forwarding a message.
#
template.forward.quote-headline = "-------- Forwarded Message --------\n"
# Enables PGP using GPG bindings. It requires the GPG lib to be
# installed on the system, and the `pgp-gpg` cargo feature on.
#
#pgp.type = "gpg"
# Enables PGP using shell commands. A PGP client needs to be installed
# on the system, like gpg. It also requires the `pgp-commands` cargo
# feature.
#
#pgp.type = "commands"
# Defines the encrypt command. The special placeholder `<recipients>`
# represents the list of recipients, formatted by
# `pgp.encrypt-recipient-fmt`.
#
#pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
# Formats recipients for `pgp.encrypt-cmd`. The special placeholder
# `<recipient>` is replaced by an actual recipient at runtime.
#
#pgp.encrypt-recipient-fmt = "--recipient <recipient>"
# Defines the separator used between formatted recipients
# `pgp.encrypt-recipient-fmt`.
#
#pgp.encrypt-recipients-sep = " "
# Defines the decrypt command.
#
#pgp.decrypt-cmd = "gpg --decrypt --quiet"
# Defines the sign command.
#
#pgp.sign-cmd = "gpg --sign --quiet --armor"
# Defines the verify command.
#
#pgp.verify-cmd = "gpg --verify --quiet"
# Enables the native Rust implementation of PGP. It requires the
# `pgp-native` cargo feature.
#
#pgp.type = "native"
# Defines where to find the PGP secret key.
#
#pgp.secret-key.path = "/path/to/secret.key"
#pgp.secret-key.keyring = "my-pgp-secret-key"
# Defines how to retrieve the PGP secret key passphrase.
#
#pgp.secret-key-passphrase.raw = "p@assw0rd"
#pgp.secret-key-passphrase.keyring = "my-pgp-passphrase"
#pgp.secret-key-passphrase.cmd = "pass show pgp-passphrase"
# Enables the Web Key Discovery protocol to discover recipients'
# public key based on their email address.
#
#pgp.wkd = true
# Enables public key servers discovery.
#
#pgp.key-servers = ["hkps://keys.openpgp.org", "hkps://keys.mailvelope.com"]
# Defines the IMAP backend as the default one for all features.
#
backend.type = "imap"
# IMAP server host name.
#
backend.host = "localhost"
# IMAP server port.
#
#backend.port = 143
backend.port = 993
# IMAP server encryption.
#
#backend.encryption.type = "none"
#backend.encryption.type = "start-tls"
backend.encryption.type = "tls"
# IMAP server login.
#
backend.login = "example@localhost"
# IMAP server password authentication configuration.
#
backend.auth.type = "password"
#
# Password can be inlined (not recommended).
#
#backend.auth.raw = "p@assw0rd"
#
# Password can be stored inside your system global keyring (requires
# the keyring cargo feature). You must run at least once `himalaya
# account configure` to set up the password.
#
#backend.auth.keyring = "example-imap"
#
# Password can be retrieved from a shell command.
#
backend.auth.cmd = "pass show example-imap"
# IMAP server OAuth 2.0 authorization configuration.
#
#backend.auth.type = "oauth2"
#
# Client identifier issued to the client during the registration
# process described in RFC6749.
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#backend.auth.client-id = "client-id"
#
# Client password issued to the client during the registration process
# described in RFC6749.
#
# Defaults to keyring "<account-name>-imap-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#backend.auth.client-secret.raw = "<raw-client-secret>"
#backend.auth.client-secret.keyring = "example-imap-client-secret"
#backend.auth.client-secret.cmd = "pass show example-imap-client-secret"
#
# Method for presenting an OAuth 2.0 bearer token to a service for
# authentication.
#
#backend.auth.method = "oauthbearer"
#backend.auth.method = "xoauth2"
#
# URL of the authorization server's authorization endpoint.
#
#backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
#
# URL of the authorization server's token endpoint.
#
#backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
#
# Access token returned by the token endpoint and used to access
# protected resources. It is recommended to use the keyring variant,
# as it will refresh automatically.
#
# Defaults to keyring "<account-name>-imap-access-token".
#
#backend.auth.access-token.raw = "<raw-access-token>"
#backend.auth.access-token.keyring = "example-imap-access-token"
#backend.auth.access-token.cmd = "pass show example-imap-access-token"
#
# Refresh token used to obtain a new access token (if supported by the
# authorization server). It is recommended to use the keyring variant,
# as it will refresh automatically.
#
# Defaults to keyring "<account-name>-imap-refresh-token".
#
#backend.auth.refresh-token.raw = "<raw-refresh-token>"
#backend.auth.refresh-token.keyring = "example-imap-refresh-token"
#backend.auth.refresh-token.cmd = "pass show example-imap-refresh-token"
#
# Enable the protection, as defined in RFC7636.
#
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
#
#backend.auth.pkce = true
#
# Access token scope(s), as defined by the authorization server.
#
#backend.auth.scope = "unique scope"
#backend.auth.scopes = ["multiple", "scopes"]
#
# URL scheme of the redirect server.
# Defaults to http.
#
#backend.auth.redirect-scheme = "http"
#
# Host name of the redirect server.
# Defaults to localhost.
#
#backend.auth.redirect-host = "localhost"
#
# Port of the redirect server.
# Defaults to the first available one.
#
#backend.auth.redirect-port = 9999
# Defines the Maildir backend as the default one for all features.
#
#backend.type = "maildir"
# The Maildir root directory. The path should point to the root level
# of the Maildir directory.
#
#backend.root-dir = "~/.Mail/example"
# Does the Maildir folder follows the Maildir++ standard?
#
# See <https://en.wikipedia.org/wiki/Maildir#Maildir++>.
#
#backend.maildirpp = false
# Defines the Notmuch backend as the default one for all features.
#
#backend.type = "notmuch"
# The path to the Notmuch database. The path should point to the root
# directory containing the Notmuch database (usually the root Maildir
# directory).
#
#backend.db-path = "~/.Mail/example"
# Overrides the default path to the Maildir folder.
#
#backend.maildir-path = "~/.Mail/example"
# Overrides the default Notmuch configuration file path.
#
#backend.config-path = "~/.notmuchrc"
# Override the default Notmuch profile name.
#
#backend.profile = "example"
# Defines the SMTP backend for the message sending feature.
#
message.send.backend.type = "smtp"
# SMTP server host name.
#
message.send.backend.host = "localhost"
# SMTP server port.
#
#message.send.backend.port = 25
#message.send.backend.port = 465
message.send.backend.port = 587
# SMTP server encryption.
#
#message.send.backend.encryption.type = "none"
#message.send.backend.encryption.type = "start-tls"
message.send.backend.encryption.type = "tls"
# SMTP server login.
#
message.send.backend.login = "example@localhost"
# SMTP server password authentication configuration.
#
message.send.backend.auth.type = "password"
#
# Password can be inlined (not recommended).
#
#message.send.backend.auth.raw = "p@assw0rd"
#
# Password can be stored inside your system global keyring (requires
# the keyring cargo feature). You must run at least once `himalaya
# account configure` to set up the password.
#
#message.send.backend.auth.keyring = "example-smtp"
#
# Password can be retrieved from a shell command.
#
message.send.backend.auth.cmd = "pass show example-smtp"
# SMTP server OAuth 2.0 authorization configuration.
#
#message.send.backend.auth.type = "oauth2"
#
# Client identifier issued to the client during the registration
# process described in RFC6749.
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#message.send.backend.auth.client-id = "client-id"
#
# Client password issued to the client during the registration process
# described in RFC6749.
#
# Defaults to keyring "<account-name>-smtp-client-secret".
# See <https://datatracker.ietf.org/doc/html/rfc6749#section-2.2>.
#
#message.send.backend.auth.client-secret.raw = "<raw-client-secret>"
#message.send.backend.auth.client-secret.keyring = "example-smtp-client-secret"
#message.send.backend.auth.client-secret.cmd = "pass show example-smtp-client-secret"
#
# Method for presenting an OAuth 2.0 bearer token to a service for
# authentication.
#
#message.send.backend.auth.method = "oauthbearer"
#message.send.backend.auth.method = "xoauth2"
#
# URL of the authorization server's authorization endpoint.
#
#message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
#
# URL of the authorization server's token endpoint.
#
#message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
#
# Access token returned by the token endpoint and used to access
# protected resources. It is recommended to use the keyring variant,
# as it will refresh automatically.
#
# Defaults to keyring "<account-name>-smtp-access-token".
#
#message.send.backend.auth.access-token.raw = "<raw-access-token>"
#message.send.backend.auth.access-token.keyring = "example-smtp-access-token"
#message.send.backend.auth.access-token.cmd = "pass show example-smtp-access-token"
#
# Refresh token used to obtain a new access token (if supported by the
# authorization server). It is recommended to use the keyring variant,
# as it will refresh automatically.
#
# Defaults to keyring "<account-name>-smtp-refresh-token".
#
#message.send.backend.auth.refresh-token.raw = "<raw-refresh-token>"
#message.send.backend.auth.refresh-token.keyring = "example-smtp-refresh-token"
#message.send.backend.auth.refresh-token.cmd = "pass show example-smtp-refresh-token"
#
# Enable the protection, as defined in RFC7636.
#
# See <https://datatracker.ietf.org/doc/html/rfc7636>.
#
#message.send.backend.auth.pkce = true
#
# Access token scope(s), as defined by the authorization server.
#
#message.send.backend.auth.scope = "unique scope"
#message.send.backend.auth.scopes = ["multiple", "scopes"]
#
# URL scheme of the redirect server.
# Defaults to http.
#
#message.send.backend.auth.redirect-scheme = "http"
#
# Host name of the redirect server.
# Defaults to localhost.
#
#message.send.backend.auth.redirect-host = "localhost"
#
# Port of the redirect server.
# Defaults to the first available one.
#
#message.send.backend.auth.redirect-port = 9999
# Defines the Sendmail backend for the message sending feature.
#
#message.send.backend.type = "sendmail"
# Customizes the sendmail shell command.
#
#message.send.backend.cmd = "/usr/bin/sendmail"

View file

@ -1,12 +1,29 @@
# This file exists for legacy Nix installs (nix-build & nix-env)
# https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix
# You generally do *not* have to modify this ever.
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash; }
) {
src = ./.;
}).defaultNix
{
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
...
}@args:
pimalaya.mkDefault (
{
src = ./.;
version = "1.0.0";
mkPackage = (
{
lib,
pkgs,
rustPlatform,
defaultFeatures,
features,
}:
pkgs.callPackage ./package.nix {
inherit lib rustPlatform;
apple-sdk = pkgs.apple-sdk;
installShellCompletions = false;
installManPages = false;
buildNoDefaultFeatures = !defaultFeatures;
buildFeatures = lib.splitString "," features;
}
);
}
// removeAttrs args [ "pimalaya" ]
)

143
flake.lock generated
View file

@ -1,138 +1,79 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1637014545,
"narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1639947939,
"narHash": "sha256-pGsM8haJadVP80GFq4xhnSpNitYNQpaXk4cnA796Cso=",
"owner": "nix-community",
"repo": "naersk",
"rev": "2fc8ce9d3c025d59fee349c1f80be9785049d653",
"lastModified": 1732405626,
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
"owner": "soywod",
"repo": "fenix",
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"owner": "soywod",
"repo": "fenix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1640418986,
"narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
"owner": "NixOS",
"lastModified": 1736437047,
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
"rev": "f17b95775191ea44bc426831235d87affb10faba",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "nixos",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"pimalaya": {
"flake": false,
"locked": {
"lastModified": 1640418986,
"narHash": "sha256-a8GGtxn2iL3WAkY5H+4E0s3Q7XJt6bTOvos9qqxT5OQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5c37ad87222cfc1ec36d6cd1364514a9efc2f7f2",
"lastModified": 1737984647,
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
"owner": "pimalaya",
"repo": "nix",
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1637453606,
"narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"owner": "pimalaya",
"repo": "nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"utils": "utils"
"fenix": "fenix",
"nixpkgs": "nixpkgs",
"pimalaya": "pimalaya"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3"
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1642838864,
"narHash": "sha256-pHnhm3HWwtvtOK7NdNHwERih3PgNlacrfeDwachIG8E=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "9fb49daf1bbe1d91e6c837706c481f9ebb3d8097",
"lastModified": 1732050317,
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}

View file

@ -1,87 +1,26 @@
{
description = "Command-line interface for email management";
description = "CLI to manage emails";
inputs = {
utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nix-community/naersk";
flake-compat = {
url = "github:edolstra/flake-compat";
# FIXME: when #358989 lands on nixos-unstable
# https://nixpk.gs/pr-tracker.html?pr=358989
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
fenix = {
# TODO: https://github.com/nix-community/fenix/pull/145
# url = "github:nix-community/fenix";
url = "github:soywod/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
pimalaya = {
url = "github:pimalaya/nix";
flake = false;
};
};
outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }:
utils.lib.eachDefaultSystem
(system:
let
name = "himalaya";
pkgs = import nixpkgs {
inherit system;
overlays = [
rust-overlay.overlay
(self: super: {
# Because rust-overlay bundles multiple rust packages
# into one derivation, specify that mega-bundle here,
# so that crate2nix will use them automatically.
rustc = self.rust-bin.stable.latest.default;
cargo = self.rust-bin.stable.latest.default;
})
];
};
in
rec {
# nix build
defaultPackage = packages.${name};
packages = {
${name} = naersk.lib.${system}.buildPackage {
pname = name;
root = ./.;
nativeBuildInputs = with pkgs; [ openssl.dev pkgconfig ];
overrideMain = _: {
postInstall = ''
mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/
'';
};
};
"${name}-vim" = pkgs.vimUtils.buildVimPluginFrom2Nix {
inherit (packages.${name}) version;
name = "${name}-vim";
src = self;
buildInputs = [ packages.${name} ];
dontConfigure = false;
configurePhase = "cd vim/";
postInstall = ''
mkdir -p $out/bin
ln -s ${packages.${name}}/bin/himalaya $out/bin/himalaya
'';
};
};
# nix run
defaultApp = apps.${name};
apps.${name} = utils.lib.mkApp {
inherit name;
drv = packages.${name};
};
# nix develop
devShell = pkgs.mkShell {
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
inputsFrom = builtins.attrValues self.packages.${system};
buildInputs = with pkgs; [
cargo
cargo-watch
trunk
ripgrep
rust-analyzer
rustfmt
rnix-lsp
nixpkgs-fmt
notmuch
];
};
}
);
outputs =
inputs:
(import inputs.pimalaya).mkFlakeOutputs inputs {
shell = ./shell.nix;
default = ./default.nix;
};
}

View file

@ -9,25 +9,47 @@ die() {
DESTDIR="${DESTDIR:-}"
PREFIX="${PREFIX:-"$DESTDIR/usr/local"}"
RELEASES_URL="https://github.com/soywod/himalaya/releases"
RELEASES_URL="https://github.com/pimalaya/himalaya/releases"
binary=himalaya
system=$(uname -s | tr [:upper:] [:lower:])
machine=$(uname -m | tr [:upper:] [:lower:])
case $system in
msys*|mingw*|cygwin*|win*) system=windows; binary=himalaya.exe;;
linux|freebsd) system=linux; binary=himalaya;;
darwin) system=macos; binary=himalaya;;
*) die "Unsupported system: $system" ;;
msys*|mingw*|cygwin*|win*)
target=x86_64-windows
binary=himalaya.exe;;
linux|freebsd)
case $machine in
x86_64) target=x86_64-linux;;
x86|i386|i686) target=i686-linux;;
arm64|aarch64) target=aarch64-linux;;
armv6l) target=armv6l-linux;;
armv7l) target=armv7l-linux;;
*) die "Unsupported machine $machine for system $system";;
esac;;
darwin)
case $machine in
x86_64) target=x86_64-darwin;;
arm64|aarch64) target=aarch64-darwin;;
*) die "Unsupported machine $machine for system $system";;
esac;;
*)
die "Unsupported system $system";;
esac
tmpdir=$(mktemp -d) || die "Failed to create tmpdir"
tmpdir=$(mktemp -d) || die "Cannot create temporary directory"
trap "rm -rf $tmpdir" EXIT
echo "Downloading latest $system release…"
curl -sLo "$tmpdir/himalaya.tar.gz" \
"$RELEASES_URL/latest/download/himalaya-$system.tar.gz"
curl -sLo "$tmpdir/himalaya.tgz" \
"$RELEASES_URL/latest/download/himalaya.$target.tgz"
echo "Installing binary…"
tar -xzf "$tmpdir/himalaya.tar.gz" -C "$tmpdir"
tar -xzf "$tmpdir/himalaya.tgz" -C "$tmpdir"
mkdir -p "$PREFIX/bin"
cp -f -- "$tmpdir/$binary" "$PREFIX/bin/$binary"

View file

@ -1,6 +0,0 @@
[package]
name = "himalaya-lib"
version = "0.1.0"
edition = "2021"
[dependencies]

View file

@ -1,8 +0,0 @@
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}

20
logo-small.svg Normal file
View file

@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 340.2 340.2" style="enable-background:new 0 0 340.2 340.2" xml:space="preserve">
<style>
.st1{fill:#f5e6ca}
</style>
<path d="m322.5 120.7-2.3-2h-.1L191 7.5c-5.6-4.8-12.6-7.3-19.7-7h-1.6c-7.2-.2-14.2 2.3-19.8 7L18.7 120.6C11.9 126.5 8 135.1 8 144.3v36.3c-.1.8-.1 1.5-.1 2.2v119.7c0 .9 0 1.9.1 2.9v15.3c0 5.8 1.7 10.4 4.9 13.6 4.3 4.2 10 4.9 15.9 4.9 1.4 0 2.8 0 4.2-.1 1.5 0 3-.1 4.6-.1h265.7c1.6 0 3.1 0 4.5.1 7.3.2 14.9.4 20.3-4.8 3.3-3.2 4.9-7.7 4.9-13.5V144.3c.1-9.1-3.8-17.7-10.5-23.6z" style="fill:#444" id="Calque_2"/>
<g id="Calque_1">
<path class="st1" d="M317.1 126.7 185.8 13.6c-4.2-3.6-9.3-5.3-14.4-5.1h-1.9c-5.1-.2-10.2 1.5-14.4 5.1L23.9 126.7c-5 4.3-7.9 10.8-7.9 17.6v176.4c0 12.6 9.7 10.3 21.7 10.3h265.7c12 0 21.7 2.2 21.7-10.3V144.3c0-6.8-2.9-13.2-7.9-17.6h-.1z"/>
<radialGradient id="SVGID_1_" cx="176.718" cy="89.04" r="180.6" gradientTransform="matrix(.9999 .0157 .011 -.6999 -4.55 211.672)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#f7bd6c"/>
<stop offset=".5" style="stop-color:#db8355"/>
<stop offset=".8" style="stop-color:#29445d"/>
<stop offset="1" style="stop-color:#143651"/>
</radialGradient>
<path d="M309.7 134.2c8.4 6.8 8.4 51.2 0 57.9l-111.5 58.2-27.4-22.1-27.4 22.1-100.1-51.5-11.4-6.7c-8.4-6.8-9.6-50 0-57.9L155.8 27.5c8.8-7.1 21.3-7.1 30.1 0l123.8 106.7z" style="fill:url(#SVGID_1_)"/>
<path d="m197.7 250.4 27 78h72.7c12.6 0 27.6-5.4 27.6-25.9V182.8c0-14.2-16.5-22.1-27.6-13.1l-99.7 80.7zm-54.5 0-27 78H43.5c-12.6 0-27.6-5.4-27.6-25.9V182.8c0-14.2 16.5-22.1 27.6-13.1l99.7 80.7z" style="fill:#fcedd0"/>
<path d="M116.7 328.1H23.1c-10.9 0-1.8-7.6-.2-8.7L134 243.2l8.9 7.2-26.3 77.7h.1zm107.3 0h93.5c10.9 0 1.8-7.6.2-8.7l-111.1-76.2-8.9 7.2 26.3 77.7z" style="fill:#7c6d5d"/>
<path class="st1" d="M317.4 322.1c-6.5-4.3-140.1-89.8-140.1-89.8-2.1-1.3-4.4-2-6.7-2s-4.7.7-6.7 2c0 0-133.6 85.5-140.1 89.8-5.3 3.5-4.8 6.1 0 6.1h294.1c4.7 0 5.2-2.7 0-6.1h-.5z"/>
<circle cx="170.9" cy="154.4" r="47.8" style="fill:#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

81
logo.svg Normal file
View file

@ -0,0 +1,81 @@
<svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 680.3 680.3">
<defs>
<radialGradient id="Dégradé_sans_nom_28" data-name="Dégradé sans nom 28" cx="345.9" cy="318.2" fx="345.9" fy="318.2" r="377.3" gradientTransform="rotate(.9 -9637.325 190.29) scale(1 .5)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#f7bd6c"/>
<stop offset=".5" stop-color="#db8355"/>
<stop offset=".8" stop-color="#29445d"/>
<stop offset="1" stop-color="#143651"/>
</radialGradient>
<style>
.cls-2,.cls-4,.cls-5,.cls-6{stroke:#fff}.cls-8{fill:#1a374a}.cls-6{stroke-miterlimit:10}.cls-2{fill:#fed894}.cls-2,.cls-4,.cls-5{stroke-width:2.3px;stroke-linecap:round;stroke-linejoin:round}.cls-10,.cls-11,.cls-12,.cls-15,.cls-16,.cls-18,.cls-8{stroke-width:0}.cls-10{fill:#fffcf9}.cls-11{fill:#fcedd0}.cls-12{fill:#233a7b}.cls-4{fill:#fff7ea}.cls-5{fill:#fdcc7c}.cls-16{opacity:.2}.cls-15{fill:#fff}.cls-16{fill:#0b5272}.cls-6{fill:#ffedd2;stroke-width:2.8px}.cls-18{fill:#e7d6be}
</style>
</defs>
<path class="cls-11" d="M646.1 249.5 371.8 13.2c-8.7-7.5-19.4-11-30-10.7h-3.9c-10.6-.3-21.3 3.2-30 10.7L33.5 249.5C23 258.5 17 272 17 286.2v368.5c0 26.2 20.3 21.6 45.3 21.6h555.2c25 0 45.3 4.6 45.3-21.6V286.2c0-14.2-6.1-27.7-16.5-36.7Z"/>
<path d="M630.8 265.1c17.5 14.1 17.6 106.9 0 121L397.9 507.7l-57.2-46.1-57.2 46.1L74.4 400.2l-23.8-14.1C33 372 30.5 281.6 50.6 265L309.2 42.1c18.3-14.8 44.6-14.8 63 0l258.6 223Z" style="stroke-width:0;fill:url(#Dégradé_sans_nom_28)"/>
<circle class="cls-15" cx="342.8" cy="320.6" r="54.4"/>
<g style="opacity:.7">
<path class="cls-15" d="M274.5 333c-13.3 4.8-34.5 7.2-48.5 9.9-8 1.4-28.8 5-36.5 6.3l-12.4.7 11.9-3.5c7.7-1.3 28.6-5 36.5-6.3 14.1-2.2 34.8-7 49-7Zm136.5.4c14.1 0 34.9 5.1 48.9 7.3 8 1.4 28.8 5.2 36.5 6.6l11.9 3.6-12.4-.8c-7.7-1.4-28.6-5.1-36.5-6.6-14-2.8-35.1-5.3-48.4-10.2Zm-206.3-12.6c2.8-1.2 6.5-1.7 9.6-1.4h19.2c3.1-.2 6.7.3 9.6 1.5-2.9 1.2-6.5 1.7-9.6 1.4h-19.2c-3 .2-6.8-.2-9.6-1.5Zm161.7-65.2c2.4-13.9 10.9-33.5 15.4-47 2.8-7.7 9.9-27.5 12.6-34.9l5.6-11.1-2.8 12.1c-2.6 7.3-9.9 27.3-12.6 34.9-5.1 13.3-11.1 33.7-18.2 46Zm-23.8-4.1c-2.5-13.9-1.2-35.2-1.6-49.5v-37.1l1.4-12.4 1.5 12.4V202c-.3 14.3 1.1 35.5-1.3 49.5Zm0-129.3c-1.3-2.7-1.7-6-1.5-9v-18c-.3-3 .1-6.2 1.3-9 1.3 2.8 1.7 5.9 1.5 9v18c.3 2.9 0 6.3-1.3 9Zm-23.7 133.5c-7.1-12.2-13.2-32.7-18.4-45.9-2.8-7.6-10.1-27.4-12.8-34.8l-2.9-12.1 5.6 11.1c2.7 7.3 10 27.2 12.8 34.8 4.6 13.5 13.1 33 15.7 46.9Zm-20.8 12.1c-10.8-9.1-23.6-26.2-33-36.9-5.2-6.2-18.8-22.3-23.9-28.4l-6.9-10.4 9.1 8.5c5 6 18.7 22.2 23.9 28.4 9 11.1 23.6 26.5 30.8 38.7Zm-15.4 18.5c-13.3-4.8-31.1-16.5-43.6-23.4-7.1-4.1-25.3-14.5-32.2-18.5l-10-7.4 11.4 4.9c6.8 3.9 25.2 14.4 32.2 18.5 12.2 7.4 31.3 16.8 42.2 25.9Zm-8.2 22.6c-14.1 0-34.9-4.9-49-7-8-1.4-28.8-5-36.5-6.3l-11.9-3.5 12.4.7c7.7 1.3 28.6 5 36.6 6.3 14 2.7 35.1 5.1 48.5 9.9Zm136.5-.4c13.3-4.9 34.5-7.4 48.4-10.2 8-1.4 28.7-5.2 36.5-6.6l12.4-.8-11.9 3.6c-7.7 1.4-28.6 5.1-36.5 6.6-14.1 2.2-34.7 7.3-48.9 7.3Zm-8.3-22.6c10.8-9.1 29.9-18.7 42-26.1 7-4.1 25.2-14.7 32.1-18.7l11.4-5-10 7.5c-6.7 3.9-25.1 14.6-32.1 18.7-12.5 6.9-30.2 18.7-43.5 23.6Zm-15.5-18.4c3.6-8.1 13-17.5 18.5-24.5 5.9-6.6 13.4-17.6 20.7-22.6-3.6 8.1-13 17.5-18.5 24.5-5.9 6.6-13.4 17.6-20.7 22.6Zm-20.7-82.8c.8 3 .5 6.7-.2 9.7l-1.6 9.4-1.6 9.4c-.3 3.1-1.3 6.5-3.1 9.2-.7-3.1-.5-6.6.2-9.7l1.6-9.4 1.6-9.4c.3-3 1.4-6.6 3.1-9.2Zm2.7-16c-.5-.7-.8-1.3-1.1-2 .2-1.1.7-4 .9-5.1.2-1.1.7-4 .9-5.1.6-.5 1-1 1.7-1.5.5.7.7 1.3 1.1 2-.2 1.1-.7 4-.9 5.1-.2 1.1-.7 4-.9 5.1-.6.5-1 1-1.7 1.5Zm-50.7 16.1c1.7 2.6 2.8 6.1 3.1 9.2l1.7 9.4 1.7 9.4c.8 3.1 1 6.6.3 9.7-1.7-2.6-2.8-6.1-3.1-9.2l-1.7-9.4-1.7-9.4c-.8-3-1-6.7-.3-9.7Zm-4.4-24.4c-1.7-2.1-2.4-4.1-2.7-6.7l-1.2-7-1.2-7c-.7-2.6-.7-4.6.2-7.2 1.7 2.1 2.4 4 2.7 6.7l1.2 7 1.2 7c.6 2.5.7 4.7-.2 7.2Zm-40.7 40.9c2.5 1.8 4.7 4.8 6.1 7.6l4.8 8.3 4.8 8.3c1.8 2.6 3.2 5.9 3.6 9-2.5-1.9-4.7-4.7-6.1-7.6l-4.8-8.3-4.8-8.3c-1.7-2.5-3.2-5.9-3.6-9ZM261 179.9c-2.3-1.4-3.7-3-4.8-5.4l-3.6-6.1-3.6-6.1c-1.5-2.2-2.2-4.1-2.3-6.8 2.3 1.4 3.6 3 4.8 5.4l3.6 6.1 3.6 6.1c1.5 2.2 2.3 4.2 2.3 6.8Zm-24.2 52.4c3 .8 6.1 2.9 8.3 5l7.4 6.1 7.4 6.1c2.6 1.8 5 4.4 6.4 7.3-3-.9-6-2.8-8.3-5l-7.4-6.1-7.4-6.1c-2.5-1.8-5.1-4.5-6.4-7.3Zm-19.1-15.9c-2.6-.5-4.5-1.6-6.4-3.4l-5.4-4.5-5.4-4.5c-2.2-1.6-3.5-3.1-4.5-5.6 2.7.6 4.4 1.5 6.4 3.4l5.4 4.5 5.4 4.5c2.1 1.5 3.6 3.2 4.5 5.6Zm-4.8 57.5c3.1-.2 6.7.6 9.5 1.9l9 3.3 9 3.3c3 .9 6.2 2.5 8.5 4.6-3.2.2-6.6-.6-9.5-1.9l-9-3.3-9-3.3c-2.9-.8-6.3-2.5-8.5-4.6Zm-23.4-8.4c-2.6.4-4.8 0-7.1-1l-6.7-2.4-6.7-2.4c-2.6-.7-4.3-1.7-6.2-3.8 2.7-.4 4.7 0 7.1 1l6.7 2.4 6.7 2.4c2.5.7 4.4 1.7 6.2 3.8Z"/>
<path class="cls-15" d="M204.7 321.2c2.8-1.3 6.5-1.7 9.6-1.5h19.2c3.1-.3 6.7.2 9.6 1.4-2.9 1.2-6.4 1.7-9.6 1.5h-19.2c-3 .3-6.8-.2-9.6-1.4Zm267.7-48.1c-2.2 2.2-5.5 3.8-8.5 4.7l-9 3.3-9 3.3c-2.9 1.3-6.3 2.1-9.5 1.9 2.3-2.2 5.5-3.8 8.5-4.7l9-3.3 9-3.3c2.8-1.3 6.4-2.1 9.5-1.9Zm23.3-8.5c1.7-2 3.6-3.1 6.1-3.8l6.6-2.4 6.6-2.4c2.5-1.1 4.4-1.5 7.1-1.1-1.8 2-3.5 3.1-6.1 3.8l-6.6 2.4-6.6 2.4c-2.4 1.1-4.5 1.5-7.1 1.1Zm-47.4-32.9c-1.3 2.8-3.9 5.5-6.4 7.3l-7.3 6.2-7.3 6.2c-2.2 2.2-5.2 4.1-8.3 5.1 1.4-2.8 3.8-5.4 6.4-7.3l7.3-6.2 7.3-6.2c2.2-2.1 5.3-4.2 8.3-5.1Zm19-16c.9-2.5 2.3-4.1 4.5-5.7l5.4-4.6 5.4-4.6c1.9-1.9 3.7-2.9 6.3-3.5-1 2.5-2.3 4.1-4.5 5.7l-5.4 4.6-5.4 4.6c-1.9 1.8-3.7 3-6.3 3.5Zm-55.8-14.8c-.3 3.1-1.8 6.5-3.5 9l-4.8 8.3-4.8 8.3c-1.4 2.8-3.5 5.7-6 7.6.4-3.1 1.7-6.4 3.5-9l4.8-8.3 4.8-8.3c1.3-2.8 3.5-5.8 6-7.6Zm12.3-21.5c0-2.7.8-4.7 2.3-6.9l3.5-6.1 3.5-6.1c1.2-2.4 2.5-4 4.8-5.4 0 2.7-.7 4.6-2.3 6.9l-3.5 6.1-3.5 6.1c-1.1 2.4-2.5 4-4.8 5.4Z"/>
</g>
<path class="cls-15" d="m460.9 185.2 2.8-.5-2.8-.5c-1.2-.2-2.1-1.1-2.2-2.2l-.5-2.8-.5 2.8c-.2 1.2-1.1 2.1-2.2 2.2l-2.8.5 2.8.5c1.2.2 2.1 1.1 2.2 2.2l.5 2.8.5-2.8c.2-1.2 1.1-2.1 2.2-2.2Z"/>
<path class="cls-16" d="M506.5 148.4c-22.1 5.7-29.8 27.2-29.8 27.2s-5.8-3.5-15.4-1.9c-8.1 1.3-16.5 8.4-19.1 16.6-16.4 0-23.1 28.3 9.7 28.3s158.3.2 158.3.2-58.4-50.2-71.7-61.7c-1.7-1.5-14.5-13.1-32.1-8.6Z"/>
<path style="fill:#f8dca4;stroke-width:0" d="M169.9 362.5h350.9V458H169.9z"/>
<path style="fill:none;stroke-width:4.8px;stroke-linecap:round;stroke-linejoin:round;stroke:#fff" d="m275.2 446.7 18.1-31.1"/>
<path class="cls-16" d="M97.3 194.2h125.3c25.7 0 32.6-7.8 32.6-20s-17-16.1-19.7-16.1c-1.8-5.5-10.4-14.7-20.7-15.1-10.4-.4-19 3.9-23.7 11.6-5.8-13.2-20.3-15.8-30.9-14.6l-62.8 54.1Zm95.3-20.6c-.3.5-.7 1-1 1.6-.2-.6-.5-1.1-.8-1.6h1.8Z"/>
<path class="cls-4" d="M174.5 180.8h-71.6c-11.6 0-18.1-8.2-18.1-22.4s12.1-33.2 36.7-33.2 34.1 14.2 37.1 22c10.8-1.7 25.4.9 31.5 14.7 5.4-7.7 13.4-12.1 23.7-11.6 10.4.4 18.4 8.2 20.7 15.1 11.2 0 15.1 2.2 15.1 7.8s-3.5 7.8-19.8 7.8h-55.2Z"/>
<path class="cls-10" d="M384 370.8h-63.4c-1 0-1.8-.8-1.8-1.8s.8-1.8 1.8-1.8H384c1 0 1.8.8 1.8 1.8s-.8 1.8-1.8 1.8Zm-40.2 9.2h-38.5c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h38.5c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Zm-6.8 8.1h-18.9c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2H337c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Zm16.7 0h-7.4c-.7 0-1.2-.6-1.2-1.2 0-.7.6-1.2 1.2-1.2h7.4c.7 0 1.2.6 1.2 1.2 0 .7-.6 1.2-1.2 1.2Z"/>
<circle class="cls-10" cx="349.4" cy="378.8" r="1.3"/>
<path class="cls-10" d="M313.9 386.7c0 .6-.5 1-1 1s-1-.5-1-1 .5-1 1-1 1 .5 1 1Z"/>
<path class="cls-6" d="M310.9 409.3C298.2 397 288 395 288 395s.8-10-6.9-17.4c-7.8-7.3-18.4-8.9-18.4-8.9s-2.5-21.6-21.2-36.3c-18.8-14.7-38.4-8.9-38.4-8.9s-22.9-35.5-52.7-37.8c-8.2-.6-15-.2-20.7.8-10.1-13.9-26.4-22.9-44.9-22.9-30.6 0-55.4 24.8-55.4 55.4s24.8 55.4 55.4 55.4 31.9-7.5 42.1-19.4l25.6 80 163.1 53.1s15.8-7.5 15.8-35-7.8-31.6-20.4-44Z"/>
<path class="cls-2" d="M125.6 319.1s54.4-16.6 96.5 41.7c10-.4 39.4 6.6 45.1 31.6 5.8 25.1 8 42.2 8 42.2H135.3l-9.6-115.5Z"/>
<path class="cls-5" d="M299.3 436.3c0-11.5-3.9-28.1-23.4-40.9-12.3-8-32.6-9.2-37.2-5.9-27.7-44.5-67.9-35.7-67.9-35.7l-13.1 73.3h96.8c-.2-.9 14.1 34.6 13.9 33.7 0 0 30.7 5.7 30.9-24.6Z"/>
<path class="cls-6" d="M599.6 263.7c-18.8 0-35.4 9.4-45.5 23.7-6.3-1.5-14.4-2.4-24.4-1.6-29.8 2.3-52.6 37.8-52.6 37.8s-19.6-5.8-38.3 8.9c-18.8 14.7-21.2 36.3-21.2 36.3s-10.6 1.5-18.4 8.9-6.9 17.4-6.9 17.4-10.2 1.9-22.8 14.3c-12.6 12.3-17.6 33.8-17.6 49.2s.9 30.2.9 30.2l175-53.6 26.8-83.9c10 14.1 26.5 23.3 45.1 23.3 30.6 0 55.4-24.8 55.4-55.4s-24.8-55.4-55.4-55.4Z"/>
<path class="cls-2" d="M573.1 322.3s-52.5-21.9-100.1 31.9c-9.9-1.4-36.6 3.9-48.1 27-11.5 23.2-12.1 41.2-12.1 41.2L552 436.3l21.1-114Z"/>
<path class="cls-5" d="M368.8 434.1s3.1-28.7 36.5-41.5c12.3-4.7 28.6-3.2 34.1.8 33-53.1 80.9-42.6 80.9-42.6l15.6 87.4H420.5c.3-1-71.1 51.7-49.3 34.7l-2.5-38.8Z"/>
<path d="M28.1 349.4s17.7-28 26.6-40.4c23.9-33.5 59.4-34 85.1 2.1 11.8 16.5 24.3 33.1 38.1 53.9 10.1 15.3 19.1-7.4 43.6 17.8 17.9 18.4 43.7 74.9 43.7 74.9l13.6-10.3v14.8l-230.1-7.6-20.6-105.1Z" style="stroke-width:3.3px;stroke-miterlimit:10;fill:#1a374a;fill-rule:evenodd;stroke:#fff"/>
<path d="M519.1 352.6c7.9 2.9 16.3-11.7 22.3-22.2 18.2-31.9 63.6-37.1 86.2-4 5.9 8.6 18.4 30.1 26.8 45.3 3.6 6.5-41.8 83.8-41.8 83.8l-256.4 8s37.1-2.3 42.5-9.5c38.5-50.9 49.7-65.9 62.7-82.9 16.1-21.1 37.8-25.6 57.7-18.4Z" style="stroke-miterlimit:10;fill:#1a374a;fill-rule:evenodd;stroke:#fff;stroke-width:3.2px"/>
<path class="cls-8" d="M292 424.1s-11.5 18.6-11.5 30.2h22.9c0-11.7-11.5-30.2-11.5-30.2Z"/>
<path class="cls-12" d="M291.5 433.3h.9v21.1h-.9z"/>
<path class="cls-12" d="m291.6 442-4.7-3.7.7-.3 4.7 3.8-.7.2zm.7 6.4-.7-.3 7.1-4.4.6.3-7 4.4z"/>
<path class="cls-8" d="M250 391.2s15.6 38.5 15.6 62.8h-31.3c0-24.3 15.6-62.8 15.6-62.8Z"/>
<path class="cls-12" d="M249.4 410.1h1.2V454h-1.2z"/>
<path class="cls-12" d="m250.5 428.4 6.4-7.9-1-.5-6.4 7.8 1 .6zm-1 13.1 1-.6-9.6-9.2-.9.6 9.5 9.2z"/>
<path class="cls-8" d="M275.6 413.9s-12.8 24.6-12.8 40.1h25.7c0-15.5-12.8-40.1-12.8-40.1Z"/>
<path class="cls-12" d="M275.1 426h1v28h-1z"/>
<path class="cls-12" d="m275.2 437.6-5.3-5 .8-.3 5.3 5-.8.3zm.8 8.4-.8-.4 7.8-5.8.8.4-7.8 5.8z"/>
<path class="cls-8" d="M306.6 423.7s-13.4 18.6-13.4 30.2H320c0-11.7-13.4-30.2-13.4-30.2Z"/>
<path class="cls-12" d="M306.1 432.8h1v21.1h-1z"/>
<path class="cls-12" d="m306.2 441.6-5.5-3.7.9-.3 5.5 3.8-.9.2zm.8 6.4-.8-.3 8.2-4.4.8.3-8.2 4.4z"/>
<path class="cls-8" d="M323.6 442.5s-4.9 7-4.9 11.4h9.9c0-4.4-4.9-11.4-4.9-11.4Z"/>
<path class="cls-12" d="M323.4 446h.4v8h-.4z"/>
<path class="cls-12" d="m323.4 449.3-2-1.4.3-.1 2 1.4-.3.1zm.3 2.4-.3-.1 3-1.7.3.1-3 1.7z"/>
<path class="cls-8" d="M333.7 442.5s-7.6 7-7.6 11.4h15.2c0-4.4-7.6-11.4-7.6-11.4Z"/>
<path class="cls-12" d="M333.4 446h.6v8h-.6z"/>
<path class="cls-12" d="m333.4 449.3-3.1-1.4.5-.1 3.1 1.4-.5.1zm.5 2.4-.5-.1 4.7-1.7.4.1-4.6 1.7z"/>
<path class="cls-8" d="M374.9 424.1s11.5 18.6 11.5 30.2h-22.9c0-11.7 11.5-30.2 11.5-30.2Z"/>
<path class="cls-12" d="M374.4 433.3h.9v21.1h-.9z"/>
<path class="cls-12" d="m375.3 442 4.7-3.7-.8-.3-4.7 3.8.8.2zm-.8 6.4.7-.3-7-4.4-.7.3 7 4.4z"/>
<path class="cls-8" d="M416.8 391.2s-15.6 38.5-15.6 62.8h31.3c0-24.3-15.6-62.8-15.6-62.8Z"/>
<path class="cls-12" d="M416.2 410.1h1.2V454h-1.2z"/>
<path class="cls-12" d="m416.3 428.4-6.4-7.9 1-.5 6.5 7.8-1.1.6zm1 13.1-.9-.6 9.5-9.2 1 .6-9.6 9.2z"/>
<path class="cls-8" d="M391.3 413.9s12.8 24.6 12.8 40.1h-25.7c0-15.5 12.8-40.1 12.8-40.1Z"/>
<path class="cls-12" d="M390.8 426h1v28h-1z"/>
<path class="cls-12" d="m391.7 437.6 5.3-5-.9-.3-5.2 5 .8.3zm-.8 8.4.8-.4-7.9-5.8-.8.4 7.9 5.8z"/>
<path class="cls-8" d="M360.2 423.7s13.4 18.6 13.4 30.2h-26.8c0-11.7 13.4-30.2 13.4-30.2Z"/>
<path class="cls-12" d="M359.7 432.8h1v21.1h-1z"/>
<path class="cls-12" d="m360.7 441.6 5.4-3.7-.8-.3-5.5 3.8.9.2zm-.9 6.4.8-.3-8.2-4.4-.8.3 8.2 4.4z"/>
<path class="cls-8" d="M343.3 442.5s4.9 7 4.9 11.4h-9.9c0-4.4 4.9-11.4 4.9-11.4Z"/>
<path class="cls-12" d="M343.1 446h.4v8h-.4z"/>
<path class="cls-12" d="m343.4 449.3 2.1-1.4-.3-.1-2.1 1.4.3.1zm-.3 2.4.3-.1-3-1.7-.3.1 3 1.7z"/>
<path class="cls-8" d="M333.2 442.5s7.6 7 7.6 11.4h-15.2c0-4.4 7.6-11.4 7.6-11.4Z"/>
<path class="cls-12" d="M332.9 446h.6v8h-.6z"/>
<path class="cls-12" d="m333.4 449.3 3.2-1.4-.5-.1-3.2 1.4.5.1zm-.4 2.4.4-.1-4.6-1.7-.5.1 4.7 1.7z"/>
<path class="cls-8" d="M38.4 453.9h617v90.7h-617z"/>
<path d="m357 510.4 95.2 141.5h151.9c26.4 0 57.6-11.3 57.6-54V370c0-29.7-34.5-46.2-57.6-27.5l-247 167.9Z" style="opacity:.2;fill:#010101;stroke-width:0"/>
<path class="cls-11" d="m397.1 507.8 56.4 163.1h151.9c26.4 0 57.6-11.3 57.6-54V366.7c0-29.7-34.5-46.2-57.6-27.5L397.1 507.8Zm-113.8 0-56.4 163.1H75c-26.4 0-57.6-11.3-57.6-54V366.7c0-29.7 34.5-46.2 57.6-27.5l208.3 168.6Z"/>
<path class="cls-18" d="M227.8 670.2H32.4c-22.7 0-3.8-15.8-.3-18.2l232.2-159.2 18.5 15-54.9 162.4Zm245.3-196.8-65.4 52.8c-2.4 1.9-5.9 1.7-7.7-.7-1.9-2.3-1.4-5.8 1-7.7l65.4-52.8c2.4-1.9 5.9-1.7 7.7.7 1.9 2.3 1.4 5.8-1 7.7Zm25.6-20.7-4 3.2c-2.4 1.9-5.9 1.7-7.7-.7-1.9-2.3-1.4-5.8 1-7.7l4-3.2c2.4-1.9 5.9-1.7 7.7.7 1.9 2.3 1.4 5.8-1 7.7Zm16.3-13.1-.5.4c-2.4 1.9-5.8 1.6-7.7-.7-1.9-2.3-1.5-5.7.9-7.7l.5-.4c2.4-1.9 5.8-1.6 7.7.7 1.9 2.3 1.5 5.7-.9 7.7Z"/>
<path class="cls-18" d="M452.1 670.2h195.4c22.7 0 3.8-15.8.3-18.2L415.6 492.8l-18.5 15L452 670.2Z"/>
<path class="cls-4" d="M440.9 205c-8.2 0-8.1-11.1 5.2-11.1 2.6-8.2 11.2-15.5 19.4-15.5s15.1 5.2 15.1 5.2 6.9-30 29.8-31.5c16.9-1.1 28.5 12.5 28.5 12.5s8.2-6.9 21.6-6.9c10.4-10.4 20.3-17.1 41.4-14.7 21 2.4 34.5 26.3 34.5 42.7S618.3 205 614 205H441Z"/>
<path class="cls-15" d="M411.9 129.6c2.7 0 2.6-3.6-1.7-3.6-.8-2.7-2.9-4.9-6.3-5-3.2-.1-4.9 1.7-4.9 1.7s-1.9-10.2-11-10.2-9.5 8-9.5 8-1.7-4.3-6.7-4.3c-9.5 0-9.3 13.5.2 13.5h39.9Z"/>
<path d="M647.2 657.7c-13.7-9-292.8-187.5-292.8-187.5-4.3-2.8-9.2-4.1-14.1-4.2h-.4c-4.9 0-9.8 1.4-14.1 4.2 0 0-279.1 178.6-292.8 187.5-11.1 7.3-10 12.8-.2 12.8h614.4c9.9 0 10.9-5.5-.2-12.8Z" style="fill:#f5e6ca;stroke-width:0"/>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

89
package.nix Normal file
View file

@ -0,0 +1,89 @@
# TODO: move this to nixpkgs
# This file aims to be a replacement for the nixpkgs derivation.
{
lib,
pkg-config,
rustPlatform,
fetchFromGitHub,
stdenv,
apple-sdk,
installShellFiles,
installShellCompletions ? stdenv.buildPlatform.canExecute stdenv.hostPlatform,
installManPages ? stdenv.buildPlatform.canExecute stdenv.hostPlatform,
notmuch,
gpgme,
buildNoDefaultFeatures ? false,
buildFeatures ? [ ],
}:
let
version = "1.0.0-beta.4";
hash = "sha256-NrWBg0sjaz/uLsNs8/T4MkUgHOUvAWRix1O5usKsw6o=";
cargoHash = "sha256-YS8IamapvmdrOPptQh2Ef9Yold0IK1XIeGs0kDIQ5b8=";
in
rustPlatform.buildRustPackage rec {
inherit cargoHash version;
inherit buildNoDefaultFeatures buildFeatures;
pname = "himalaya";
src = fetchFromGitHub {
inherit hash;
owner = "pimalaya";
repo = "himalaya";
rev = "v${version}";
};
nativeBuildInputs = [
pkg-config
] ++ lib.optional (installManPages || installShellCompletions) installShellFiles;
buildInputs =
[ ]
++ lib.optional stdenv.hostPlatform.isDarwin apple-sdk
++ lib.optional (builtins.elem "notmuch" buildFeatures) notmuch
++ lib.optional (builtins.elem "pgp-gpg" buildFeatures) gpgme;
doCheck = false;
auditable = false;
# unit tests only
cargoTestFlags = [ "--lib" ];
postInstall =
''
mkdir -p $out/share/{applications,completions,man}
cp assets/himalaya.desktop "$out"/share/applications/
''
+ lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
"$out"/bin/himalaya man "$out"/share/man
''
+ lib.optionalString installManPages ''
installManPage "$out"/share/man/*
''
+ lib.optionalString (stdenv.buildPlatform.canExecute stdenv.hostPlatform) ''
"$out"/bin/himalaya completion bash > "$out"/share/completions/himalaya.bash
"$out"/bin/himalaya completion elvish > "$out"/share/completions/himalaya.elvish
"$out"/bin/himalaya completion fish > "$out"/share/completions/himalaya.fish
"$out"/bin/himalaya completion powershell > "$out"/share/completions/himalaya.powershell
"$out"/bin/himalaya completion zsh > "$out"/share/completions/himalaya.zsh
''
+ lib.optionalString installShellCompletions ''
installShellCompletion "$out"/share/completions/himalaya.{bash,fish,zsh}
'';
meta = rec {
description = "CLI to manage emails";
mainProgram = "himalaya";
homepage = "https://github.com/pimalaya/himalaya";
changelog = "${homepage}/blob/v${version}/CHANGELOG.md";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [
soywod
toastal
yanganto
];
};
}

4
rust-toolchain.toml Normal file
View file

@ -0,0 +1,4 @@
[toolchain]
channel = "1.82.0"
profile = "default"
components = ["rust-src", "rust-analyzer"]

View file

@ -1,74 +0,0 @@
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Auto"
indent_style = "Block"
use_small_heuristics = "Default"
fn_call_width = 60
attr_fn_like_width = 70
struct_lit_width = 18
struct_variant_width = 35
array_width = 60
chain_width = 60
single_line_if_else_max_width = 50
wrap_comments = false
format_code_in_doc_comments = false
comment_width = 80
normalize_comments = false
normalize_doc_attributes = false
license_template_path = ""
format_strings = false
format_macro_matchers = false
format_macro_bodies = true
empty_item_single_line = true
struct_lit_single_line = true
fn_single_line = false
where_single_line = false
imports_indent = "Block"
imports_layout = "Mixed"
imports_granularity = "Preserve"
group_imports = "Preserve"
reorder_imports = true
reorder_modules = true
reorder_impl_items = false
type_punctuation_density = "Wide"
space_before_colon = false
space_after_colon = true
spaces_around_ranges = false
binop_separator = "Front"
remove_nested_parens = true
combine_control_expr = true
overflow_delimited_expr = false
struct_field_align_threshold = 0
enum_discrim_align_threshold = 0
match_arm_blocks = true
match_arm_leading_pipes = "Never"
force_multiline_blocks = false
fn_args_layout = "Tall"
brace_style = "SameLineWhere"
control_brace_style = "AlwaysSameLine"
trailing_semicolon = true
trailing_comma = "Vertical"
match_block_trailing_comma = false
blank_lines_upper_bound = 1
blank_lines_lower_bound = 0
edition = "2015"
version = "One"
inline_attribute_width = 0
merge_derives = true
use_try_shorthand = false
use_field_init_shorthand = false
force_explicit_abi = true
condense_wildcard_suffixes = false
color = "Auto"
unstable_features = false
disable_all_formatting = false
skip_children = false
hide_parse_errors = false
error_on_line_overflow = false
error_on_unformatted = false
report_todo = "Never"
report_fixme = "Never"
ignore = []
emit_mode = "Files"
make_backup = false

BIN
screenshot.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View file

@ -1,12 +1,6 @@
# This file exists for legacy nix-shell
# https://nixos.wiki/wiki/Flakes#Using_flakes_project_from_a_legacy_Nix
# You generally do *not* have to modify this ever.
(import (
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash; }
) {
src = ./.;
}).shellNix
{
pimalaya ? import (fetchTarball "https://github.com/pimalaya/nix/archive/master.tar.gz"),
...
}@args:
pimalaya.mkShell ({ rustToolchainFile = ./rust-toolchain.toml; } // removeAttrs args [ "pimalaya" ])

1
src/account/arg/mod.rs Normal file
View file

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

37
src/account/arg/name.rs Normal file
View file

@ -0,0 +1,37 @@
use clap::Parser;
/// The account name argument parser.
#[derive(Debug, Parser)]
pub struct AccountNameArg {
/// The name of the account.
///
/// An account name corresponds to an entry in the table at the
/// root level of your TOML configuration file.
#[arg(name = "account_name", value_name = "ACCOUNT")]
pub name: String,
}
/// The optional account name argument parser.
#[derive(Debug, Parser)]
pub struct OptionalAccountNameArg {
/// The name of the account.
///
/// An account name corresponds to an entry in the table at the
/// root level of your TOML configuration file.
///
/// If omitted, the account marked as default will be used.
#[arg(name = "account_name", value_name = "ACCOUNT")]
pub name: Option<String>,
}
/// The account name flag parser.
#[derive(Debug, Default, Parser)]
pub struct AccountNameFlag {
/// Override the default account.
///
/// An account name corresponds to an entry in the table at the
/// root level of your TOML configuration file.
#[arg(long = "account", short = 'a')]
#[arg(name = "account_name", value_name = "NAME")]
pub name: Option<String>,
}

View file

@ -0,0 +1,52 @@
use std::path::PathBuf;
use clap::Parser;
use color_eyre::Result;
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
/// Configure the given account.
///
/// This command allows you to configure an existing account or to
/// create a new one, using the wizard. The `wizard` cargo feature is
/// required.
#[derive(Debug, Parser)]
pub struct AccountConfigureCommand {
#[command(flatten)]
pub account: AccountNameArg,
}
impl AccountConfigureCommand {
#[cfg(feature = "wizard")]
pub async fn execute(
self,
mut config: TomlConfig,
config_path: Option<&PathBuf>,
) -> Result<()> {
use pimalaya_tui::{himalaya::wizard, terminal::config::TomlConfig as _};
use tracing::info;
info!("executing account configure command");
let path = match config_path {
Some(path) => path.clone(),
None => TomlConfig::default_path()?,
};
let account_name = Some(self.account.name.as_str());
let account_config = config
.accounts
.remove(&self.account.name)
.unwrap_or_default();
wizard::edit(path, config, account_name, account_config).await?;
Ok(())
}
#[cfg(not(feature = "wizard"))]
pub async fn execute(self, _: TomlConfig, _: Option<&PathBuf>) -> Result<()> {
color_eyre::eyre::bail!("This command requires the `wizard` cargo feature to work");
}
}

View file

@ -0,0 +1,233 @@
use std::{
io::{stdout, Write},
sync::Arc,
};
use clap::Parser;
use color_eyre::{Result, Section};
#[cfg(all(feature = "keyring", feature = "imap"))]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
#[cfg(feature = "maildir")]
use email::maildir::MaildirContextBuilder;
#[cfg(feature = "notmuch")]
use email::notmuch::NotmuchContextBuilder;
#[cfg(feature = "sendmail")]
use email::sendmail::SendmailContextBuilder;
#[cfg(all(feature = "keyring", feature = "smtp"))]
use email::smtp::config::SmtpAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::SmtpContextBuilder;
use email::{backend::BackendBuilder, config::Config};
#[cfg(feature = "keyring")]
use pimalaya_tui::terminal::prompt;
use pimalaya_tui::{
himalaya::config::{Backend, SendingBackend},
terminal::config::TomlConfig as _,
};
use crate::{account::arg::name::OptionalAccountNameArg, config::TomlConfig};
/// Diagnose and fix the given account.
///
/// This command diagnoses the given account and can even try to fix
/// it. It mostly checks if the configuration is valid, if backends
/// can be instanciated and if sessions work as expected.
#[derive(Debug, Parser)]
pub struct AccountDoctorCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
/// Try to fix the given account.
///
/// This argument can be used to (re)configure keyring entries for
/// example.
#[arg(long, short)]
pub fix: bool,
}
impl AccountDoctorCommand {
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
let mut stdout = stdout();
if let Some(name) = self.account.name.as_ref() {
print!("Checking TOML configuration integrity for account {name}");
} else {
print!("Checking TOML configuration integrity for default account… ");
}
stdout.flush()?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let account_config = Arc::new(account_config);
println!("OK");
#[cfg(feature = "keyring")]
if self.fix {
if prompt::bool("Would you like to reset existing keyring entries?", false)? {
print!("Resetting keyring entries… ");
stdout.flush()?;
#[cfg(feature = "imap")]
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => config.reset().await?,
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => config.reset().await?,
_ => (),
}
#[cfg(feature = "smtp")]
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => config.reset().await?,
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => config.reset().await?,
_ => (),
}
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config.reset().await?;
}
println!("OK");
}
#[cfg(feature = "imap")]
match toml_account_config.imap_auth_config() {
Some(ImapAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("IMAP password")?))
.await?;
}
#[cfg(feature = "oauth2")]
Some(ImapAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("IMAP OAuth 2.0 client secret")?))
.await?;
}
_ => (),
};
#[cfg(feature = "smtp")]
match toml_account_config.smtp_auth_config() {
Some(SmtpAuthConfig::Password(config)) => {
config
.configure(|| Ok(prompt::password("SMTP password")?))
.await?;
}
#[cfg(feature = "oauth2")]
Some(SmtpAuthConfig::OAuth2(config)) => {
config
.configure(|| Ok(prompt::secret("SMTP OAuth 2.0 client secret")?))
.await?;
}
_ => (),
};
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
if let Some(config) = &toml_account_config.pgp {
config
.configure(&toml_account_config.email, || {
Ok(prompt::password("PGP secret key password")?)
})
.await?;
}
}
match toml_account_config.backend {
#[cfg(feature = "maildir")]
Some(Backend::Maildir(mdir_config)) => {
print!("Checking Maildir integrity… ");
stdout.flush()?;
let ctx = MaildirContextBuilder::new(account_config.clone(), Arc::new(mdir_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
#[cfg(feature = "imap")]
Some(Backend::Imap(imap_config)) => {
print!("Checking IMAP integrity… ");
stdout.flush()?;
let ctx = ImapContextBuilder::new(account_config.clone(), Arc::new(imap_config))
.with_pool_size(1);
let res = BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await;
if self.fix {
res?;
} else {
res.note("Run with --fix to (re)configure your account.")?;
}
println!("OK");
}
#[cfg(feature = "notmuch")]
Some(Backend::Notmuch(notmuch_config)) => {
print!("Checking Notmuch integrity… ");
stdout.flush()?;
let ctx =
NotmuchContextBuilder::new(account_config.clone(), Arc::new(notmuch_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
_ => (),
}
let sending_backend = toml_account_config
.message
.and_then(|msg| msg.send)
.and_then(|send| send.backend);
match sending_backend {
#[cfg(feature = "smtp")]
Some(SendingBackend::Smtp(smtp_config)) => {
print!("Checking SMTP integrity… ");
stdout.flush()?;
let ctx = SmtpContextBuilder::new(account_config.clone(), Arc::new(smtp_config));
let res = BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await;
if self.fix {
res?;
} else {
res.note("Run with --fix to (re)configure your account.")?;
}
println!("OK");
}
#[cfg(feature = "sendmail")]
Some(SendingBackend::Sendmail(sendmail_config)) => {
print!("Checking Sendmail integrity… ");
stdout.flush()?;
let ctx =
SendmailContextBuilder::new(account_config.clone(), Arc::new(sendmail_config));
BackendBuilder::new(account_config.clone(), ctx)
.check_up()
.await?;
println!("OK");
}
_ => (),
}
Ok(())
}
}

View file

@ -0,0 +1,41 @@
use clap::Parser;
use color_eyre::Result;
use pimalaya_tui::{
himalaya::config::{Accounts, AccountsTable},
terminal::cli::printer::Printer,
};
use tracing::info;
use crate::config::TomlConfig;
/// List all existing accounts.
///
/// This command lists all the accounts defined in your TOML
/// configuration file.
#[derive(Debug, Parser)]
pub struct AccountListCommand {
/// The maximum width the table should not exceed.
///
/// This argument will force the table not to exceed the given
/// width, in pixels. Columns may shrink with ellipsis in order to
/// fit the width.
#[arg(long = "max-width", short = 'w')]
#[arg(name = "table_max_width", value_name = "PIXELS")]
pub table_max_width: Option<u16>,
}
impl AccountListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list accounts command");
let accounts = Accounts::from(config.accounts.iter());
let table = AccountsTable::from(accounts)
.with_some_width(self.table_max_width)
.with_some_preset(config.account_list_table_preset())
.with_some_name_color(config.account_list_table_name_color())
.with_some_backends_color(config.account_list_table_backends_color())
.with_some_default_color(config.account_list_table_default_color());
printer.out(table)
}
}

View file

@ -0,0 +1,41 @@
mod configure;
mod doctor;
mod list;
use std::path::PathBuf;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
};
/// Configure, list and diagnose your accounts.
///
/// An account is a group of settings, identified by a unique
/// name. This subcommand allows you to manage your accounts.
#[derive(Debug, Subcommand)]
pub enum AccountSubcommand {
Configure(AccountConfigureCommand),
Doctor(AccountDoctorCommand),
List(AccountListCommand),
}
impl AccountSubcommand {
pub async fn execute(
self,
printer: &mut impl Printer,
config: TomlConfig,
config_path: Option<&PathBuf>,
) -> Result<()> {
match self {
Self::Configure(cmd) => cmd.execute(config, config_path).await,
Self::Doctor(cmd) => cmd.execute(&config).await,
Self::List(cmd) => cmd.execute(printer, &config).await,
}
}
}

3
src/account/config.rs Normal file
View file

@ -0,0 +1,3 @@
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
pub type TomlAccountConfig = HimalayaTomlAccountConfig;

3
src/account/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod arg;
pub mod command;
pub mod config;

156
src/cli.rs Normal file
View file

@ -0,0 +1,156 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use color_eyre::Result;
use pimalaya_tui::{
long_version,
terminal::{
cli::{
arg::path_parser,
printer::{OutputFmt, Printer},
},
config::TomlConfig as _,
},
};
use crate::{
account::command::AccountSubcommand,
completion::command::CompletionGenerateCommand,
config::TomlConfig,
envelope::command::EnvelopeSubcommand,
flag::command::FlagSubcommand,
folder::command::FolderSubcommand,
manual::command::ManualGenerateCommand,
message::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand,
},
};
#[derive(Parser, Debug)]
#[command(name = env!("CARGO_PKG_NAME"))]
#[command(author, version, about)]
#[command(long_version = long_version!())]
#[command(propagate_version = true, infer_subcommands = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<HimalayaCommand>,
/// Override the default configuration file path.
///
/// The given paths are shell-expanded then canonicalized (if
/// applicable). If the first path does not point to a valid file,
/// the wizard will propose to assist you in the creation of the
/// configuration file. Other paths are merged with the first one,
/// which allows you to separate your public config from your
/// private(s) one(s).
#[arg(short, long = "config", global = true, env = "HIMALAYA_CONFIG")]
#[arg(value_name = "PATH", value_parser = path_parser)]
pub config_paths: Vec<PathBuf>,
/// Customize the output format.
///
/// The output format determine how to display commands output to
/// the terminal.
///
/// The possible values are:
///
/// - json: output will be in a form of a JSON-compatible object
///
/// - plain: output will be in a form of either a plain text or
/// table, depending on the command
#[arg(long, short, global = true)]
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
pub output: OutputFmt,
/// Enable logs with spantrace.
///
/// This is the same as running the command with `RUST_LOG=debug`
/// environment variable.
#[arg(long, global = true, conflicts_with = "trace")]
pub debug: bool,
/// Enable verbose logs with backtrace.
///
/// This is the same as running the command with `RUST_LOG=trace`
/// and `RUST_BACKTRACE=1` environment variables.
#[arg(long, global = true, conflicts_with = "debug")]
pub trace: bool,
}
#[derive(Subcommand, Debug)]
pub enum HimalayaCommand {
#[command(subcommand)]
#[command(alias = "accounts")]
Account(AccountSubcommand),
#[command(subcommand)]
#[command(visible_alias = "mailbox", aliases = ["mailboxes", "mboxes", "mbox"])]
#[command(alias = "folders")]
Folder(FolderSubcommand),
#[command(subcommand)]
#[command(alias = "envelopes")]
Envelope(EnvelopeSubcommand),
#[command(subcommand)]
#[command(alias = "flags")]
Flag(FlagSubcommand),
#[command(subcommand)]
#[command(alias = "messages", alias = "msgs", alias = "msg")]
Message(MessageSubcommand),
#[command(subcommand)]
#[command(alias = "attachments")]
Attachment(AttachmentSubcommand),
#[command(subcommand)]
#[command(alias = "templates", alias = "tpls", alias = "tpl")]
Template(TemplateSubcommand),
#[command(arg_required_else_help = true)]
#[command(alias = "manuals", alias = "mans")]
Manual(ManualGenerateCommand),
#[command(arg_required_else_help = true)]
#[command(alias = "completions")]
Completion(CompletionGenerateCommand),
}
impl HimalayaCommand {
pub async fn execute(self, printer: &mut impl Printer, config_paths: &[PathBuf]) -> Result<()> {
match self {
Self::Account(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, config, config_paths.first()).await
}
Self::Folder(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Envelope(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Flag(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Message(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Attachment(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Template(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
}
Self::Manual(cmd) => cmd.execute(printer).await,
Self::Completion(cmd) => cmd.execute().await,
}
}
}

32
src/completion/command.rs Normal file
View file

@ -0,0 +1,32 @@
use std::io;
use clap::{value_parser, CommandFactory, Parser};
use clap_complete::Shell;
use color_eyre::Result;
use tracing::info;
use crate::cli::Cli;
/// Print completion script for the given shell to stdout.
///
/// This command allows you to generate completion script for a given
/// shell. The script is printed to the standard output. If you want
/// to write it to a file, just use unix redirection.
#[derive(Debug, Parser)]
pub struct CompletionGenerateCommand {
/// Shell for which completion script should be generated for.
#[arg(value_parser = value_parser!(Shell))]
pub shell: Shell,
}
impl CompletionGenerateCommand {
pub async fn execute(self) -> Result<()> {
info!("executing generate completion command");
let mut cmd = Cli::command();
let name = cmd.get_name().to_string();
clap_complete::generate(self.shell, &mut cmd, name, &mut io::stdout());
Ok(())
}
}

1
src/completion/mod.rs Normal file
View file

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

Some files were not shown because too many files have changed in this diff Show more