Compare commits

...

472 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
166 changed files with 9620 additions and 9493 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,17 +0,0 @@
---
name: Do not open issues on GitHub
about: Instead send an email at ~soywod/pimalaya@todo.sr.ht
title: ''
labels: invalid
assignees: ''
---
Himalaya is slowly migrating away from GitHub. The new bug tracker is
now on [sourcehut](https://sr.ht/). You can submit an issue either by:
* Sending an email at
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht)
(it is the simplest since you do not need to create any account)
* Submitting [this form](https://todo.sr.ht/~soywod/pimalaya) (you
need a free sourcehut account)

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

View file

@ -1,150 +0,0 @@
name: release
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:
- target: linux
os: ubuntu-latest
- target: macos
os: macos-latest
- target: musl
os: ubuntu-latest
# TODO: put back when nix package .#windows is fixed
# - target: windows
# os: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v22
with:
nix_path: nixpkgs=channel:nixos-22.11
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with:
name: soywod
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build release
run: nix build .#${{ matrix.target }}
- name: Compress executable
run: |
mkdir -p {man,completions}
cp result/bin/himalaya* .
nix run .#${{ matrix.target }} man ./man
nix run .#${{ matrix.target }} completion bash > ./completions/himalaya.bash
nix run .#${{ matrix.target }} completion elvish > ./completions/himalaya.elvish
nix run .#${{ matrix.target }} completion fish > ./completions/himalaya.fish
nix run .#${{ matrix.target }} completion powershell > ./completions/himalaya.powershell
nix run .#${{ matrix.target }} completion zsh > ./completions/himalaya.zsh
tar -czf himalaya.tgz himalaya* man completions
zip -r himalaya.zip himalaya* man completions
- name: Upload tar.gz 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.tgz
asset_name: himalaya-${{ matrix.target }}.tgz
asset_content_type: application/gzip
- name: Upload zip 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.zip
asset_name: himalaya-${{ matrix.target }}.zip
asset_content_type: application/zip
# TODO: remove me when nix package .#windows is fixed
deploy_windows_github:
runs-on: windows-latest
needs: create_release
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Builds release
uses: actions-rs/cargo@v1
with:
command: build
args: --release
- name: Compress executable
run: |
mkdir man
mkdir completions
copy target/release/himalaya.exe .
./himalaya.exe man ./man
./himalaya.exe completion bash > ./completions/himalaya.bash
./himalaya.exe completion elvish > ./completions/himalaya.elvish
./himalaya.exe completion fish > ./completions/himalaya.fish
./himalaya.exe completion powershell > ./completions/himalaya.powershell
./himalaya.exe completion zsh > ./completions/himalaya.zsh
tar -czf himalaya.tgz himalaya.exe man completions
Compress-Archive -Path himalaya.exe,man,completions -DestinationPath himalaya.zip
- name: Upload tar.gz 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.tgz
asset_name: himalaya-windows.tgz
asset_content_type: application/gzip
- name: Upload zip 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.zip
asset_name: himalaya-windows.zip
asset_content_type: application/zip
deploy_crates:
runs-on: ubuntu-latest
needs: create_release
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v22
with:
nix_path: nixpkgs=channel:nixos-22.11
extra_nix_config: |
experimental-features = nix-command flakes
- name: Publish library to crates.io
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
run: nix develop -c cargo publish --no-verify --token ${CARGO_REGISTRY_TOKEN}

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,24 +0,0 @@
name: tests
on:
pull_request:
push:
jobs:
tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v20
with:
nix_path: nixpkgs=channel:nixos-22.11
extra_nix_config: |
experimental-features = nix-command flakes
- uses: cachix/cachix-action@v12
with:
name: soywod
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build then test
run: nix build

File diff suppressed because it is too large Load diff

View file

@ -1,49 +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!
## Development
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 tool:
`cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`
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.
```sh
# starts a nix shell (the first launch may take a while)
$ nix-shell
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
# builds the CLI
$ cargo build
# runs the CLI
$ cargo run -- list
```text
rustup update
```
## Contributing
or install manually the following dependencies:
If you find a **bug**, please send an email at
[~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
If you have a **question**, please send an email at
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
## Build
If you want to **propose a feature** or **fix a bug**, please send a
patch at
[~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht)
using [git send-email](https://git-scm.com/docs/git-send-email) (see
[this guide](https://git-send-email.io/) on how to configure it).
```text
cargo build
```
If you want to **subscribe** to the mailing list, please send an email
at
[~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht).
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`.
If you want to **unsubscribe** to the mailing list, please send an
email at
[~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
Finally, you can build a release with `--release`:
If you want to **discuss** about the project, feel free to join the
[Matrix](https://matrix.org/) workspace
[#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
or contact me directly
[@soywod](https://matrix.to/#/@soywod:matrix.org).
```text
cargo build --no-default-features --features imap,smtp,keyring --release
```
## Override dependencies
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`:
```toml
[patch.crates-io]
email-lib = { path = "/path/to/email-lib" }
```
If you get the following error:
```text
note: perhaps two different versions of crate email are being used?
```
then you may need to override more Pimalaya's sub-dependencies:
```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).

5003
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,71 +1,70 @@
[package]
name = "himalaya"
description = "CLI to manage your emails."
version = "0.8.3"
description = "CLI to manage emails"
version = "1.1.0"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021"
license = "MIT"
categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "mail", "email", "client", "imap"]
homepage = "https://pimalaya.org/himalaya/"
documentation = "https://pimalaya.org/himalaya/"
repository = "https://github.com/soywod/himalaya"
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]
all-features = true
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = ["imap-backend", "smtp-sender"]
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"]
# backends
imap-backend = ["pimalaya-email/imap-backend"]
notmuch-backend = ["pimalaya-email/notmuch-backend"]
# senders
smtp-sender = ["pimalaya-email/smtp-sender"]
[build-dependencies]
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
[dev-dependencies]
async-trait = "0.1"
tempfile = "3.3"
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
[dependencies]
anyhow = "1.0"
atty = "0.2"
chrono = "0.4.24"
clap = "4.0"
clap_complete = "4.0"
ariadne = "0.2"
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
console = "0.15.2"
dialoguer = "0.10.2"
dirs = "4.0.0"
email_address = "0.2.4"
env_logger = "0.8"
erased-serde = "0.3"
indicatif = "0.17"
log = "0.4"
md5 = "0.7.0"
once_cell = "1.16.0"
pimalaya-email = "=0.14.0"
pimalaya-keyring = "=0.0.5"
pimalaya-oauth2 = "=0.0.3"
pimalaya-process = "=0.0.5"
pimalaya-secret = "=0.0.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shellexpand = "2.1"
termcolor = "1.1"
terminal_size = "0.1"
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.7.4"
toml_edit = "0.19.8"
unicode-width = "0.1"
toml = "0.8"
tracing = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[target.'cfg(target_env = "musl")'.dependencies]
rusqlite = { version = "0.29", features = [] }
[target.'cfg(not(target_env = "musl"))'.dependencies]
rusqlite = { version = "0.29", features = ["bundled"] }
[patch.crates-io]
imap-codec.git = "https://github.com/duesee/imap-codec"
[target.'cfg(not(target_env = "windows"))'.dependencies]
coredump = "=0.1.2"
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"

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 soywod <clement.douin@posteo.net>
Copyright (c) 2022-2024 soywod <clement.douin@posteo.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

737
README.md
View file

@ -1,119 +1,694 @@
# 📫 Himalaya [![GitHub release](https://img.shields.io/github/v/release/soywod/himalaya?color=success)](https://github.com/soywod/himalaya/releases/latest) [![Matrix](https://img.shields.io/matrix/pimalaya.himalaya:matrix.org?color=success&label=chat)](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
<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>
https://pimalaya.org/himalaya/
```
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
```
CLI to manage your emails, based on the [pimalaya-email](https://sr.ht/~soywod/pimalaya/) library.
![image](https://user-images.githubusercontent.com/10437171/138774902-7b9de5a3-93eb-44b0-8cfb-6d2e11e3b1aa.png)
*Disclaimer: the project is under active development, do not use in production before the `v1.0.0`.*
![screenshot](./screenshot.jpeg)
## Features
- [Folder listing]
- [Envelopes listing], [searching] and [sorting]
- [Email composition] based on `$EDITOR`
- Email manipulation ([copy]/[move]/[delete])
- [Multi-accounting]
- [Account listing]
- [Account synchronization] for offline usage
- IMAP, Maildir and Notmuch support
- IMAP IDLE mode for [real-time notifications]
- PGP end-to-end encryption
- [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)
[Folder listing]: https://pimalaya.org/himalaya/cli/usage/folders/list.html
[Envelopes listing]: https://pimalaya.org/himalaya/cli/usage/envelopes/list.html
[searching]: https://pimalaya.org/himalaya/cli/usage/envelopes/search.html
[sorting]: https://pimalaya.org/himalaya/cli/usage/envelopes/sort.html
[Email composition]: https://pimalaya.org/himalaya/cli/usage/emails/write.html
[copy]: https://pimalaya.org/himalaya/cli/usage/emails/copy.html
[move]: https://pimalaya.org/himalaya/cli/usage/emails/move.html
[delete]: https://pimalaya.org/himalaya/cli/usage/emails/delete.html
[Multi-accounting]: https://pimalaya.org/himalaya/cli/configuration/index.html
[Account listing]: https://pimalaya.org/himalaya/cli/usage/accounts/list.html
[Account synchronization]: https://pimalaya.org/himalaya/cli/usage/accounts/synchronize.html
[real-time notifications]: https://pimalaya.org/himalaya/cli/usage/notifications.html
[Completions]: https://pimalaya.org/himalaya/cli/tips/completion.html
*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
<table align="center">
<tr>
<td width="50%">
<a href="https://repology.org/project/himalaya/versions">
<img src="https://repology.org/badge/vertical-allrepos/himalaya.svg" alt="Packaging status" />
</a>
</td>
<td width="50%">
<details>
<summary>Pre-built binary</summary>
```bash
# Arch Linux (official)
$ pacman -S himalaya
Himalaya CLI can be installed with the installer:
# Arch Linux (from sources)
$ yay -S himalaya-git
*As root:*
# Homebrew
$ brew install himalaya
```
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | sudo sh
```
# Scoop
$ scoop install himalaya
*As a regular user:*
# Cargo
$ cargo install himalaya
```
curl -sSL https://raw.githubusercontent.com/pimalaya/himalaya/master/install.sh | PREFIX=~/.local sh
```
# Nix
$ nix-env -i himalaya
```
These commands install the latest binary from the GitHub [releases](https://github.com/pimalaya/himalaya/releases) section.
*See the [documentation](https://pimalaya.org/himalaya/cli/installation/index.html) for other installation methods.*
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.
</td>
</tr>
</table>
*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
Please read the [documentation](https://pimalaya.org/himalaya/cli/configuration/index.html).
Just run `himalaya`, the wizard will help you to configure your default account.
## Contributing
Accounts can be (re)configured via the wizard using the command `himalaya account configure <name>`.
If you find a **bug** that [does not exist yet](https://todo.sr.ht/~soywod/pimalaya), please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.sr.ht).
You can also manually edit your own configuration, from scratch:
If you have a **question**, please send an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht).
- 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
If you want to **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht) using [git send-email](https://git-scm.com/docs/git-send-email) (see [this guide](https://git-send-email.io/) on how to configure it).
<details>
<summary>Proton Mail (Bridge)</summary>
If you want to **subscribe** to the mailing list, please send an email at [~soywod/pimalaya+subscribe@lists.sr.ht](mailto:~soywod/pimalaya+subscribe@lists.sr.ht).
When using Proton Bridge, emails are synchronized locally and exposed via a local IMAP/SMTP server. This implies 2 things:
If you want to **unsubscribe** to the mailing list, please send an email at [~soywod/pimalaya+unsubscribe@lists.sr.ht](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht).
- 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.
If you want to **discuss** about the project, feel free to join the [Matrix](https://matrix.org/) workspace [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org) or contact me directly [@soywod](https://matrix.to/#/@soywod:matrix.org).
```toml
[accounts.proton]
email = "example@proton.me"
## Credits
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 = "*****"
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
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 = "*****"
```
Special thanks to the [nlnet](https://nlnet.nl/project/Himalaya/index.html) foundation that helped Himalaya to receive financial support from the [NGI Assure](https://www.ngi.eu/ngi-projects/ngi-assure/) program of the European Commission in September, 2022.
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
* [himalaya-lib](https://git.sr.ht/~soywod/himalaya-lib)
* [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 library
* [lettre](https://github.com/lettre/lettre), a Rust mailer library
* [mailparse](https://github.com/staktrace/mailparse), a Rust MIME email parser.
- 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
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/)
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:
- [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)
[![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)
[![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,54 +1,651 @@
display-name = "Display NAME"
signature-delim = "~~"
signature = "~/.signature"
downloads-dir = "~/downloads"
folder-listing-page-size = 12
email-listing-page-size = 12
email-reading-headers = ["From", "To"]
email-reading-verify-cmd = "gpg --verify -q"
email-reading-decrypt-cmd = "gpg -dq"
email-writing-sign-cmd = "gpg -o - -saq"
email-writing-encrypt-cmd = "gpg -o - -eqar <recipient>"
################################################################################
###[ Global configuration ]#####################################################
################################################################################
[example]
# 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
display-name = "Display NAME (gmail)"
email = "display.name@gmail.local"
backend = "imap"
imap-host = "imap.gmail.com"
imap-login = "display.name@gmail.local"
imap-auth = "passwd"
imap-passwd.cmd = "pass show gmail"
imap-port = 993
imap-ssl = true
imap-starttls = false
imap-notify-cmd = """📫 "<sender>" "<subject>""""
imap-notify-query = "NOT SEEN"
imap-watch-cmds = ["echo \"received server changes!\""]
# The email address associated to the current account.
#
email = "example@localhost"
sender = "smtp"
smtp-host = "smtp.gmail.com"
smtp-login = "display.name@gmail.local"
smtp-auth = "passwd"
smtp-passwd.cmd = "pass show piana/gmail"
smtp-port = 465
smtp-ssl = true
smtp-starttls = false
# The display name of the account. This and the email are used to
# build the full email address: "Example" <example@localhost>
#
display-name = "Example"
sync = true
sync-dir = "/tmp/sync/gmail"
sync-folders-strategy.include = ["INBOX"]
# 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"
[example.folder-aliases]
inbox = "INBOX"
drafts = "[Gmail]/Drafts"
sent = "[Gmail]/Sent Mail"
trash = "[Gmail]/Trash"
# Signature delimiter. It delimits the end of the message body from
# the signature.
#
signature-delim = "-- \n"
[example.email-hooks]
pre-send = "echo $1"
# Downloads directory path. It is mostly used for downloading
# attachments. Defaults to the system temporary directory.
#
downloads-dir = "~/downloads"
[example.email-reading-format]
type = "fixed"
width = 64
# 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" ]
)

136
flake.lock generated
View file

@ -8,127 +8,66 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1686032467,
"narHash": "sha256-KUCS237H0G1QGx5ehhEmh5yKtcDGCxvVXVtz8xEDAKE=",
"owner": "nix-community",
"lastModified": 1732405626,
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
"owner": "soywod",
"repo": "fenix",
"rev": "1a3e0f661119a7435099b118912d65bdbbf3bb11",
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
"type": "github"
},
"original": {
"owner": "nix-community",
"owner": "soywod",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1685518550,
"narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1660459072,
"narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "a20de23b925fd8264fd7fad6454652e142fd7f73",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1679567394,
"narHash": "sha256-ZvLuzPeARDLiQUt6zSZFGOs+HZmE+3g4QURc8mkBsfM=",
"owner": "nix-community",
"repo": "naersk",
"rev": "88cd22380154a2c36799fe8098888f0f59861a15",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1685883127,
"narHash": "sha256-zPDaPNrAtBnO24rNqjHLINHsqTdRbgWy1c/TL3EdwlM=",
"lastModified": 1736437047,
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d4a9ff82fc18723219b60c66fb2ccb0734c460eb",
"rev": "f17b95775191ea44bc426831235d87affb10faba",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-22.11",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
},
"pimalaya": {
"flake": false,
"locked": {
"lastModified": 1737984647,
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
"owner": "pimalaya",
"repo": "nix",
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
"type": "github"
},
"original": {
"owner": "pimalaya",
"repo": "nix",
"type": "github"
}
},
"root": {
"inputs": {
"fenix": "fenix",
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"gitignore": "gitignore",
"naersk": "naersk",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"pimalaya": "pimalaya"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1685984106,
"narHash": "sha256-dOEuU1AuASOWdXT/SbVpD8uX7JjiW3lCp08SbviHuww=",
"lastModified": 1732050317,
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "d42d55feaafa71e14521bbfe6e7011fbb41980f0",
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
"type": "github"
},
"original": {
@ -137,21 +76,6 @@
"repo": "rust-analyzer",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

138
flake.nix
View file

@ -1,134 +1,26 @@
{
description = "CLI to manage your emails.";
description = "CLI to manage emails";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-22.11";
flake-utils.url = "github:numtide/flake-utils";
gitignore = {
url = "github:hercules-ci/gitignore.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
# FIXME: when #358989 lands on nixos-unstable
# https://nixpk.gs/pr-tracker.html?pr=358989
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
fenix = {
url = "github:nix-community/fenix";
# TODO: https://github.com/nix-community/fenix/pull/145
# url = "github:nix-community/fenix";
url = "github:soywod/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk = {
url = "github:nix-community/naersk";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-compat = {
url = "github:edolstra/flake-compat";
pimalaya = {
url = "github:pimalaya/nix";
flake = false;
};
};
outputs = { self, nixpkgs, flake-utils, gitignore, fenix, naersk, ... }:
let
inherit (gitignore.lib) gitignoreSource;
mkToolchain = import ./rust-toolchain.nix fenix;
mkDevShells = buildPlatform:
let
pkgs = import nixpkgs { system = buildPlatform; };
rust-toolchain = mkToolchain.fromFile { system = buildPlatform; };
in
{
default = pkgs.mkShell {
buildInputs = with pkgs; [
# Nix env
rnix-lsp
nixpkgs-fmt
# Rust env
rust-toolchain
# notmuch
notmuch
];
};
};
mkPackage = pkgs: buildPlatform: targetPlatform: package:
let
toolchain = mkToolchain.fromTarget {
inherit pkgs buildPlatform targetPlatform;
};
naersk' = naersk.lib.${buildPlatform}.override {
cargo = toolchain;
rustc = toolchain;
};
package' = {
name = "himalaya";
src = gitignoreSource ./.;
overrideMain = _: {
postInstall = ''
mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/
'';
};
doCheck = true;
cargoTestOptions = opts: opts ++ [ "--lib" ];
} // pkgs.lib.optionalAttrs (!isNull targetPlatform) {
CARGO_BUILD_TARGET = targetPlatform;
} // package;
in
naersk'.buildPackage package';
mkPackages = buildPlatform:
let
pkgs = import nixpkgs { system = buildPlatform; };
mkPackageWithTarget = mkPackage pkgs buildPlatform;
defaultPackage = mkPackage pkgs buildPlatform null { };
in
{
default = defaultPackage;
linux = defaultPackage;
macos = defaultPackage;
musl = mkPackageWithTarget "x86_64-unknown-linux-musl" (with pkgs.pkgsStatic; {
CARGO_BUILD_RUSTFLAGS = "-C target-feature=+crt-static";
SQLITE3_STATIC = 1;
SQLITE3_LIB_DIR = "${sqlite.out}/lib";
hardeningDisable = [ "all" ];
});
# FIXME: package does not build, assembler messages: unknown
# pseudo-op…
windows = mkPackageWithTarget "x86_64-pc-windows-gnu" {
strictDeps = true;
depsBuildBuild = with pkgs.pkgsCross.mingwW64; [
stdenv.cc
windows.pthreads
];
};
};
mkApp = drv: flake-utils.lib.mkApp {
inherit drv;
name = "himalaya";
};
mkApps = buildPlatform: {
default = mkApp self.packages.${buildPlatform}.default;
linux = mkApp self.packages.${buildPlatform}.linux;
macos = mkApp self.packages.${buildPlatform}.macos;
musl = mkApp self.packages.${buildPlatform}.musl;
windows =
let
pkgs = import nixpkgs { system = buildPlatform; };
wine = pkgs.wine.override { wineBuild = "wine64"; };
himalaya = self.packages.${buildPlatform}.windows;
app = pkgs.writeShellScriptBin "himalaya" ''
export WINEPREFIX="$(mktemp -d)"
${wine}/bin/wine64 ${himalaya}/bin/himalaya.exe $@
'';
in
mkApp app;
};
in
flake-utils.lib.eachDefaultSystem (system: {
devShells = mkDevShells system;
packages = mkPackages system;
apps = mkApps system;
});
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"

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
];
};
}

View file

@ -1,27 +0,0 @@
fenix:
let
file = ./rust-toolchain.toml;
sha256 = "ks0nMEGGXKrHnfv4Fku+vhQ7gx76ruv6Ij4fKZR3l78=";
in
{
fromFile = { system }: fenix.packages.${system}.fromToolchainFile {
inherit file sha256;
};
fromTarget = { pkgs, buildPlatform, targetPlatform ? null }:
let
inherit ((pkgs.lib.importTOML file).toolchain) channel;
toolchain = fenix.packages.${buildPlatform};
in
if
isNull targetPlatform
then
fenix.packages.${buildPlatform}.${channel}.toolchain
else
toolchain.combine [
toolchain.${channel}.rustc
toolchain.${channel}.cargo
toolchain.targets.${targetPlatform}.${channel}.rust-std
];
}

View file

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

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;

24
src/cache/args.rs vendored
View file

@ -1,24 +0,0 @@
//! This module provides arguments related to the cache.
use clap::{Arg, ArgAction, ArgMatches};
const ARG_DISABLE_CACHE: &str = "disable-cache";
/// Represents the disable cache flag argument. This argument allows
/// the user to disable any sort of cache.
pub fn arg() -> Arg {
Arg::new(ARG_DISABLE_CACHE)
.help("Disable any sort of cache")
.long_help(
"Disable any sort of cache. The action depends on
the command it applies on.",
)
.long("disable-cache")
.global(true)
.action(ArgAction::SetTrue)
}
/// Represents the disable cache flag parser.
pub fn parse_disable_cache_flag(m: &ArgMatches) -> bool {
m.get_flag(ARG_DISABLE_CACHE)
}

192
src/cache/id_mapper.rs vendored
View file

@ -1,192 +0,0 @@
use anyhow::{anyhow, Context, Result};
use log::{debug, trace};
#[cfg(feature = "imap-backend")]
use pimalaya_email::backend::ImapBackend;
#[cfg(feature = "notmuch-backend")]
use pimalaya_email::backend::NotmuchBackend;
use pimalaya_email::{
account::AccountConfig,
backend::{Backend, MaildirBackend},
};
use std::path::{Path, PathBuf};
const ID_MAPPER_DB_FILE_NAME: &str = ".id-mapper.sqlite";
#[derive(Debug)]
pub enum IdMapper {
Dummy,
Mapper(String, rusqlite::Connection),
}
impl IdMapper {
pub fn find_closest_db_path(dir: impl AsRef<Path>) -> PathBuf {
let mut db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
let mut db_parent_dir = dir.as_ref().parent();
while !db_path.is_file() {
match db_parent_dir {
Some(dir) => {
db_path = dir.join(ID_MAPPER_DB_FILE_NAME);
db_parent_dir = dir.parent();
}
None => {
db_path = dir.as_ref().join(ID_MAPPER_DB_FILE_NAME);
break;
}
}
}
db_path
}
pub fn new(
backend: &dyn Backend,
account_config: &AccountConfig,
folder: &str,
) -> Result<Self> {
#[cfg(feature = "imap-backend")]
if backend.as_any().is::<ImapBackend>() {
return Ok(IdMapper::Dummy);
}
let mut db_path = PathBuf::new();
if let Some(backend) = backend.as_any().downcast_ref::<MaildirBackend>() {
db_path = Self::find_closest_db_path(backend.path())
}
#[cfg(feature = "notmuch-backend")]
if let Some(backend) = backend.as_any().downcast_ref::<NotmuchBackend>() {
db_path = Self::find_closest_db_path(backend.path())
}
let folder = account_config.get_folder_alias(folder)?;
let digest = md5::compute(account_config.name.clone() + &folder);
let table = format!("id_mapper_{digest:x}");
debug!("creating id mapper table {table} at {db_path:?}…");
let conn = rusqlite::Connection::open(&db_path)
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
let query = format!(
"CREATE TABLE IF NOT EXISTS {table} (
id INTEGER PRIMARY KEY AUTOINCREMENT,
internal_id TEXT UNIQUE
)",
);
trace!("create table query: {query:#?}");
conn.execute(&query, [])
.context("cannot create id mapper table")?;
Ok(Self::Mapper(table, conn))
}
pub fn create_alias<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
let id = id.as_ref();
match self {
Self::Dummy => Ok(id.to_owned()),
Self::Mapper(table, conn) => {
debug!("creating alias for id {id}…");
let query = format!("INSERT OR IGNORE INTO {} (internal_id) VALUES (?)", table);
trace!("insert query: {query:#?}");
conn.execute(&query, [id])
.with_context(|| format!("cannot create id alias for id {id}"))?;
let alias = conn.last_insert_rowid().to_string();
debug!("created alias {alias} for id {id}");
Ok(alias)
}
}
}
pub fn get_or_create_alias<I>(&self, id: I) -> Result<String>
where
I: AsRef<str>,
{
let id = id.as_ref();
match self {
Self::Dummy => Ok(id.to_owned()),
Self::Mapper(table, conn) => {
debug!("getting alias for id {id}…");
let query = format!("SELECT id FROM {} WHERE internal_id = ?", table);
trace!("select query: {query:#?}");
let mut stmt = conn
.prepare(&query)
.with_context(|| format!("cannot get alias for id {id}"))?;
let aliases: Vec<i64> = stmt
.query_map([id], |row| row.get(0))
.with_context(|| format!("cannot get alias for id {id}"))?
.collect::<rusqlite::Result<_>>()
.with_context(|| format!("cannot get alias for id {id}"))?;
let alias = match aliases.first() {
Some(alias) => {
debug!("found alias {alias} for id {id}");
alias.to_string()
}
None => {
debug!("alias not found, creating it…");
self.create_alias(id)?
}
};
Ok(alias)
}
}
}
pub fn get_id<A>(&self, alias: A) -> Result<String>
where
A: AsRef<str>,
{
let alias = alias.as_ref();
let alias = alias
.parse::<i64>()
.context(format!("cannot parse id mapper alias {alias}"))?;
match self {
Self::Dummy => Ok(alias.to_string()),
Self::Mapper(table, conn) => {
debug!("getting id from alias {alias}…");
let query = format!("SELECT internal_id FROM {} WHERE id = ?", table);
trace!("select query: {query:#?}");
let mut stmt = conn
.prepare(&query)
.with_context(|| format!("cannot get id from alias {alias}"))?;
let ids: Vec<String> = stmt
.query_map([alias], |row| row.get(0))
.with_context(|| format!("cannot get id from alias {alias}"))?
.collect::<rusqlite::Result<_>>()
.with_context(|| format!("cannot get id from alias {alias}"))?;
let id = ids
.first()
.ok_or_else(|| anyhow!("cannot get id from alias {alias}"))?
.to_owned();
debug!("found id {id} from alias {alias}");
Ok(id)
}
}
}
pub fn get_ids<A, I>(&self, aliases: I) -> Result<Vec<String>>
where
A: AsRef<str>,
I: IntoIterator<Item = A>,
{
aliases
.into_iter()
.map(|alias| self.get_id(alias))
.collect()
}
}

4
src/cache/mod.rs vendored
View file

@ -1,4 +0,0 @@
pub mod args;
mod id_mapper;
pub use id_mapper::IdMapper;

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

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::{value_parser, Arg, ArgMatches, Command};
use clap_complete::Shell;
use log::debug;
const ARG_SHELL: &str = "shell";
const CMD_COMPLETION: &str = "completion";
type SomeShell = Shell;
/// Completion commands.
pub enum Cmd {
/// Generate completion script for the given shell.
Generate(SomeShell),
}
/// Completion command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_COMPLETION) {
let shell = m.get_one::<Shell>(ARG_SHELL).cloned().unwrap();
debug!("shell: {:?}", shell);
return Ok(Some(Cmd::Generate(shell)));
};
Ok(None)
}
/// Completion subcommands.
pub fn subcmd() -> Command {
Command::new(CMD_COMPLETION)
.about("Generates the completion script for the given shell")
.args(&[Arg::new(ARG_SHELL)
.value_parser(value_parser!(Shell))
.required(true)])
}

View file

@ -1,15 +0,0 @@
//! Module related to completion handling.
//!
//! This module gathers all completion commands.
use anyhow::Result;
use clap::Command;
use clap_complete::Shell;
use std::io::stdout;
/// Generates completion script from the given [`clap::App`] for the given shell slice.
pub fn generate<'a>(mut cmd: Command, shell: Shell) -> Result<()> {
let name = cmd.get_name().to_string();
clap_complete::generate(shell, &mut cmd, name, &mut stdout());
Ok(())
}

View file

@ -1,8 +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](https://docs.rs/clap/2.33.3/clap/enum.Shell.html).
pub mod args;
pub mod handlers;

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;

3
src/config.rs Normal file
View file

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

View file

@ -1,21 +0,0 @@
//! This module provides arguments related to the user config.
use clap::{Arg, ArgMatches};
const ARG_CONFIG: &str = "config";
/// Represents the config file path argument. This argument allows the
/// user to customize the config file path.
pub fn arg() -> Arg {
Arg::new(ARG_CONFIG)
.help("Set a custom configuration file path")
.long("config")
.short('c')
.global(true)
.value_name("PATH")
}
/// Represents the config file path argument parser.
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_CONFIG).map(String::as_str)
}

View file

@ -1,620 +0,0 @@
//! Deserialized config module.
//!
//! This module contains the raw deserialized representation of the
//! user configuration file.
use anyhow::{anyhow, Context, Result};
use dialoguer::Confirm;
use dirs::{config_dir, home_dir};
use log::{debug, trace};
use pimalaya_email::{
account::AccountConfig,
email::{EmailHooks, EmailTextPlainFormat},
};
use pimalaya_process::Cmd;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf, process};
use toml;
use crate::{
account::DeserializedAccountConfig,
config::{prelude::*, wizard},
wizard_prompt, wizard_warn,
};
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
#[serde(alias = "name")]
pub display_name: Option<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_listing_datetime_fmt: Option<String>,
pub email_listing_datetime_local_tz: Option<bool>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(
default,
with = "EmailTextPlainFormatDef",
skip_serializing_if = "EmailTextPlainFormat::is_default"
)]
pub email_reading_format: EmailTextPlainFormat,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_verify_cmd: Option<Cmd>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_decrypt_cmd: Option<Cmd>,
pub email_writing_headers: Option<Vec<String>>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_writing_sign_cmd: Option<Cmd>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_writing_encrypt_cmd: Option<Cmd>,
pub email_sending_save_copy: Option<bool>,
#[serde(
default,
with = "EmailHooksDef",
skip_serializing_if = "EmailHooks::is_empty"
)]
pub email_hooks: EmailHooks,
#[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> {
debug!("path: {:?}", path);
let config = if let Some(path) = path.map(PathBuf::from).or_else(Self::path) {
let content = fs::read_to_string(path).context("cannot read config file")?;
toml::from_str(&content).context("cannot parse config file")?
} else {
wizard_warn!("Himalaya could not find an already existing configuration file.");
if !Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default()
{
process::exit(0);
}
wizard::configure()?
};
if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account"));
}
trace!("config: {:#?}", config);
Ok(config)
}
/// Tries to return a config path from a few default settings.
///
/// Tries paths in this order:
///
/// - `"$XDG_CONFIG_DIR/himalaya/config.toml"` (or equivalent to `$XDG_CONFIG_DIR` in other
/// OSes.)
/// - `"$HOME/.config/himalaya/config.toml"`
/// - `"$HOME/.himalayarc"`
///
/// Returns `Some(path)` if the path exists, otherwise `None`.
pub fn path() -> Option<PathBuf> {
config_dir()
.map(|p| p.join("himalaya").join("config.toml"))
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".config").join("himalaya").join("config.toml")))
.filter(|p| p.exists())
.or_else(|| home_dir().map(|p| p.join(".himalayarc")))
.filter(|p| p.exists())
}
pub fn to_account_config(&self, account_name: Option<&str>) -> Result<AccountConfig> {
let (account_name, deserialized_account_config) = match account_name {
Some("default") | Some("") | None => self
.accounts
.iter()
.find_map(|(name, account)| {
account
.default
.filter(|default| *default == true)
.map(|_| (name.clone(), account))
})
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => self
.accounts
.get(name)
.map(|account| (name.to_string(), account))
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
}?;
Ok(deserialized_account_config.to_account_config(account_name, self))
}
}
#[cfg(test)]
mod tests {
use pimalaya_email::{
account::PasswdConfig,
backend::{BackendConfig, MaildirConfig},
sender::{SenderConfig, SendmailConfig},
};
use pimalaya_secret::Secret;
#[cfg(feature = "notmuch-backend")]
use pimalaya_email::backend::NotmuchConfig;
#[cfg(feature = "imap-backend")]
use pimalaya_email::backend::{ImapAuthConfig, ImapConfig};
#[cfg(feature = "smtp-sender")]
use pimalaya_email::sender::{SmtpAuthConfig, SmtpConfig};
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
fn make_config(config: &str) -> Result<DeserializedConfig> {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", config).unwrap();
DeserializedConfig::from_opt_path(file.into_temp_path().to_str())
}
#[test]
fn empty_config() {
let config = make_config("");
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"config file must contain at least one account"
);
}
#[test]
fn account_missing_email_field() {
let config = make_config("[account]");
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `email`"));
}
#[test]
fn account_missing_backend_field() {
let config = make_config(
"[account]
email = \"test@localhost\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `backend`"));
}
#[test]
fn account_invalid_backend_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"bad\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("unknown variant `bad`"));
}
#[test]
fn imap_account_missing_host_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `imap-host`"));
}
#[test]
fn account_backend_imap_missing_port_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `imap-port`"));
}
#[test]
fn account_backend_imap_missing_login_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `imap-login`"));
}
#[test]
fn account_backend_imap_missing_passwd_cmd_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993
imap-login = \"login\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `imap-auth`"));
}
#[test]
fn account_backend_maildir_missing_root_dir_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"maildir\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `maildir-root-dir`"));
}
#[cfg(feature = "notmuch-backend")]
#[test]
fn account_backend_notmuch_missing_db_path_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"notmuch\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `notmuch-db-path`"));
}
#[test]
fn account_missing_sender_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `sender`"));
}
#[test]
fn account_invalid_sender_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"bad\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("unknown variant `bad`, expected one of `none`, `smtp`, `sendmail`"),);
}
#[test]
fn account_smtp_sender_missing_host_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `smtp-host`"));
}
#[test]
fn account_smtp_sender_missing_port_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `smtp-port`"));
}
#[test]
fn account_smtp_sender_missing_login_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `smtp-login`"));
}
#[test]
fn account_smtp_sender_missing_auth_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25
smtp-login = \"login\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `smtp-auth`"));
}
#[test]
fn account_sendmail_sender_missing_cmd_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"sendmail\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.contains("missing field `sendmail-cmd`"));
}
#[cfg(feature = "smtp-sender")]
#[test]
fn account_smtp_sender_minimum_config() {
use pimalaya_email::sender::SenderConfig;
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25
smtp-login = \"login\"
smtp-auth = \"passwd\"
smtp-passwd = { cmd = \"echo password\" }",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig {
email: "test@localhost".into(),
sender: SenderConfig::Smtp(SmtpConfig {
host: "localhost".into(),
port: 25,
login: "login".into(),
auth: SmtpAuthConfig::Passwd(PasswdConfig {
passwd: Secret::new_cmd(String::from("echo password"))
}),
..SmtpConfig::default()
}),
..DeserializedAccountConfig::default()
}
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_sendmail_sender_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"sendmail\"
sendmail-cmd = \"echo send\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig {
email: "test@localhost".into(),
sender: SenderConfig::Sendmail(SendmailConfig {
cmd: Cmd::from("echo send")
}),
..DeserializedAccountConfig::default()
}
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_backend_imap_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993
imap-login = \"login\"
imap-auth = \"passwd\"
imap-passwd = { cmd = \"echo password\" }",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig {
email: "test@localhost".into(),
backend: BackendConfig::Imap(ImapConfig {
host: "localhost".into(),
port: 993,
login: "login".into(),
auth: ImapAuthConfig::Passwd(PasswdConfig {
passwd: Secret::new_cmd(String::from("echo password"))
}),
..ImapConfig::default()
}),
..DeserializedAccountConfig::default()
}
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_backend_maildir_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"maildir\"
maildir-root-dir = \"/tmp/maildir\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig {
email: "test@localhost".into(),
backend: BackendConfig::Maildir(MaildirConfig {
root_dir: "/tmp/maildir".into(),
}),
..DeserializedAccountConfig::default()
}
)]),
..DeserializedConfig::default()
}
);
}
#[cfg(feature = "notmuch-backend")]
#[test]
fn account_backend_notmuch_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"notmuch\"
notmuch-db-path = \"/tmp/notmuch.db\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig {
email: "test@localhost".into(),
backend: BackendConfig::Notmuch(NotmuchConfig {
db_path: "/tmp/notmuch.db".into(),
}),
..DeserializedAccountConfig::default()
}
)]),
..DeserializedConfig::default()
}
);
}
}

View file

@ -1,6 +0,0 @@
pub mod args;
pub mod config;
pub mod prelude;
pub mod wizard;
pub use config::*;

View file

@ -1,389 +0,0 @@
#[cfg(feature = "notmuch-backend")]
use pimalaya_email::backend::NotmuchConfig;
#[cfg(feature = "imap-backend")]
use pimalaya_email::backend::{ImapAuthConfig, ImapConfig};
#[cfg(feature = "smtp-sender")]
use pimalaya_email::sender::{SmtpAuthConfig, SmtpConfig};
use pimalaya_email::{
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
backend::{BackendConfig, MaildirConfig},
email::{EmailHooks, EmailTextPlainFormat},
folder::sync::FolderSyncStrategy,
sender::{SenderConfig, SendmailConfig},
};
use pimalaya_keyring::Entry;
use pimalaya_process::{Cmd, Pipeline, SingleCmd};
use pimalaya_secret::Secret;
use serde::{ser::SerializeSeq, Deserialize, Serialize, Serializer};
use std::{collections::HashSet, ops::Deref, path::PathBuf};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Entry", from = "String")]
pub struct EntryDef(#[serde(getter = "Deref::deref")] String);
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "SingleCmd", from = "String")]
pub struct SingleCmdDef(#[serde(getter = "Deref::deref")] String);
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Pipeline", from = "Vec<String>")]
pub struct PipelineDef(
#[serde(getter = "Deref::deref", serialize_with = "pipeline")] Vec<SingleCmd>,
);
// NOTE: did not find the way to do it with macros…
pub fn pipeline<S>(cmds: &Vec<SingleCmd>, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = s.serialize_seq(Some(cmds.len()))?;
for cmd in cmds {
seq.serialize_element(&cmd.to_string())?;
}
seq.end()
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Cmd", untagged)]
pub enum CmdDef {
#[serde(with = "SingleCmdDef")]
SingleCmd(SingleCmd),
#[serde(with = "PipelineDef")]
Pipeline(Pipeline),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Option<Cmd>", from = "OptionCmd")]
pub struct OptionCmdDef;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum OptionCmd {
#[default]
#[serde(skip_serializing)]
None,
#[serde(with = "SingleCmdDef")]
SingleCmd(SingleCmd),
#[serde(with = "PipelineDef")]
Pipeline(Pipeline),
}
impl From<OptionCmd> for Option<Cmd> {
fn from(cmd: OptionCmd) -> Option<Cmd> {
match cmd {
OptionCmd::None => None,
OptionCmd::SingleCmd(cmd) => Some(Cmd::SingleCmd(cmd)),
OptionCmd::Pipeline(pipeline) => Some(Cmd::Pipeline(pipeline)),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "Secret", rename_all = "kebab-case")]
pub enum SecretDef {
Raw(String),
#[serde(with = "CmdDef")]
Cmd(Cmd),
#[serde(with = "EntryDef", rename = "keyring")]
KeyringEntry(Entry),
#[default]
Undefined,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "OAuth2Method")]
pub enum OAuth2MethodDef {
#[serde(rename = "xoauth2", alias = "XOAUTH2")]
XOAuth2,
#[serde(rename = "oauthbearer", alias = "OAUTHBEARER")]
OAuthBearer,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "BackendConfig", tag = "backend", rename_all = "kebab-case")]
pub enum BackendConfigDef {
#[default]
None,
#[cfg(feature = "imap-backend")]
#[serde(with = "ImapConfigDef")]
Imap(ImapConfig),
#[serde(with = "MaildirConfigDef")]
Maildir(MaildirConfig),
#[cfg(feature = "notmuch-backend")]
#[serde(with = "NotmuchConfigDef")]
Notmuch(NotmuchConfig),
}
#[cfg(feature = "imap-backend")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "ImapConfig")]
pub struct ImapConfigDef {
#[serde(rename = "imap-host")]
pub host: String,
#[serde(rename = "imap-port")]
pub port: u16,
#[serde(rename = "imap-ssl")]
pub ssl: Option<bool>,
#[serde(rename = "imap-starttls")]
pub starttls: Option<bool>,
#[serde(rename = "imap-insecure")]
pub insecure: Option<bool>,
#[serde(rename = "imap-login")]
pub login: String,
#[serde(flatten, with = "ImapAuthConfigDef")]
pub auth: ImapAuthConfig,
#[serde(rename = "imap-notify-cmd")]
pub notify_cmd: Option<String>,
#[serde(rename = "imap-notify-query")]
pub notify_query: Option<String>,
#[serde(rename = "imap-watch-cmds")]
pub watch_cmds: Option<Vec<String>>,
}
#[cfg(feature = "imap-backend")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "ImapAuthConfig", tag = "imap-auth")]
pub enum ImapAuthConfigDef {
#[serde(rename = "passwd", alias = "password", with = "ImapPasswdConfigDef")]
Passwd(#[serde(default)] PasswdConfig),
#[serde(rename = "oauth2", with = "ImapOAuth2ConfigDef")]
OAuth2(OAuth2Config),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "PasswdConfig")]
pub struct ImapPasswdConfigDef {
#[serde(
rename = "imap-passwd",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub passwd: Secret,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "OAuth2Config")]
pub struct ImapOAuth2ConfigDef {
#[serde(rename = "imap-oauth2-method", with = "OAuth2MethodDef", default)]
pub method: OAuth2Method,
#[serde(rename = "imap-oauth2-client-id")]
pub client_id: String,
#[serde(
rename = "imap-oauth2-client-secret",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub client_secret: Secret,
#[serde(rename = "imap-oauth2-auth-url")]
pub auth_url: String,
#[serde(rename = "imap-oauth2-token-url")]
pub token_url: String,
#[serde(
rename = "imap-oauth2-access-token",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub access_token: Secret,
#[serde(
rename = "imap-oauth2-refresh-token",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub refresh_token: Secret,
#[serde(flatten, with = "ImapOAuth2ScopesDef")]
pub scopes: OAuth2Scopes,
#[serde(rename = "imap-oauth2-pkce", default)]
pub pkce: bool,
#[serde(
rename = "imap-oauth2-redirect-host",
default = "OAuth2Config::default_redirect_host"
)]
pub redirect_host: String,
#[serde(
rename = "imap-oauth2-redirect-port",
default = "OAuth2Config::default_redirect_port"
)]
pub redirect_port: u16,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "OAuth2Scopes")]
pub enum ImapOAuth2ScopesDef {
#[serde(rename = "imap-oauth2-scope")]
Scope(String),
#[serde(rename = "imap-oauth2-scopes")]
Scopes(Vec<String>),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "MaildirConfig", rename_all = "kebab-case")]
pub struct MaildirConfigDef {
#[serde(rename = "maildir-root-dir")]
pub root_dir: PathBuf,
}
#[cfg(feature = "notmuch-backend")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "NotmuchConfig", rename_all = "kebab-case")]
pub struct NotmuchConfigDef {
#[serde(rename = "notmuch-db-path")]
pub db_path: PathBuf,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(
remote = "EmailTextPlainFormat",
tag = "type",
content = "width",
rename_all = "kebab-case"
)]
pub enum EmailTextPlainFormatDef {
#[default]
Auto,
Flowed,
Fixed(usize),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "SenderConfig", tag = "sender", rename_all = "kebab-case")]
pub enum SenderConfigDef {
#[default]
None,
#[cfg(feature = "smtp-sender")]
#[serde(with = "SmtpConfigDef")]
Smtp(SmtpConfig),
#[serde(with = "SendmailConfigDef")]
Sendmail(SendmailConfig),
}
#[cfg(feature = "smtp-sender")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "SmtpConfig")]
struct SmtpConfigDef {
#[serde(rename = "smtp-host")]
pub host: String,
#[serde(rename = "smtp-port")]
pub port: u16,
#[serde(rename = "smtp-ssl")]
pub ssl: Option<bool>,
#[serde(rename = "smtp-starttls")]
pub starttls: Option<bool>,
#[serde(rename = "smtp-insecure")]
pub insecure: Option<bool>,
#[serde(rename = "smtp-login")]
pub login: String,
#[serde(flatten, with = "SmtpAuthConfigDef")]
pub auth: SmtpAuthConfig,
}
#[cfg(feature = "smtp-sender")]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "SmtpAuthConfig", tag = "smtp-auth")]
pub enum SmtpAuthConfigDef {
#[serde(rename = "passwd", alias = "password", with = "SmtpPasswdConfigDef")]
Passwd(#[serde(default)] PasswdConfig),
#[serde(rename = "oauth2", with = "SmtpOAuth2ConfigDef")]
OAuth2(OAuth2Config),
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "PasswdConfig", default)]
pub struct SmtpPasswdConfigDef {
#[serde(
rename = "smtp-passwd",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub passwd: Secret,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "OAuth2Config")]
pub struct SmtpOAuth2ConfigDef {
#[serde(rename = "smtp-oauth2-method", with = "OAuth2MethodDef", default)]
pub method: OAuth2Method,
#[serde(rename = "smtp-oauth2-client-id")]
pub client_id: String,
#[serde(
rename = "smtp-oauth2-client-secret",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub client_secret: Secret,
#[serde(rename = "smtp-oauth2-auth-url")]
pub auth_url: String,
#[serde(rename = "smtp-oauth2-token-url")]
pub token_url: String,
#[serde(
rename = "smtp-oauth2-access-token",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub access_token: Secret,
#[serde(
rename = "smtp-oauth2-refresh-token",
with = "SecretDef",
default,
skip_serializing_if = "Secret::is_undefined"
)]
pub refresh_token: Secret,
#[serde(flatten, with = "SmtpOAuth2ScopesDef")]
pub scopes: OAuth2Scopes,
#[serde(rename = "smtp-oauth2-pkce", default)]
pub pkce: bool,
#[serde(
rename = "imap-oauth2-redirect-host",
default = "OAuth2Config::default_redirect_host"
)]
pub redirect_host: String,
#[serde(
rename = "imap-oauth2-redirect-port",
default = "OAuth2Config::default_redirect_port"
)]
pub redirect_port: u16,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "OAuth2Scopes")]
pub enum SmtpOAuth2ScopesDef {
#[serde(rename = "smtp-oauth2-scope")]
Scope(String),
#[serde(rename = "smtp-oauth2-scopes")]
Scopes(Vec<String>),
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "SendmailConfig", rename_all = "kebab-case")]
pub struct SendmailConfigDef {
#[serde(rename = "sendmail-cmd", with = "CmdDef")]
cmd: Cmd,
}
/// Represents the email hooks. Useful for doing extra email
/// processing before or after sending it.
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "EmailHooks", rename_all = "kebab-case")]
pub struct EmailHooksDef {
/// Represents the hook called just before sending an email.
#[serde(default, with = "OptionCmdDef")]
pub pre_send: Option<Cmd>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(remote = "FolderSyncStrategy", rename_all = "kebab-case")]
pub enum FolderSyncStrategyDef {
#[default]
All,
#[serde(alias = "only")]
Include(HashSet<String>),
#[serde(alias = "except")]
#[serde(alias = "ignore")]
Exclude(HashSet<String>),
}

View file

@ -1,122 +0,0 @@
use super::DeserializedConfig;
use crate::account;
use anyhow::Result;
use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password, Select};
use once_cell::sync::Lazy;
use std::{env, fs, io, path::PathBuf, process};
#[macro_export]
macro_rules! wizard_warn {
($($arg:tt)*) => {
println!("{}", console::style(format!($($arg)*)).yellow().bold());
};
}
#[macro_export]
macro_rules! wizard_prompt {
($($arg:tt)*) => {
format!("{}", console::style(format!($($arg)*)).italic())
};
}
#[macro_export]
macro_rules! wizard_log {
($($arg:tt)*) => {
println!("");
println!("{}", console::style(format!($($arg)*)).underlined());
println!("");
};
}
pub(crate) static THEME: Lazy<ColorfulTheme> = Lazy::new(ColorfulTheme::default);
pub(crate) fn configure() -> Result<DeserializedConfig> {
wizard_log!("Configuring your first account:");
let mut config = DeserializedConfig::default();
while let Some((name, account_config)) = account::wizard::configure()? {
config.accounts.insert(name, account_config);
if !Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to configure another account?"
))
.default(false)
.interact_opt()?
.unwrap_or_default()
{
break;
}
wizard_log!("Configuring another account:");
}
// If one account is setup, make it the default. If multiple
// accounts are setup, decide which will be the default. If no
// accounts are setup, exit the process.
let default_account = match config.accounts.len() {
0 => process::exit(0),
1 => Some(config.accounts.values_mut().next().unwrap()),
_ => {
let accounts = config.accounts.clone();
let accounts: Vec<&String> = accounts.keys().collect();
println!("{} accounts have been configured.", accounts.len());
Select::with_theme(&*THEME)
.with_prompt(wizard_prompt!(
"Which account would you like to set as your default?"
))
.items(&accounts)
.default(0)
.interact_opt()?
.and_then(|idx| config.accounts.get_mut(accounts[idx]))
}
};
if let Some(account) = default_account {
account.default = Some(true);
} else {
process::exit(0)
}
let path = Input::with_theme(&*THEME)
.with_prompt(wizard_prompt!(
"Where would you like to save your configuration?"
))
.default(
dirs::config_dir()
.map(|p| p.join("himalaya").join("config.toml"))
.unwrap_or_else(|| env::temp_dir().join("himalaya").join("config.toml"))
.to_string_lossy()
.to_string(),
)
.validate_with(|path: &String| shellexpand::full(path).map(|_| ()))
.interact()?;
let path: PathBuf = shellexpand::full(&path).unwrap().to_string().into();
println!("Writing the configuration to {path:?}");
fs::create_dir_all(path.parent().unwrap_or(&path))?;
fs::write(path, toml::to_string(&config)?)?;
Ok(config)
}
pub(crate) fn prompt_passwd(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME)
.with_prompt(prompt)
.with_confirmation(
"Confirm password",
"Passwords do not match, please try again.",
)
.interact()
}
pub(crate) fn prompt_secret(prompt: &str) -> io::Result<String> {
Password::with_theme(&*THEME)
.with_prompt(prompt)
.report(false)
.interact()
}

View file

@ -1,54 +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 serde::Serialize;
use std::fmt;
use crate::ui::table::{Cell, Row, Table};
/// 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 fmt::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())
}
}

View file

@ -1,61 +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 pimalaya_email::backend::BackendConfig;
use serde::Serialize;
use std::{collections::hash_map::Iter, ops::Deref};
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
use super::{Account, DeserializedAccountConfig};
/// 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(())
}
}
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, account)| match &account.backend {
BackendConfig::None => Account::new(name, "none", false),
BackendConfig::Maildir(_) => {
Account::new(name, "maildir", account.default.unwrap_or_default())
}
#[cfg(feature = "imap-backend")]
BackendConfig::Imap(_) => {
Account::new(name, "imap", account.default.unwrap_or_default())
}
#[cfg(feature = "notmuch-backend")]
BackendConfig::Notmuch(_) => {
Account::new(name, "notmuch", account.default.unwrap_or_default())
}
})
.collect();
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
Self(accounts)
}
}

View file

@ -1,144 +0,0 @@
//! This module provides arguments related to the user account config.
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use log::info;
use pimalaya_email::folder::sync::FolderSyncStrategy;
use std::collections::HashSet;
use crate::{folder, ui::table};
const ARG_ACCOUNT: &str = "account";
const ARG_DRY_RUN: &str = "dry-run";
const ARG_RESET: &str = "reset";
const CMD_ACCOUNTS: &str = "accounts";
const CMD_CONFIGURE: &str = "configure";
const CMD_LIST: &str = "list";
const CMD_SYNC: &str = "sync";
type DryRun = bool;
type Reset = bool;
/// Represents the account commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
/// Represents the list accounts command.
List(table::args::MaxTableWidth),
/// Represents the sync account command.
Sync(Option<FolderSyncStrategy>, DryRun),
/// Configure the current selected account.
Configure(Reset),
}
/// Represents the account command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ACCOUNTS) {
if let Some(m) = m.subcommand_matches(CMD_SYNC) {
info!("sync account subcommand matched");
let dry_run = parse_dry_run_arg(m);
let include = folder::args::parse_include_arg(m);
let exclude = folder::args::parse_exclude_arg(m);
let folders_strategy = if let Some(folder) = folder::args::parse_source_arg(m) {
Some(FolderSyncStrategy::Include(HashSet::from_iter([
folder.to_owned()
])))
} else if !include.is_empty() {
Some(FolderSyncStrategy::Include(include.to_owned()))
} else if !exclude.is_empty() {
Some(FolderSyncStrategy::Exclude(exclude))
} else if folder::args::parse_all_arg(m) {
Some(FolderSyncStrategy::All)
} else {
None
};
Some(Cmd::Sync(folders_strategy, dry_run))
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
info!("list accounts subcommand matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(m) = m.subcommand_matches(CMD_CONFIGURE) {
info!("configure account subcommand matched");
let reset = parse_reset_flag(m);
Some(Cmd::Configure(reset))
} else {
info!("no account subcommand matched, falling back to subcommand list");
Some(Cmd::List(None))
}
} else {
None
};
Ok(cmd)
}
/// Represents the account subcommand.
pub fn subcmd() -> Command {
Command::new(CMD_ACCOUNTS)
.about("Manage accounts")
.subcommands([
Command::new(CMD_LIST)
.about("List all accounts from the config file")
.arg(table::args::max_width()),
Command::new(CMD_SYNC)
.about("Synchronize the given account locally")
.arg(folder::args::all_arg("Synchronize all folders"))
.arg(folder::args::include_arg(
"Synchronize only the given folders",
))
.arg(folder::args::exclude_arg(
"Synchronize all folders except the given ones",
))
.arg(dry_run()),
Command::new(CMD_CONFIGURE)
.about("Configure the current selected account")
.aliases(["config", "conf", "cfg"])
.arg(reset_flag()),
])
}
/// Represents the user account name argument. This argument allows
/// the user to select a different account than the default one.
pub fn arg() -> Arg {
Arg::new(ARG_ACCOUNT)
.help("Set the account")
.long("account")
.short('a')
.global(true)
.value_name("STRING")
}
/// Represents the user account name argument parser.
pub fn parse_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_ACCOUNT).map(String::as_str)
}
/// Represents the user account sync dry run flag. This flag allows
/// the user to see the changes of a sync without applying them.
pub fn dry_run() -> Arg {
Arg::new(ARG_DRY_RUN)
.help("Do not apply changes of the synchronization")
.long_help(
"Do not apply changes of the synchronization.
Changes can be visualized with the RUST_LOG=trace environment variable.",
)
.short('d')
.long("dry-run")
.action(ArgAction::SetTrue)
}
/// Represents the user account sync dry run flag parser.
pub fn parse_dry_run_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_DRY_RUN)
}
pub fn reset_flag() -> Arg {
Arg::new(ARG_RESET)
.help("Reset the configuration")
.short('r')
.long("reset")
.action(ArgAction::SetTrue)
}
pub fn parse_reset_flag(m: &ArgMatches) -> bool {
m.get_flag(ARG_RESET)
}

View file

@ -1,263 +0,0 @@
//! Deserialized account config module.
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
#[cfg(feature = "imap-backend")]
use pimalaya_email::backend::ImapAuthConfig;
#[cfg(feature = "smtp-sender")]
use pimalaya_email::sender::SmtpAuthConfig;
use pimalaya_email::{
account::AccountConfig,
backend::BackendConfig,
email::{EmailHooks, EmailTextPlainFormat},
folder::sync::FolderSyncStrategy,
sender::SenderConfig,
};
use pimalaya_process::Cmd;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
use crate::config::{prelude::*, DeserializedConfig};
/// Represents all existing kind of account config.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(tag = "backend", rename_all = "kebab-case")]
pub struct DeserializedAccountConfig {
pub email: String,
pub default: Option<bool>,
pub display_name: Option<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_listing_datetime_fmt: Option<String>,
pub email_listing_datetime_local_tz: Option<bool>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(
default,
with = "EmailTextPlainFormatDef",
skip_serializing_if = "EmailTextPlainFormat::is_default"
)]
pub email_reading_format: EmailTextPlainFormat,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_verify_cmd: Option<Cmd>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_reading_decrypt_cmd: Option<Cmd>,
pub email_writing_headers: Option<Vec<String>>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_writing_sign_cmd: Option<Cmd>,
#[serde(
default,
with = "OptionCmdDef",
skip_serializing_if = "Option::is_none"
)]
pub email_writing_encrypt_cmd: Option<Cmd>,
pub email_sending_save_copy: Option<bool>,
#[serde(
default,
with = "EmailHooksDef",
skip_serializing_if = "EmailHooks::is_empty"
)]
pub email_hooks: EmailHooks,
pub sync: Option<bool>,
pub sync_dir: Option<PathBuf>,
#[serde(
default,
with = "FolderSyncStrategyDef",
skip_serializing_if = "FolderSyncStrategy::is_default"
)]
pub sync_folders_strategy: FolderSyncStrategy,
#[serde(flatten, with = "BackendConfigDef")]
pub backend: BackendConfig,
#[serde(flatten, with = "SenderConfigDef")]
pub sender: SenderConfig,
}
impl DeserializedAccountConfig {
pub fn to_account_config(&self, name: String, config: &DeserializedConfig) -> AccountConfig {
let mut folder_aliases = config
.folder_aliases
.as_ref()
.map(ToOwned::to_owned)
.unwrap_or_default();
folder_aliases.extend(
self.folder_aliases
.as_ref()
.map(ToOwned::to_owned)
.unwrap_or_default(),
);
AccountConfig {
name: name.clone(),
email: self.email.to_owned(),
display_name: self
.display_name
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)),
signature_delim: self
.signature_delim
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)),
signature: self
.signature
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.signature.as_ref().map(ToOwned::to_owned)),
downloads_dir: self
.downloads_dir
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)),
folder_listing_page_size: self
.folder_listing_page_size
.or_else(|| config.folder_listing_page_size),
folder_aliases,
email_listing_page_size: self
.email_listing_page_size
.or_else(|| config.email_listing_page_size),
email_listing_datetime_fmt: self
.email_listing_datetime_fmt
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_listing_datetime_fmt
.as_ref()
.map(ToOwned::to_owned)
}),
email_listing_datetime_local_tz: self
.email_listing_datetime_local_tz
.or_else(|| config.email_listing_datetime_local_tz),
email_reading_headers: self
.email_reading_headers
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)),
email_reading_format: self.email_reading_format.clone(),
email_reading_verify_cmd: self
.email_reading_verify_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_reading_verify_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_reading_decrypt_cmd: self
.email_reading_decrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_reading_decrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_writing_sign_cmd: self
.email_writing_sign_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_writing_sign_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_writing_encrypt_cmd: self
.email_writing_encrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_writing_encrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_writing_headers: self
.email_writing_headers
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.email_writing_headers.as_ref().map(ToOwned::to_owned)),
email_sending_save_copy: self.email_sending_save_copy.unwrap_or(true),
email_hooks: EmailHooks {
pre_send: self.email_hooks.pre_send.clone(),
},
sync: self.sync.unwrap_or_default(),
sync_dir: self.sync_dir.clone(),
sync_folders_strategy: self.sync_folders_strategy.clone(),
backend: {
let mut backend = self.backend.clone();
#[cfg(feature = "imap-backend")]
if let BackendConfig::Imap(config) = &mut backend {
match &mut config.auth {
ImapAuthConfig::Passwd(secret) => {
secret.set_keyring_entry_if_undefined(format!("{name}-imap-passwd"));
}
ImapAuthConfig::OAuth2(config) => {
config.client_secret.set_keyring_entry_if_undefined(format!(
"{name}-imap-oauth2-client-secret"
));
config.access_token.set_keyring_entry_if_undefined(format!(
"{name}-imap-oauth2-access-token"
));
config.refresh_token.set_keyring_entry_if_undefined(format!(
"{name}-imap-oauth2-refresh-token"
));
}
};
}
backend
},
sender: {
let mut sender = self.sender.clone();
#[cfg(feature = "smtp-sender")]
if let SenderConfig::Smtp(config) = &mut sender {
match &mut config.auth {
SmtpAuthConfig::Passwd(secret) => {
secret.set_keyring_entry_if_undefined(format!("{name}-smtp-passwd"));
}
SmtpAuthConfig::OAuth2(config) => {
config.client_secret.set_keyring_entry_if_undefined(format!(
"{name}-smtp-oauth2-client-secret"
));
config.access_token.set_keyring_entry_if_undefined(format!(
"{name}-smtp-oauth2-access-token"
));
config.refresh_token.set_keyring_entry_if_undefined(format!(
"{name}-smtp-oauth2-refresh-token"
));
}
};
}
sender
},
}
}
}

View file

@ -1,387 +0,0 @@
//! Account handlers module.
//!
//! This module gathers all account actions triggered by the CLI.
use anyhow::Result;
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
use log::{info, trace, warn};
use once_cell::sync::Lazy;
#[cfg(feature = "imap-backend")]
use pimalaya_email::backend::ImapAuthConfig;
#[cfg(feature = "smtp-sender")]
use pimalaya_email::sender::SmtpAuthConfig;
use pimalaya_email::{
account::{
sync::{AccountSyncBuilder, AccountSyncProgressEvent},
AccountConfig,
},
backend::BackendConfig,
sender::SenderConfig,
};
use std::{collections::HashMap, sync::Mutex};
use crate::{
config::{
wizard::{prompt_passwd, prompt_secret},
DeserializedConfig,
},
printer::{PrintTableOpts, Printer},
Accounts,
};
const MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
});
const SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
)
.unwrap()
});
const SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
});
/// Configure the current selected account
pub async fn configure(config: &AccountConfig, reset: bool) -> Result<()> {
info!("entering the configure account handler");
if reset {
#[cfg(feature = "imap-backend")]
if let BackendConfig::Imap(imap_config) = &config.backend {
let reset = match &imap_config.auth {
ImapAuthConfig::Passwd(passwd) => passwd.reset(),
ImapAuthConfig::OAuth2(oauth2) => oauth2.reset(),
};
if let Err(err) = reset {
warn!("error while resetting imap secrets, skipping it");
warn!("{err}");
}
}
#[cfg(feature = "smtp-sender")]
if let SenderConfig::Smtp(smtp_config) = &config.sender {
let reset = match &smtp_config.auth {
SmtpAuthConfig::Passwd(passwd) => passwd.reset(),
SmtpAuthConfig::OAuth2(oauth2) => oauth2.reset(),
};
if let Err(err) = reset {
warn!("error while resetting smtp secrets, skipping it");
warn!("{err}");
}
}
}
#[cfg(feature = "imap-backend")]
if let BackendConfig::Imap(imap_config) = &config.backend {
match &imap_config.auth {
ImapAuthConfig::Passwd(passwd) => {
passwd.configure(|| prompt_passwd("IMAP password")).await
}
ImapAuthConfig::OAuth2(oauth2) => {
oauth2
.configure(|| prompt_secret("IMAP OAuth 2.0 client secret"))
.await
}
}?;
}
#[cfg(feature = "smtp-sender")]
if let SenderConfig::Smtp(smtp_config) = &config.sender {
match &smtp_config.auth {
SmtpAuthConfig::Passwd(passwd) => {
passwd.configure(|| prompt_passwd("SMTP password")).await
}
SmtpAuthConfig::OAuth2(oauth2) => {
oauth2
.configure(|| prompt_secret("SMTP OAuth 2.0 client secret"))
.await
}
}?;
}
println!(
"Account successfully {}configured!",
if reset { "re" } else { "" }
);
Ok(())
}
/// Lists all accounts.
pub fn list<'a, P: Printer>(
max_width: Option<usize>,
config: &AccountConfig,
deserialized_config: &DeserializedConfig,
printer: &mut P,
) -> Result<()> {
info!("entering the list accounts handler");
let accounts: Accounts = deserialized_config.accounts.iter().into();
trace!("accounts: {:?}", accounts);
printer.print_table(
Box::new(accounts),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)?;
info!("<< account list handler");
Ok(())
}
/// Synchronizes the account defined using argument `-a|--account`. If
/// no account given, synchronizes the default one.
pub async fn sync<P: Printer>(
printer: &mut P,
sync_builder: AccountSyncBuilder,
dry_run: bool,
) -> Result<()> {
info!("entering the sync accounts handler");
trace!("dry run: {dry_run}");
if dry_run {
let report = sync_builder.sync().await?;
let mut hunks_count = report.folders_patch.len();
if !report.folders_patch.is_empty() {
printer.print_log("Folders patch:")?;
for (hunk, _) in report.folders_patch {
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
if !report.emails_patch.is_empty() {
printer.print_log("Envelopes patch:")?;
for (hunk, _) in report.emails_patch {
hunks_count += 1;
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
printer.print(format!(
"Estimated patch length for account to be synchronized: {hunks_count}",
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.print("Account successfully synchronized!")?;
} else {
let multi = MultiProgress::new();
let sub_progresses = Mutex::new(HashMap::new());
let main_progress = multi.add(
ProgressBar::new(100)
.with_style(MAIN_PROGRESS_STYLE.clone())
.with_message("Synchronizing folders…"),
);
// Force the progress bar to show
main_progress.set_position(0);
let report = sync_builder
.with_on_progress(move |evt| {
use AccountSyncProgressEvent::*;
Ok(match evt {
ApplyFolderPatches(..) => {
main_progress.inc(3);
}
ApplyEnvelopePatches(patches) => {
let mut envelopes_progresses = sub_progresses.lock().unwrap();
let patches_len = patches.values().fold(0, |sum, patch| sum + patch.len());
main_progress.set_length((110 * patches_len / 100) as u64);
main_progress.set_position((5 * patches_len / 100) as u64);
main_progress.set_message("Synchronizing envelopes…");
for (folder, patch) in patches {
let progress = ProgressBar::new(patch.len() as u64)
.with_style(SUB_PROGRESS_STYLE.clone())
.with_prefix(folder.clone())
.with_finish(ProgressFinish::AndClear);
let progress = multi.add(progress);
envelopes_progresses.insert(folder, progress.clone());
}
}
ApplyEnvelopeHunk(hunk) => {
main_progress.inc(1);
let mut progresses = sub_progresses.lock().unwrap();
if let Some(progress) = progresses.get_mut(hunk.folder()) {
progress.inc(1);
if progress.position() == (progress.length().unwrap() - 1) {
progress.set_style(SUB_PROGRESS_DONE_STYLE.clone())
} else {
progress.set_message(format!("{hunk}"));
}
}
}
ApplyEnvelopeCachePatch(_patch) => {
main_progress.set_length(100);
main_progress.set_position(95);
main_progress.set_message("Saving cache database…");
}
ExpungeFolders(folders) => {
let mut progresses = sub_progresses.lock().unwrap();
for progress in progresses.values() {
progress.finish_and_clear()
}
progresses.clear();
main_progress.set_position(100);
main_progress.set_message(format!("Expunging {} folders…", folders.len()));
}
_ => (),
})
})
.sync()
.await?;
let folders_patch_err = report
.folders_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !folders_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the folders patch:")?;
folders_patch_err
.iter()
.try_for_each(|(hunk, err)| printer.print_log(format!(" - {hunk}: {err}")))?;
}
if let Some(err) = report.folders_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the folder cache patch: {err}"
))?;
}
let envelopes_patch_err = report
.emails_patch
.iter()
.filter_map(|(hunk, err)| err.as_ref().map(|err| (hunk, err)))
.collect::<Vec<_>>();
if !envelopes_patch_err.is_empty() {
printer.print_log("")?;
printer.print_log("Errors occurred while applying the envelopes patch:")?;
for (hunk, err) in folders_patch_err {
printer.print_log(format!(" - {hunk}: {err}"))?;
}
}
if let Some(err) = report.emails_cache_patch.1 {
printer.print_log("")?;
printer.print_log(format!(
"Error occurred while applying the envelopes cache patch: {err}"
))?;
}
printer.print("Account successfully synchronized!")?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use pimalaya_email::{account::AccountConfig, backend::ImapConfig};
use std::{collections::HashMap, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::{
account::DeserializedAccountConfig,
printer::{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 Printer 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_log<T: Debug + Print>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(&mut self, _data: T) -> Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
let mut printer = PrinterServiceTest::default();
let config = AccountConfig::default();
let deserialized_config = DeserializedConfig {
accounts: HashMap::from_iter([(
"account-1".into(),
DeserializedAccountConfig {
default: Some(true),
backend: BackendConfig::Imap(ImapConfig::default()),
..DeserializedAccountConfig::default()
},
)]),
..DeserializedConfig::default()
};
assert!(list(None, &config, &deserialized_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,10 +0,0 @@
pub mod account;
pub mod accounts;
pub mod args;
pub mod config;
pub mod handlers;
pub(crate) mod wizard;
pub use account::*;
pub use accounts::*;
pub use config::*;

View file

@ -1,39 +0,0 @@
use anyhow::{anyhow, Result};
use dialoguer::Input;
use email_address::EmailAddress;
use crate::{backend, config::wizard::THEME, sender};
use super::DeserializedAccountConfig;
pub(crate) fn configure() -> Result<Option<(String, DeserializedAccountConfig)>> {
let mut config = DeserializedAccountConfig::default();
let account_name = Input::with_theme(&*THEME)
.with_prompt("Account name")
.default(String::from("Personal"))
.interact()?;
config.email = Input::with_theme(&*THEME)
.with_prompt("Email address")
.validate_with(|email: &String| {
if EmailAddress::is_valid(email) {
Ok(())
} else {
Err(anyhow!("Invalid email address: {email}"))
}
})
.interact()?;
config.display_name = Some(
Input::with_theme(&*THEME)
.with_prompt("Full display name")
.interact()?,
);
config.backend = backend::wizard::configure(&account_name, &config.email)?;
config.sender = sender::wizard::configure(&account_name, &config.email)?;
Ok(Some((account_name, config)))
}

View file

@ -1,62 +0,0 @@
//! Module related to IMAP CLI.
//!
//! This module provides subcommands and a command matcher related to IMAP.
use anyhow::Result;
use clap::{value_parser, Arg, ArgMatches, Command};
use log::debug;
const ARG_KEEPALIVE: &str = "keepalive";
const CMD_NOTIFY: &str = "notify";
const CMD_WATCH: &str = "watch";
type Keepalive = u64;
/// IMAP commands.
pub enum Cmd {
/// 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<Cmd>> {
if let Some(m) = m.subcommand_matches(CMD_NOTIFY) {
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
debug!("keepalive: {}", keepalive);
return Ok(Some(Cmd::Notify(*keepalive)));
}
if let Some(m) = m.subcommand_matches(CMD_WATCH) {
let keepalive = m.get_one::<u64>(ARG_KEEPALIVE).unwrap();
debug!("keepalive: {}", keepalive);
return Ok(Some(Cmd::Watch(*keepalive)));
}
Ok(None)
}
/// IMAP subcommands.
pub fn subcmds<'a>() -> Vec<Command> {
vec![
Command::new(CMD_NOTIFY)
.about("Notifies when new messages arrive in the given folder")
.alias("idle")
.arg(keepalive_arg()),
Command::new(CMD_WATCH)
.about("Watches IMAP server changes")
.arg(keepalive_arg()),
]
}
/// Represents the keepalive argument.
pub fn keepalive_arg() -> Arg {
Arg::new(ARG_KEEPALIVE)
.help("Specifies the keepalive duration.")
.long("keepalive")
.short('k')
.value_name("SECS")
.default_value("500")
.value_parser(value_parser!(u64))
}

View file

@ -1,16 +0,0 @@
//! Module related to IMAP handling.
//!
//! This module gathers all IMAP handlers triggered by the CLI.
use anyhow::Result;
use pimalaya_email::backend::ImapBackend;
pub async fn notify(imap: &mut ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
imap.notify(keepalive, folder).await?;
Ok(())
}
pub async fn watch(imap: &mut ImapBackend, folder: &str, keepalive: u64) -> Result<()> {
imap.watch(keepalive, folder).await?;
Ok(())
}

View file

@ -1,3 +0,0 @@
pub mod args;
pub mod handlers;
pub(crate) mod wizard;

View file

@ -1,221 +0,0 @@
use anyhow::Result;
use dialoguer::{Confirm, Input, Password, Select};
use pimalaya_email::{
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
backend::{BackendConfig, ImapAuthConfig, ImapConfig},
};
use pimalaya_oauth2::{AuthorizationCodeGrant, Client};
use pimalaya_secret::Secret;
use crate::{
config::wizard::{prompt_passwd, THEME},
wizard_log, wizard_prompt,
};
const SSL_TLS: &str = "SSL/TLS";
const STARTTLS: &str = "STARTTLS";
const NONE: &str = "None";
const PROTOCOLS: &[&str] = &[SSL_TLS, STARTTLS, NONE];
const PASSWD: &str = "Password";
const OAUTH2: &str = "OAuth 2.0";
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
const KEYRING: &str = "Ask my password, then save it in my system's global keyring";
const RAW: &str = "Ask my password, then save it in the configuration file (not safe)";
const CMD: &str = "Ask me a shell command that exposes my password";
pub(crate) fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
let mut config = ImapConfig::default();
config.host = Input::with_theme(&*THEME)
.with_prompt("IMAP host")
.default(format!("imap.{}", email.rsplit_once('@').unwrap().1))
.interact()?;
let protocol = Select::with_theme(&*THEME)
.with_prompt("IMAP security protocol")
.items(PROTOCOLS)
.default(0)
.interact_opt()?;
let default_port = match protocol {
Some(idx) if PROTOCOLS[idx] == SSL_TLS => {
config.ssl = Some(true);
993
}
Some(idx) if PROTOCOLS[idx] == STARTTLS => {
config.starttls = Some(true);
143
}
_ => 143,
};
config.port = Input::with_theme(&*THEME)
.with_prompt("IMAP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
.map(|input| input.parse::<u16>().unwrap())?;
config.login = Input::with_theme(&*THEME)
.with_prompt("IMAP login")
.default(email.to_owned())
.interact()?;
let auth = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication mechanism")
.items(AUTH_MECHANISMS)
.default(0)
.interact_opt()?;
config.auth = match auth {
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
let secret = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let config = match secret {
Some(idx) if SECRETS[idx] == KEYRING => {
Secret::new_keyring_entry(format!("{account_name}-imap-passwd"))
.set_keyring_entry_secret(prompt_passwd("IMAP password")?)?;
PasswdConfig::default()
}
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
passwd: Secret::Raw(prompt_passwd("IMAP password")?),
},
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
passwd: Secret::new_cmd(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-imap-passwd"))
.interact()?,
),
},
_ => PasswdConfig::default(),
};
ImapAuthConfig::Passwd(config)
}
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
let mut config = OAuth2Config::default();
let method = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
config.method = match method {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Password::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 client secret")
.interact()?;
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))
.set_keyring_entry_secret(&client_secret)?;
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 authorization URL")
.interact()?;
config.token_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 token URL")
.interact()?;
config.scopes = OAuth2Scopes::Scope(
Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 main scope")
.interact()?,
);
while Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more IMAP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
.unwrap_or_default()
{
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
scopes.push(
Input::with_theme(&*THEME)
.with_prompt("Additional IMAP OAuth 2.0 scope")
.interact()?,
);
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(config.redirect_host.clone())
.with_redirect_port(config.redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(config.redirect_host.clone())
.with_redirect_port(config.redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{}", redirect_url.to_string());
println!("");
let (access_token, refresh_token) =
auth_code_grant.wait_for_redirection(&client, csrf_token)?;
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))
.set_keyring_entry_secret(access_token)?;
if let Some(refresh_token) = &refresh_token {
Secret::new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))
.set_keyring_entry_secret(refresh_token)?;
}
ImapAuthConfig::OAuth2(config)
}
_ => ImapAuthConfig::default(),
};
Ok(BackendConfig::Imap(config))
}

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,23 +0,0 @@
use anyhow::Result;
use dialoguer::Input;
use dirs::home_dir;
use pimalaya_email::backend::{BackendConfig, MaildirConfig};
use crate::config::wizard::THEME;
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = MaildirConfig::default();
let mut input = Input::with_theme(&*THEME);
if let Some(home) = home_dir() {
input.default(home.join("Mail").display().to_string());
};
config.root_dir = input
.with_prompt("Maildir directory")
.interact_text()?
.into();
Ok(BackendConfig::Maildir(config))
}

View file

@ -1,6 +0,0 @@
#[cfg(feature = "imap-backend")]
pub mod imap;
pub mod maildir;
#[cfg(feature = "notmuch-backend")]
pub mod notmuch;
pub(crate) mod wizard;

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,20 +0,0 @@
use anyhow::Result;
use dialoguer::Input;
use pimalaya_email::backend::{BackendConfig, NotmuchBackend, NotmuchConfig};
use crate::config::wizard::THEME;
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = NotmuchConfig::default();
config.db_path = if let Ok(db_path) = NotmuchBackend::get_default_db_path() {
db_path
} else {
let db_path: String = Input::with_theme(&*THEME)
.with_prompt("Notmuch database path")
.interact_text()?;
db_path.into()
};
Ok(BackendConfig::Notmuch(config))
}

View file

@ -1,44 +0,0 @@
use anyhow::Result;
use dialoguer::Select;
use pimalaya_email::backend::BackendConfig;
use crate::config::wizard::THEME;
#[cfg(feature = "imap-backend")]
use super::imap;
use super::maildir;
#[cfg(feature = "notmuch-backend")]
use super::notmuch;
#[cfg(feature = "imap-backend")]
const IMAP: &str = "IMAP";
const MAILDIR: &str = "Maildir";
#[cfg(feature = "notmuch-backend")]
const NOTMUCH: &str = "Notmuch";
const NONE: &str = "None";
const BACKENDS: &[&str] = &[
#[cfg(feature = "imap-backend")]
IMAP,
MAILDIR,
#[cfg(feature = "notmuch-backend")]
NOTMUCH,
NONE,
];
pub(crate) fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
let backend = Select::with_theme(&*THEME)
.with_prompt("Email backend")
.items(BACKENDS)
.default(0)
.interact_opt()?;
match backend {
#[cfg(feature = "imap-backend")]
Some(idx) if BACKENDS[idx] == IMAP => imap::wizard::configure(account_name, email),
Some(idx) if BACKENDS[idx] == MAILDIR => maildir::wizard::configure(),
#[cfg(feature = "notmuch-backend")]
Some(idx) if BACKENDS[idx] == NOTMUCH => notmuch::wizard::configure(),
_ => Ok(BackendConfig::None),
}
}

View file

@ -1,438 +0,0 @@
//! Email CLI module.
//!
//! This module contains the command matcher, the subcommands and the
//! arguments related to the email domain.
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use crate::{flag, folder, tpl, ui::table};
const ARG_CRITERIA: &str = "criterion";
const ARG_HEADERS: &str = "headers";
const ARG_ID: &str = "id";
const ARG_IDS: &str = "ids";
const ARG_MIME_TYPE: &str = "mime-type";
const ARG_PAGE: &str = "page";
const ARG_PAGE_SIZE: &str = "page-size";
const ARG_QUERY: &str = "query";
const ARG_RAW: &str = "raw";
const ARG_REPLY_ALL: &str = "reply-all";
const CMD_ATTACHMENTS: &str = "attachments";
const CMD_COPY: &str = "copy";
const CMD_DELETE: &str = "delete";
const CMD_FORWARD: &str = "forward";
const CMD_LIST: &str = "list";
const CMD_MOVE: &str = "move";
const CMD_READ: &str = "read";
const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save";
const CMD_SEARCH: &str = "search";
const CMD_SEND: &str = "send";
const CMD_SORT: &str = "sort";
const CMD_WRITE: &str = "write";
pub type All = bool;
pub type Criteria = String;
pub type Folder<'a> = &'a str;
pub type Headers<'a> = Vec<&'a str>;
pub type Id<'a> = &'a str;
pub type Ids<'a> = Vec<&'a str>;
pub type Page = usize;
pub type PageSize = usize;
pub type Query = String;
pub type Raw = bool;
pub type RawEmail = String;
pub type TextMime<'a> = &'a str;
/// Represents the email commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Attachments(Ids<'a>),
Copy(Ids<'a>, Folder<'a>),
Delete(Ids<'a>),
Flag(Option<flag::args::Cmd<'a>>),
Forward(Id<'a>, tpl::args::Headers<'a>, tpl::args::Body<'a>),
List(table::args::MaxTableWidth, Option<PageSize>, Page),
Move(Ids<'a>, Folder<'a>),
Read(Ids<'a>, TextMime<'a>, Raw, Headers<'a>),
Reply(Id<'a>, All, tpl::args::Headers<'a>, tpl::args::Body<'a>),
Save(RawEmail),
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
Send(RawEmail),
Sort(
Criteria,
Query,
table::args::MaxTableWidth,
Option<PageSize>,
Page,
),
Tpl(Option<tpl::args::Cmd<'a>>),
Write(tpl::args::Headers<'a>, tpl::args::Body<'a>),
}
/// Email command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
let ids = parse_ids_arg(m);
Cmd::Attachments(ids)
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
let ids = parse_ids_arg(m);
let folder = folder::args::parse_target_arg(m);
Cmd::Copy(ids, folder)
} else if let Some(m) = m.subcommand_matches(CMD_DELETE) {
let ids = parse_ids_arg(m);
Cmd::Delete(ids)
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
Cmd::Flag(flag::args::matches(m)?)
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
let id = parse_id_arg(m);
let headers = tpl::args::parse_headers_arg(m);
let body = tpl::args::parse_body_arg(m);
Cmd::Forward(id, headers, body)
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
Cmd::List(max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
let ids = parse_ids_arg(m);
let folder = folder::args::parse_target_arg(m);
Cmd::Move(ids, folder)
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
let ids = parse_ids_arg(m);
let mime = parse_mime_type_arg(m);
let raw = parse_raw_flag(m);
let headers = parse_headers_arg(m);
Cmd::Read(ids, mime, raw, headers)
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
let id = parse_id_arg(m);
let all = parse_reply_all_flag(m);
let headers = tpl::args::parse_headers_arg(m);
let body = tpl::args::parse_body_arg(m);
Cmd::Reply(id, all, headers, body)
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
let email = parse_raw_arg(m);
Cmd::Save(email)
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
let query = parse_query_arg(m);
Cmd::Search(query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
let criteria = parse_criteria_arg(m);
let query = parse_query_arg(m);
Cmd::Sort(criteria, query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
let email = parse_raw_arg(m);
Cmd::Send(email)
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
Cmd::Tpl(tpl::args::matches(m)?)
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
let headers = tpl::args::parse_headers_arg(m);
let body = tpl::args::parse_body_arg(m);
Cmd::Write(headers, body)
} else {
Cmd::List(None, None, 0)
};
Ok(Some(cmd))
}
/// Represents the email subcommands.
pub fn subcmds() -> Vec<Command> {
vec![
flag::args::subcmds(),
tpl::args::subcmds(),
vec![
Command::new(CMD_ATTACHMENTS)
.about("Downloads all emails attachments")
.arg(ids_arg()),
Command::new(CMD_LIST)
.alias("lst")
.about("List envelopes")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width()),
Command::new(CMD_SEARCH)
.aliases(["query", "q"])
.about("Filter envelopes matching the given query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width())
.arg(query_arg()),
Command::new(CMD_SORT)
.about("Sort envelopes by the given criteria and matching the given query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width())
.arg(criteria_arg())
.arg(query_arg()),
Command::new(CMD_WRITE)
.about("Write a new email")
.aliases(["new", "n"])
.args(tpl::args::args()),
Command::new(CMD_SEND)
.about("Send a raw email")
.arg(raw_arg()),
Command::new(CMD_SAVE)
.about("Save a raw email")
.arg(raw_arg()),
Command::new(CMD_READ)
.about("Read text bodies of emails")
.arg(mime_type_arg())
.arg(raw_flag())
.arg(headers_arg())
.arg(ids_arg()),
Command::new(CMD_REPLY)
.about("Answer to an email")
.arg(reply_all_flag())
.args(tpl::args::args())
.arg(id_arg()),
Command::new(CMD_FORWARD)
.aliases(["fwd", "f"])
.about("Forward an email")
.args(tpl::args::args())
.arg(id_arg()),
Command::new(CMD_COPY)
.alias("cp")
.about("Copy emails to the given folder")
.arg(folder::args::target_arg())
.arg(ids_arg()),
Command::new(CMD_MOVE)
.alias("mv")
.about("Move emails to the given folder")
.arg(folder::args::target_arg())
.arg(ids_arg()),
Command::new(CMD_DELETE)
.aliases(["remove", "rm"])
.about("Delete emails")
.arg(ids_arg()),
],
]
.concat()
}
/// Represents the email id argument.
pub fn id_arg() -> Arg {
Arg::new(ARG_ID)
.help("Specifies the target email")
.value_name("ID")
.required(true)
}
/// Represents the email id argument parser.
pub fn parse_id_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_ID).unwrap()
}
/// Represents the email ids argument.
pub fn ids_arg() -> Arg {
Arg::new(ARG_IDS)
.help("Email ids")
.value_name("IDS")
.num_args(1..)
.required(true)
}
/// Represents the email ids argument parser.
pub fn parse_ids_arg(matches: &ArgMatches) -> Vec<&str> {
matches
.get_many::<String>(ARG_IDS)
.unwrap()
.map(String::as_str)
.collect()
}
/// Represents the email sort criteria argument.
pub fn criteria_arg<'a>() -> Arg {
Arg::new(ARG_CRITERIA)
.help("Email sorting preferences")
.long("criterion")
.short('c')
.value_name("CRITERION:ORDER")
.action(ArgAction::Append)
.value_parser([
"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",
])
}
/// Represents the email sort criteria argument parser.
pub fn parse_criteria_arg(matches: &ArgMatches) -> String {
matches
.get_many::<String>(ARG_CRITERIA)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect::<Vec<_>>()
.join(" ")
}
/// Represents the email reply all argument.
pub fn reply_all_flag() -> Arg {
Arg::new(ARG_REPLY_ALL)
.help("Include all recipients")
.long("all")
.short('A')
.action(ArgAction::SetTrue)
}
/// Represents the email reply all argument parser.
pub fn parse_reply_all_flag(matches: &ArgMatches) -> bool {
matches.get_flag(ARG_REPLY_ALL)
}
/// Represents the page size argument.
fn page_size_arg() -> Arg {
Arg::new(ARG_PAGE_SIZE)
.help("Page size")
.long("page-size")
.short('s')
.value_name("INT")
}
/// Represents the page size argument parser.
fn parse_page_size_arg(matches: &ArgMatches) -> Option<usize> {
matches
.get_one::<String>(ARG_PAGE_SIZE)
.and_then(|s| s.parse().ok())
}
/// Represents the page argument.
fn page_arg() -> Arg {
Arg::new(ARG_PAGE)
.help("Page number")
.short('p')
.long("page")
.value_name("INT")
.default_value("1")
}
/// Represents the page argument parser.
fn parse_page_arg(matches: &ArgMatches) -> usize {
matches
.get_one::<String>(ARG_PAGE)
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default()
}
/// Represents the email headers argument.
pub fn headers_arg() -> Arg {
Arg::new(ARG_HEADERS)
.help("Shows additional headers with the email")
.long("header")
.short('H')
.value_name("STRING")
.action(ArgAction::Append)
}
/// Represents the email headers argument parser.
pub fn parse_headers_arg(m: &ArgMatches) -> Vec<&str> {
m.get_many::<String>(ARG_HEADERS)
.unwrap_or_default()
.map(String::as_str)
.collect::<Vec<_>>()
}
/// Represents the raw flag.
pub fn raw_flag() -> Arg {
Arg::new(ARG_RAW)
.help("Returns raw version of email")
.long("raw")
.short('r')
.action(ArgAction::SetTrue)
}
/// Represents the raw flag parser.
pub fn parse_raw_flag(m: &ArgMatches) -> bool {
m.get_flag(ARG_RAW)
}
/// Represents the email raw argument.
pub fn raw_arg() -> Arg {
Arg::new(ARG_RAW).raw(true)
}
/// Represents the email raw argument parser.
pub fn parse_raw_arg(m: &ArgMatches) -> String {
m.get_one::<String>(ARG_RAW).cloned().unwrap_or_default()
}
/// Represents the email MIME type argument.
pub fn mime_type_arg() -> Arg {
Arg::new(ARG_MIME_TYPE)
.help("MIME type to use")
.short('t')
.long("mime-type")
.value_name("MIME")
.value_parser(["plain", "html"])
.default_value("plain")
}
/// Represents the email MIME type argument parser.
pub fn parse_mime_type_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_MIME_TYPE).unwrap()
}
/// Represents the email query argument.
pub fn query_arg() -> Arg {
Arg::new(ARG_QUERY)
.long_help("The query system depends on the backend, see the wiki for more details")
.value_name("QUERY")
.num_args(1..)
.required(true)
}
/// Represents the email query argument parser.
pub fn parse_query_arg(matches: &ArgMatches) -> String {
matches
.get_many::<String>(ARG_QUERY)
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd.as_str(), 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(" ")
}

View file

@ -1,420 +0,0 @@
use anyhow::{anyhow, Context, Result};
use atty::Stream;
use log::{debug, trace};
use pimalaya_email::{
account::AccountConfig,
backend::Backend,
email::{template::FilterParts, Flag, Flags, Message, MessageBuilder},
sender::Sender,
};
use std::{
fs,
io::{self, BufRead},
};
use url::Url;
use uuid::Uuid;
use crate::{
printer::{PrintTableOpts, Printer},
ui::editor,
Envelopes, IdMapper,
};
pub async fn attachments<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let emails = backend.get_emails(&folder, ids.clone()).await?;
let mut index = 0;
let mut emails_count = 0;
let mut attachments_count = 0;
for email in emails.to_vec() {
let id = ids.get(index).unwrap();
let attachments = email.attachments()?;
index = index + 1;
if attachments.is_empty() {
printer.print_log(format!("No attachment found for email #{}", id))?;
continue;
} else {
emails_count = emails_count + 1;
}
printer.print_log(format!(
"{} attachment(s) found for email #{}…",
attachments.len(),
id
))?;
for attachment in attachments {
let filename = attachment
.filename
.unwrap_or_else(|| Uuid::new_v4().to_string());
let filepath = config.download_fpath(&filename)?;
printer.print_log(format!("Downloading {:?}", filepath))?;
fs::write(&filepath, &attachment.body).context("cannot download attachment")?;
attachments_count = attachments_count + 1;
}
}
match attachments_count {
0 => printer.print("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"),
n => printer.print(format!(
"Downloaded {} attachment(s) from {} email(s)!",
n, emails_count,
)),
}
}
pub async fn copy<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
from_folder: &str,
to_folder: &str,
ids: Vec<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.copy_emails(&from_folder, &to_folder, ids).await?;
printer.print("Email(s) successfully copied!")
}
pub async fn delete<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.delete_emails(&folder, ids).await?;
printer.print("Email(s) successfully deleted!")
}
pub async fn forward<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
folder: &str,
id: &str,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids([id])?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let tpl = backend
.get_emails(&folder, ids)
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_forward_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?;
trace!("initial template: {}", *tpl);
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
Ok(())
}
pub async fn list<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.email_listing_page_size());
debug!("page size: {}", page_size);
let envelopes = Envelopes::from_backend(
config,
id_mapper,
backend.list_envelopes(&folder, page_size, page).await?,
)?;
trace!("envelopes: {:?}", envelopes);
printer.print_table(
Box::new(envelopes),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)
}
/// Parses and edits a message from a [mailto] URL string.
///
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
pub async fn mailto<P: Printer>(
config: &AccountConfig,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
printer: &mut P,
url: &Url,
) -> Result<()> {
let mut builder = MessageBuilder::new().to(url.path());
for (key, val) in url.query_pairs() {
match key.to_lowercase().as_bytes() {
b"cc" => builder = builder.cc(val.to_string()),
b"bcc" => builder = builder.bcc(val.to_string()),
b"subject" => builder = builder.subject(val),
b"body" => builder = builder.text_body(val),
_ => (),
}
}
let tpl = config
.generate_tpl_interpreter()
.show_only_headers(config.email_writing_headers())
.interpret_msg_builder(builder)
.await?;
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await
}
pub async fn move_<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
from_folder: &str,
to_folder: &str,
ids: Vec<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.move_emails(&from_folder, &to_folder, ids).await?;
printer.print("Email(s) successfully moved!")
}
pub async fn read<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
text_mime: &str,
raw: bool,
headers: Vec<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let emails = backend.get_emails(&folder, ids).await?;
let mut glue = "";
let mut bodies = String::default();
for email in emails.to_vec() {
bodies.push_str(glue);
if raw {
// emails do not always have valid utf8, uses "lossy" to
// display what can be displayed
bodies.push_str(&String::from_utf8_lossy(email.raw()?).into_owned());
} else {
let tpl: String = email
.to_read_tpl(&config, |tpl| match text_mime {
"html" => tpl
.hide_all_headers()
.filter_parts(FilterParts::Only("text/html".into())),
_ => tpl.show_additional_headers(&headers),
})
.await?
.into();
bodies.push_str(&tpl);
}
glue = "\n\n";
}
printer.print(bodies)
}
pub async fn reply<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
folder: &str,
id: &str,
all: bool,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids([id])?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let tpl = backend
.get_emails(&folder, ids)
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_reply_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.with_reply_all(all)
.build()
.await?;
trace!("initial template: {}", *tpl);
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
backend
.add_flags(&folder, vec![id], &Flags::from_iter([Flag::Answered]))
.await?;
Ok(())
}
pub async fn save<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
raw_email: String,
) -> Result<()> {
let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json();
let raw_email = if is_tty || is_json {
raw_email.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
let id = backend
.add_email(&folder, raw_email.as_bytes(), &Flags::default())
.await?;
id_mapper.create_alias(id)?;
Ok(())
}
pub async fn search<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.email_listing_page_size());
let envelopes = Envelopes::from_backend(
config,
id_mapper,
backend
.search_envelopes(&folder, &query, "", page_size, page)
.await?,
)?;
let opts = PrintTableOpts {
format: &config.email_reading_format,
max_width,
};
printer.print_table(Box::new(envelopes), opts)
}
pub async fn sort<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
sort: String,
query: String,
max_width: Option<usize>,
page_size: Option<usize>,
page: usize,
) -> Result<()> {
let page_size = page_size.unwrap_or(config.email_listing_page_size());
let envelopes = Envelopes::from_backend(
config,
id_mapper,
backend
.search_envelopes(&folder, &query, &sort, page_size, page)
.await?,
)?;
let opts = PrintTableOpts {
format: &config.email_reading_format,
max_width,
};
printer.print_table(Box::new(envelopes), opts)
}
pub async fn send<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
raw_email: String,
) -> Result<()> {
let folder = config.sent_folder_alias()?;
let is_tty = atty::is(Stream::Stdin);
let is_json = printer.is_json();
let raw_email = if is_tty || is_json {
raw_email.replace("\r", "").replace("\n", "\r\n")
} else {
io::stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\r\n")
};
trace!("raw email: {:?}", raw_email);
sender.send(raw_email.as_bytes()).await?;
if config.email_sending_save_copy {
backend
.add_email(
&folder,
raw_email.as_bytes(),
&Flags::from_iter([Flag::Seen]),
)
.await?;
}
Ok(())
}
pub async fn write<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let tpl = Message::new_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?;
trace!("initial template: {}", *tpl);
editor::edit_tpl_with_editor(config, printer, backend, sender, tpl).await?;
Ok(())
}

View file

@ -1,2 +0,0 @@
pub mod args;
pub mod handlers;

View file

@ -1,66 +0,0 @@
use serde::Serialize;
use crate::{
ui::{Cell, Row, Table},
Flag, Flags,
};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Mailbox {
pub name: Option<String>,
pub addr: String,
}
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelope {
pub id: String,
pub flags: Flags,
pub subject: String,
pub from: Mailbox,
pub date: String,
}
impl Table for Envelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("FROM").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let unseen = !self.flags.contains(&Flag::Seen);
let flags = {
let mut flags = String::new();
flags.push_str(if !unseen { " " } else { "" });
flags.push_str(if self.flags.contains(&Flag::Answered) {
""
} else {
" "
});
flags.push_str(if self.flags.contains(&Flag::Flagged) {
""
} else {
" "
});
flags
};
let subject = &self.subject;
let sender = if let Some(name) = &self.from.name {
name
} else {
&self.from.addr
};
let date = &self.date;
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}

View file

@ -1,129 +0,0 @@
use anyhow::Result;
use pimalaya_email::account::AccountConfig;
use serde::Serialize;
use std::ops;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
Envelope, IdMapper, Mailbox,
};
/// Represents the list of envelopes.
#[derive(Clone, Debug, Default, Serialize)]
pub struct Envelopes(Vec<Envelope>);
impl Envelopes {
pub fn from_backend(
config: &AccountConfig,
id_mapper: &IdMapper,
envelopes: pimalaya_email::email::Envelopes,
) -> Result<Envelopes> {
let envelopes = envelopes
.iter()
.map(|envelope| {
Ok(Envelope {
id: id_mapper.get_or_create_alias(&envelope.id)?,
flags: envelope.flags.clone().into(),
subject: envelope.subject.clone(),
from: Mailbox {
name: envelope.from.name.clone(),
addr: envelope.from.addr.clone(),
},
date: envelope.format_date(config),
})
})
.collect::<Result<Vec<_>>>()?;
Ok(Envelopes(envelopes))
}
}
impl ops::Deref for Envelopes {
type Target = Vec<Envelope>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for Envelopes {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use chrono::DateTime;
use pimalaya_email::account::AccountConfig;
use std::env;
use crate::{Envelopes, IdMapper};
#[test]
fn default_datetime_fmt() {
let config = AccountConfig::default();
let id_mapper = IdMapper::Dummy;
let envelopes =
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
..Default::default()
}]);
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
let expected_date = "2023-06-15 09:42+04:00";
let date = &envelopes.first().unwrap().date;
assert_eq!(date, expected_date);
}
#[test]
fn custom_datetime_fmt() {
let id_mapper = IdMapper::Dummy;
let config = AccountConfig {
email_listing_datetime_fmt: Some("%d/%m/%Y %Hh%M".into()),
..AccountConfig::default()
};
let envelopes =
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
..Default::default()
}]);
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
let expected_date = "15/06/2023 09h42";
let date = &envelopes.first().unwrap().date;
assert_eq!(date, expected_date);
}
#[test]
fn custom_datetime_fmt_with_local_tz() {
env::set_var("TZ", "UTC");
let id_mapper = IdMapper::Dummy;
let config = AccountConfig {
email_listing_datetime_fmt: Some("%d/%m/%Y %Hh%M".into()),
email_listing_datetime_local_tz: Some(true),
..AccountConfig::default()
};
let envelopes =
pimalaya_email::email::Envelopes::from_iter([pimalaya_email::email::Envelope {
date: DateTime::parse_from_rfc3339("2023-06-15T09:42:00+04:00").unwrap(),
..Default::default()
}]);
let envelopes = Envelopes::from_backend(&config, &id_mapper, envelopes).unwrap();
let expected_date = "15/06/2023 05h42";
let date = &envelopes.first().unwrap().date;
assert_eq!(date, expected_date);
}
}

View file

@ -1,5 +0,0 @@
pub mod envelope;
pub mod envelopes;
pub use envelope::*;
pub use envelopes::*;

View file

@ -1,105 +0,0 @@
//! Email flag CLI module.
//!
//! This module contains the command matcher, the subcommands and the
//! arguments related to the email flag domain.
use anyhow::Result;
use clap::{Arg, ArgMatches, Command};
use log::{debug, info};
use pimalaya_email::email::{Flag, Flags};
use crate::email;
const ARG_FLAGS: &str = "flag";
const CMD_ADD: &str = "add";
const CMD_REMOVE: &str = "remove";
const CMD_SET: &str = "set";
pub(crate) const CMD_FLAG: &str = "flags";
/// Represents the flag commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Add(email::args::Ids<'a>, Flags),
Remove(email::args::Ids<'a>, Flags),
Set(email::args::Ids<'a>, Flags),
}
/// Represents the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
debug!("add flags command matched");
let ids = email::args::parse_ids_arg(m);
let flags = parse_flags_arg(m);
Some(Cmd::Add(ids, flags))
} else if let Some(m) = m.subcommand_matches(CMD_REMOVE) {
info!("remove flags command matched");
let ids = email::args::parse_ids_arg(m);
let flags = parse_flags_arg(m);
Some(Cmd::Remove(ids, flags))
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
debug!("set flags command matched");
let ids = email::args::parse_ids_arg(m);
let flags = parse_flags_arg(m);
Some(Cmd::Set(ids, flags))
} else {
None
};
Ok(cmd)
}
/// Represents the flag subcommands.
pub fn subcmds<'a>() -> Vec<Command> {
vec![Command::new(CMD_FLAG)
.about("Handles email flags")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new(CMD_ADD)
.about("Adds flags to an email")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)
.subcommand(
Command::new(CMD_REMOVE)
.aliases(["delete", "del", "d"])
.about("Removes flags from an email")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)
.subcommand(
Command::new(CMD_SET)
.aliases(["change", "c"])
.about("Sets flags of an email")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)]
}
/// Represents the flags argument.
pub fn flags_arg() -> Arg {
Arg::new(ARG_FLAGS)
.value_name("FLAGS")
.help("The flags")
.long_help(
"The list of flags.
It can be one of: seen, answered, flagged, deleted, or draft.
Other flags are considered custom.",
)
.num_args(1..)
.required(true)
.last(true)
}
/// Represents the flags argument parser.
pub fn parse_flags_arg(matches: &ArgMatches) -> Flags {
Flags::from_iter(
matches
.get_many::<String>(ARG_FLAGS)
.unwrap_or_default()
.map(String::as_str)
.map(Flag::from),
)
}

View file

@ -1,26 +0,0 @@
use serde::Serialize;
/// Represents the flag variants.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
pub enum Flag {
Seen,
Answered,
Flagged,
Deleted,
Draft,
Custom(String),
}
impl From<&pimalaya_email::email::Flag> for Flag {
fn from(flag: &pimalaya_email::email::Flag) -> Self {
use pimalaya_email::email::Flag::*;
match flag {
Seen => Flag::Seen,
Answered => Flag::Answered,
Flagged => Flag::Flagged,
Deleted => Flag::Deleted,
Draft => Flag::Draft,
Custom(flag) => Flag::Custom(flag.clone()),
}
}
}

View file

@ -1,21 +0,0 @@
use serde::Serialize;
use std::{collections::HashSet, ops};
use crate::Flag;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize)]
pub struct Flags(pub HashSet<Flag>);
impl ops::Deref for Flags {
type Target = HashSet<Flag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<pimalaya_email::email::Flags> for Flags {
fn from(flags: pimalaya_email::email::Flags) -> Self {
Flags(flags.iter().map(Flag::from).collect())
}
}

View file

@ -1,46 +0,0 @@
use anyhow::Result;
use pimalaya_email::{backend::Backend, email::Flags};
use crate::{printer::Printer, IdMapper};
pub async fn add<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.add_flags(folder, ids, flags).await?;
printer.print("Flag(s) successfully added!")
}
pub async fn set<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.set_flags(folder, ids, flags).await?;
printer.print("Flag(s) successfully set!")
}
pub async fn remove<P: Printer>(
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
ids: Vec<&str>,
flags: &Flags,
) -> Result<()> {
let ids = id_mapper.get_ids(ids)?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
backend.remove_flags(folder, ids, flags).await?;
printer.print("Flag(s) successfully removed!")
}

View file

@ -1,8 +0,0 @@
pub mod args;
pub mod handlers;
pub mod flag;
pub use flag::*;
pub mod flags;
pub use flags::*;

View file

@ -1,237 +0,0 @@
//! Folder CLI module.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to the folder domain.
use std::collections::HashSet;
use anyhow::Result;
use clap::{self, Arg, ArgAction, ArgMatches, Command};
use log::{debug, info};
use crate::ui::table;
const ARG_ALL: &str = "all";
const ARG_EXCLUDE: &str = "exclude";
const ARG_INCLUDE: &str = "include";
const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create";
const CMD_DELETE: &str = "delete";
const CMD_EXPUNGE: &str = "expunge";
const CMD_FOLDERS: &str = "folders";
const CMD_LIST: &str = "list";
/// Represents the folder commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
Create,
List(table::args::MaxTableWidth),
Expunge,
Delete,
}
/// Represents the folder command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
info!("expunge folder subcommand matched");
Some(Cmd::Expunge)
} else if let Some(_) = m.subcommand_matches(CMD_CREATE) {
debug!("create folder command matched");
Some(Cmd::Create)
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
debug!("list folders command matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
debug!("delete folder command matched");
Some(Cmd::Delete)
} else {
info!("no folder subcommand matched, falling back to subcommand list");
Some(Cmd::List(None))
}
} else {
None
};
Ok(cmd)
}
/// Represents the folder subcommand.
pub fn subcmd() -> Command {
Command::new(CMD_FOLDERS)
.about("Manage folders")
.subcommands([
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
Command::new(CMD_CREATE)
.aliases(["add", "new"])
.about("Create a new folder"),
Command::new(CMD_LIST)
.about("List folders")
.arg(table::args::max_width()),
Command::new(CMD_DELETE)
.aliases(["remove", "rm"])
.about("Delete a folder with all its emails"),
])
}
/// Represents the source folder argument.
pub fn source_arg() -> Arg {
Arg::new(ARG_SOURCE)
.help("Set the source folder")
.long("folder")
.short('f')
.global(true)
.value_name("SOURCE")
}
/// Represents the source folder argument parser.
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
}
/// Represents the all folders argument.
pub fn all_arg(help: &'static str) -> Arg {
Arg::new(ARG_ALL)
.help(help)
.long("all-folders")
.alias("all")
.short('A')
.action(ArgAction::SetTrue)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_INCLUDE)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the all folders argument parser.
pub fn parse_all_arg(m: &ArgMatches) -> bool {
m.get_flag(ARG_ALL)
}
/// Represents the folders to include argument.
pub fn include_arg(help: &'static str) -> Arg {
Arg::new(ARG_INCLUDE)
.help(help)
.long("include-folder")
.alias("only")
.short('F')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the folders to include argument parser.
pub fn parse_include_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_INCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the folders to exclude argument.
pub fn exclude_arg(help: &'static str) -> Arg {
Arg::new(ARG_EXCLUDE)
.help(help)
.long("exclude-folder")
.alias("except")
.short('x')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_INCLUDE)
}
/// Represents the folders to exclude argument parser.
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_EXCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the target folder argument.
pub fn target_arg() -> Arg {
Arg::new(ARG_TARGET)
.help("Specifies the target folder")
.value_name("TARGET")
.required(true)
}
/// Represents the target folder argument parser.
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
}
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, Command};
use super::*;
#[test]
fn it_should_match_cmds() {
let arg = Command::new("himalaya")
.subcommand(subcmd())
.get_matches_from(&["himalaya", "folders"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
let arg = Command::new("himalaya")
.subcommand(subcmd())
.get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
}
#[test]
fn it_should_match_source_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
Command::new("himalaya")
.arg(source_arg())
.get_matches_from(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(None, app.get_one::<String>(ARG_SOURCE).map(String::as_str));
let app = get_matches_from!["-f", "SOURCE"];
assert_eq!(
Some("SOURCE"),
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
);
let app = get_matches_from!["--folder", "SOURCE"];
assert_eq!(
Some("SOURCE"),
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
);
}
#[test]
fn it_should_match_target_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
Command::new("himalaya")
.arg(target_arg())
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
let app = get_matches_from!["TARGET"];
assert_eq!(
Some("TARGET"),
app.unwrap()
.get_one::<String>(ARG_TARGET)
.map(String::as_str)
);
}
}

View file

@ -1,32 +0,0 @@
use serde::Serialize;
use crate::ui::{Cell, Row, Table};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
pub name: String,
pub desc: String,
}
impl From<&pimalaya_email::folder::Folder> for Folder {
fn from(folder: &pimalaya_email::folder::Folder) -> Self {
Folder {
name: folder.name.clone(),
desc: folder.desc.clone(),
}
}
}
impl Table for Folder {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").bold().underline().white())
.cell(Cell::new("DESC").bold().underline().white())
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.name).blue())
.cell(Cell::new(&self.desc).green())
}
}

View file

@ -1,35 +0,0 @@
use anyhow::Result;
use serde::Serialize;
use std::ops;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
Folder,
};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folders(Vec<Folder>);
impl ops::Deref for Folders {
type Target = Vec<Folder>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<pimalaya_email::folder::Folders> for Folders {
fn from(folders: pimalaya_email::folder::Folders) -> Self {
Folders(folders.iter().map(Folder::from).collect())
}
}
impl PrintTable for Folders {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}

View file

@ -1,288 +0,0 @@
//! Folder handling module.
//!
//! This module gathers all folder actions triggered by the CLI.
use anyhow::Result;
use dialoguer::Confirm;
use pimalaya_email::{account::AccountConfig, backend::Backend};
use std::process;
use crate::{
printer::{PrintTableOpts, Printer},
Folders,
};
pub async fn expunge<P: Printer>(
printer: &mut P,
backend: &mut dyn Backend,
folder: &str,
) -> Result<()> {
backend.expunge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully expunged!"))
}
pub async fn list<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &mut dyn Backend,
max_width: Option<usize>,
) -> Result<()> {
let folders: Folders = backend.list_folders().await?.into();
printer.print_table(
// TODO: remove Box
Box::new(folders),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)
}
pub async fn create<P: Printer>(
printer: &mut P,
backend: &mut dyn Backend,
folder: &str,
) -> Result<()> {
backend.add_folder(folder).await?;
printer.print("Folder successfully created!")
}
pub async fn delete<P: Printer>(
printer: &mut P,
backend: &mut dyn Backend,
folder: &str,
) -> Result<()> {
if let Some(false) | None = Confirm::new()
.with_prompt(format!("Confirm deletion of folder {folder}?"))
.default(false)
.report(false)
.interact_opt()?
{
process::exit(0);
};
backend.delete_folder(folder).await?;
printer.print("Folder successfully deleted!")
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use pimalaya_email::{
account::AccountConfig,
backend::Backend,
email::{Envelope, Envelopes, Flags, Messages},
folder::{Folder, Folders},
};
use std::{any::Any, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::printer::{Print, PrintTable, WriteColor};
use super::*;
#[tokio::test]
async 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 Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> anyhow::Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> anyhow::Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
#[async_trait]
impl Backend for TestBackend {
fn name(&self) -> String {
unimplemented!();
}
async fn add_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
unimplemented!();
}
async fn list_folders(&mut self) -> pimalaya_email::Result<Folders> {
Ok(Folders::from_iter([
Folder {
name: "INBOX".into(),
desc: "desc".into(),
},
Folder {
name: "Sent".into(),
desc: "desc".into(),
},
]))
}
async fn expunge_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
unimplemented!();
}
async fn purge_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
unimplemented!();
}
async fn delete_folder(&mut self, _: &str) -> pimalaya_email::Result<()> {
unimplemented!();
}
async fn get_envelope(&mut self, _: &str, _: &str) -> pimalaya_email::Result<Envelope> {
unimplemented!();
}
async fn list_envelopes(
&mut self,
_: &str,
_: usize,
_: usize,
) -> pimalaya_email::Result<Envelopes> {
unimplemented!()
}
async fn search_envelopes(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> pimalaya_email::Result<Envelopes> {
unimplemented!()
}
async fn add_email(
&mut self,
_: &str,
_: &[u8],
_: &Flags,
) -> pimalaya_email::Result<String> {
unimplemented!()
}
async fn get_emails(
&mut self,
_: &str,
_: Vec<&str>,
) -> pimalaya_email::Result<Messages> {
unimplemented!()
}
async fn preview_emails(
&mut self,
_: &str,
_: Vec<&str>,
) -> pimalaya_email::Result<Messages> {
unimplemented!()
}
async fn copy_emails(
&mut self,
_: &str,
_: &str,
_: Vec<&str>,
) -> pimalaya_email::Result<()> {
unimplemented!()
}
async fn move_emails(
&mut self,
_: &str,
_: &str,
_: Vec<&str>,
) -> pimalaya_email::Result<()> {
unimplemented!()
}
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> pimalaya_email::Result<()> {
unimplemented!()
}
async fn add_flags(
&mut self,
_: &str,
_: Vec<&str>,
_: &Flags,
) -> pimalaya_email::Result<()> {
unimplemented!()
}
async fn set_flags(
&mut self,
_: &str,
_: Vec<&str>,
_: &Flags,
) -> pimalaya_email::Result<()> {
unimplemented!()
}
async fn remove_flags(
&mut self,
_: &str,
_: Vec<&str>,
_: &Flags,
) -> pimalaya_email::Result<()> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
unimplemented!()
}
}
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
assert!(list(&account_config, &mut printer, &mut backend, None)
.await
.is_ok());
assert_eq!(
concat![
"\n",
"NAME │DESC \n",
"INBOX │desc \n",
"Sent │desc \n",
"\n"
],
printer.writer.content
);
}
}

View file

@ -1,8 +0,0 @@
pub mod folder;
pub use folder::*;
pub mod folders;
pub use folders::*;
pub mod args;
pub mod handlers;

View file

@ -1,16 +0,0 @@
pub mod account;
pub mod backend;
pub mod email;
pub mod envelope;
pub mod flag;
pub mod folder;
pub mod sender;
pub mod tpl;
pub use self::account::{args, handlers, Account, Accounts};
pub use self::backend::*;
pub use self::email::*;
pub use self::envelope::*;
pub use self::flag::*;
pub use self::folder::*;
pub use self::tpl::*;

View file

@ -1,4 +0,0 @@
pub mod sendmail;
#[cfg(feature = "smtp-sender")]
pub mod smtp;
pub(crate) mod wizard;

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,17 +0,0 @@
use anyhow::Result;
use dialoguer::Input;
use pimalaya_email::sender::{SenderConfig, SendmailConfig};
use crate::config::wizard::THEME;
pub(crate) fn configure() -> Result<SenderConfig> {
let mut config = SendmailConfig::default();
config.cmd = Input::with_theme(&*THEME)
.with_prompt("Sendmail-compatible shell command to send emails")
.default(String::from("/usr/bin/msmtp"))
.interact()?
.into();
Ok(SenderConfig::Sendmail(config))
}

View file

@ -1 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,221 +0,0 @@
use anyhow::Result;
use dialoguer::{Confirm, Input, Select};
use pimalaya_email::{
account::{OAuth2Config, OAuth2Method, OAuth2Scopes, PasswdConfig},
sender::{SenderConfig, SmtpAuthConfig, SmtpConfig},
};
use pimalaya_oauth2::{AuthorizationCodeGrant, Client};
use pimalaya_secret::Secret;
use crate::{
config::wizard::{prompt_passwd, THEME},
wizard_log, wizard_prompt,
};
const SSL_TLS: &str = "SSL/TLS";
const STARTTLS: &str = "STARTTLS";
const NONE: &str = "None";
const PROTOCOLS: &[&str] = &[SSL_TLS, STARTTLS, NONE];
const PASSWD: &str = "Password";
const OAUTH2: &str = "OAuth 2.0";
const AUTH_MECHANISMS: &[&str] = &[PASSWD, OAUTH2];
const XOAUTH2: &str = "XOAUTH2";
const OAUTHBEARER: &str = "OAUTHBEARER";
const OAUTH2_MECHANISMS: &[&str] = &[XOAUTH2, OAUTHBEARER];
const SECRETS: &[&str] = &[KEYRING, RAW, CMD];
const KEYRING: &str = "Ask the password, then save it in my system's global keyring";
const RAW: &str = "Ask the password, then save it in the configuration file (not safe)";
const CMD: &str = "Use a shell command that exposes the password";
pub(crate) fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
let mut config = SmtpConfig::default();
config.host = Input::with_theme(&*THEME)
.with_prompt("SMTP host")
.default(format!("smtp.{}", email.rsplit_once('@').unwrap().1))
.interact()?;
let protocol = Select::with_theme(&*THEME)
.with_prompt("SMTP security protocol")
.items(PROTOCOLS)
.default(0)
.interact_opt()?;
let default_port = match protocol {
Some(idx) if PROTOCOLS[idx] == SSL_TLS => {
config.ssl = Some(true);
465
}
Some(idx) if PROTOCOLS[idx] == STARTTLS => {
config.starttls = Some(true);
587
}
_ => 25,
};
config.port = Input::with_theme(&*THEME)
.with_prompt("SMTP port")
.validate_with(|input: &String| input.parse::<u16>().map(|_| ()))
.default(default_port.to_string())
.interact()
.map(|input| input.parse::<u16>().unwrap())?;
config.login = Input::with_theme(&*THEME)
.with_prompt("SMTP login")
.default(email.to_owned())
.interact()?;
let auth = Select::with_theme(&*THEME)
.with_prompt("SMTP authentication mechanism")
.items(AUTH_MECHANISMS)
.default(0)
.interact_opt()?;
config.auth = match auth {
Some(idx) if AUTH_MECHANISMS[idx] == PASSWD => {
let secret = Select::with_theme(&*THEME)
.with_prompt("SMTP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let config = match secret {
Some(idx) if SECRETS[idx] == KEYRING => {
Secret::new_keyring_entry(format!("{account_name}-smtp-passwd"))
.set_keyring_entry_secret(prompt_passwd("SMTP password")?)?;
PasswdConfig::default()
}
Some(idx) if SECRETS[idx] == RAW => PasswdConfig {
passwd: Secret::Raw(prompt_passwd("SMTP password")?),
},
Some(idx) if SECRETS[idx] == CMD => PasswdConfig {
passwd: Secret::new_cmd(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-smtp-passwd"))
.interact()?,
),
},
_ => PasswdConfig::default(),
};
SmtpAuthConfig::Passwd(config)
}
Some(idx) if AUTH_MECHANISMS[idx] == OAUTH2 => {
let mut config = OAuth2Config::default();
let method = Select::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
config.method = match method {
Some(idx) if OAUTH2_MECHANISMS[idx] == XOAUTH2 => OAuth2Method::XOAuth2,
Some(idx) if OAUTH2_MECHANISMS[idx] == OAUTHBEARER => OAuth2Method::OAuthBearer,
_ => OAuth2Method::XOAuth2,
};
config.client_id = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client id")
.interact()?;
let client_secret: String = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 client secret")
.interact()?;
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-client-secret"))
.set_keyring_entry_secret(&client_secret)?;
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 authorization URL")
.interact()?;
config.token_url = Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 token URL")
.interact()?;
config.scopes = OAuth2Scopes::Scope(
Input::with_theme(&*THEME)
.with_prompt("SMTP OAuth 2.0 main scope")
.interact()?,
);
while Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more SMTP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
.unwrap_or_default()
{
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
scopes.push(
Input::with_theme(&*THEME)
.with_prompt("Additional SMTP OAuth 2.0 scope")
.interact()?,
);
config.scopes = OAuth2Scopes::Scopes(scopes);
}
config.pkce = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to enable PKCE verification?"
))
.default(true)
.interact_opt()?
.unwrap_or(true);
wizard_log!("To complete your OAuth 2.0 setup, click on the following link:");
let client = Client::new(
config.client_id.clone(),
client_secret,
config.auth_url.clone(),
config.token_url.clone(),
)?
.with_redirect_host(config.redirect_host.clone())
.with_redirect_port(config.redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(config.redirect_host.clone())
.with_redirect_port(config.redirect_port);
if config.pkce {
auth_code_grant = auth_code_grant.with_pkce();
}
for scope in config.scopes.clone() {
auth_code_grant = auth_code_grant.with_scope(scope);
}
let (redirect_url, csrf_token) = auth_code_grant.get_redirect_url(&client);
println!("{}", redirect_url.to_string());
println!("");
let (access_token, refresh_token) =
auth_code_grant.wait_for_redirection(&client, csrf_token)?;
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-access-token"))
.set_keyring_entry_secret(access_token)?;
if let Some(refresh_token) = &refresh_token {
Secret::new_keyring_entry(format!("{account_name}-smtp-oauth2-refresh-token"))
.set_keyring_entry_secret(refresh_token)?;
}
SmtpAuthConfig::OAuth2(config)
}
_ => SmtpAuthConfig::default(),
};
Ok(SenderConfig::Smtp(config))
}

View file

@ -1,36 +0,0 @@
use anyhow::Result;
use dialoguer::Select;
use pimalaya_email::sender::SenderConfig;
use crate::config::wizard::THEME;
use super::sendmail;
#[cfg(feature = "smtp-sender")]
use super::smtp;
#[cfg(feature = "smtp-sender")]
const SMTP: &str = "SMTP";
const SENDMAIL: &str = "Sendmail";
const NONE: &str = "None";
const SENDERS: &[&str] = &[
#[cfg(feature = "smtp-sender")]
SMTP,
SENDMAIL,
NONE,
];
pub(crate) fn configure(account_name: &str, email: &str) -> Result<SenderConfig> {
let sender = Select::with_theme(&*THEME)
.with_prompt("Email sender")
.items(SENDERS)
.default(0)
.interact_opt()?;
match sender {
#[cfg(feature = "smtp-sender")]
Some(n) if SENDERS[n] == SMTP => smtp::wizard::configure(account_name, email),
Some(n) if SENDERS[n] == SENDMAIL => sendmail::wizard::configure(),
_ => Ok(SenderConfig::None),
}
}

View file

@ -1,148 +0,0 @@
//! Module related to email template CLI.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to email templating.
use anyhow::Result;
use clap::{Arg, ArgAction, ArgMatches, Command};
use log::warn;
use crate::email;
const ARG_BODY: &str = "body";
const ARG_HEADERS: &str = "headers";
const ARG_TPL: &str = "template";
const CMD_FORWARD: &str = "forward";
const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save";
const CMD_SEND: &str = "send";
const CMD_WRITE: &str = "write";
pub const CMD_TPL: &str = "template";
pub type RawTpl = String;
pub type Headers<'a> = Option<Vec<(&'a str, &'a str)>>;
pub type Body<'a> = Option<&'a str>;
/// Represents the template commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Forward(email::args::Id<'a>, Headers<'a>, Body<'a>),
Write(Headers<'a>, Body<'a>),
Reply(email::args::Id<'a>, email::args::All, Headers<'a>, Body<'a>),
Save(RawTpl),
Send(RawTpl),
}
/// Represents the template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
let id = email::args::parse_id_arg(m);
let headers = parse_headers_arg(m);
let body = parse_body_arg(m);
Some(Cmd::Forward(id, headers, body))
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
let id = email::args::parse_id_arg(m);
let all = email::args::parse_reply_all_flag(m);
let headers = parse_headers_arg(m);
let body = parse_body_arg(m);
Some(Cmd::Reply(id, all, headers, body))
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
let raw_tpl = parse_raw_arg(m);
Some(Cmd::Save(raw_tpl))
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
let raw_tpl = parse_raw_arg(m);
Some(Cmd::Send(raw_tpl))
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
let headers = parse_headers_arg(m);
let body = parse_body_arg(m);
Some(Cmd::Write(headers, body))
} else {
None
};
Ok(cmd)
}
/// Represents the template subcommands.
pub fn subcmds<'a>() -> Vec<Command> {
vec![Command::new(CMD_TPL)
.alias("tpl")
.about("Handles email templates")
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new(CMD_FORWARD)
.alias("fwd")
.about("Generates a template for forwarding an email")
.arg(email::args::id_arg())
.args(&args()),
)
.subcommand(
Command::new(CMD_REPLY)
.about("Generates a template for replying to an email")
.arg(email::args::id_arg())
.arg(email::args::reply_all_flag())
.args(&args()),
)
.subcommand(
Command::new(CMD_SAVE)
.about("Compiles the template into a valid email then saves it")
.arg(Arg::new(ARG_TPL).raw(true)),
)
.subcommand(
Command::new(CMD_SEND)
.about("Compiles the template into a valid email then sends it")
.arg(Arg::new(ARG_TPL).raw(true)),
)
.subcommand(
Command::new(CMD_WRITE)
.aliases(["new", "n"])
.about("Generates a template for writing a new email")
.args(&args()),
)]
}
/// Represents the template arguments.
pub fn args() -> Vec<Arg> {
vec![
Arg::new(ARG_HEADERS)
.help("Overrides a specific header")
.short('H')
.long("header")
.value_name("KEY:VAL")
.action(ArgAction::Append),
Arg::new(ARG_BODY)
.help("Overrides the body")
.short('B')
.long("body")
.value_name("STRING"),
]
}
/// Represents the template headers argument parser.
pub fn parse_headers_arg(m: &ArgMatches) -> Headers<'_> {
m.get_many::<String>(ARG_HEADERS).map(|h| {
h.filter_map(|h| match h.split_once(':') {
Some((key, val)) => Some((key, val.trim())),
None => {
warn!("invalid raw header {h:?}, skipping it");
None
}
})
.collect()
})
}
/// Represents the template body argument parser.
pub fn parse_body_arg(matches: &ArgMatches) -> Body<'_> {
matches.get_one::<String>(ARG_BODY).map(String::as_str)
}
/// Represents the raw template argument parser.
pub fn parse_raw_arg(matches: &ArgMatches) -> RawTpl {
matches
.get_one::<String>(ARG_TPL)
.cloned()
.unwrap_or_default()
}

View file

@ -1,149 +0,0 @@
use anyhow::{anyhow, Result};
use atty::Stream;
use pimalaya_email::{
account::AccountConfig,
backend::Backend,
email::{Flags, Message, Tpl},
sender::Sender,
};
use std::io::{stdin, BufRead};
use crate::{printer::Printer, IdMapper};
pub async fn forward<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
id: &str,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids([id])?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let tpl: String = backend
.get_emails(folder, ids)
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_forward_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?
.into();
printer.print(tpl)
}
pub async fn reply<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
id: &str,
all: bool,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let ids = id_mapper.get_ids([id])?;
let ids = ids.iter().map(String::as_str).collect::<Vec<_>>();
let tpl: String = backend
.get_emails(folder, ids)
.await?
.first()
.ok_or_else(|| anyhow!("cannot find email {}", id))?
.to_reply_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.with_reply_all(all)
.build()
.await?
.into();
printer.print(tpl)
}
pub async fn save<P: Printer>(
config: &AccountConfig,
printer: &mut P,
id_mapper: &IdMapper,
backend: &mut dyn Backend,
folder: &str,
tpl: String,
) -> Result<()> {
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
})
.some_pgp_sign_cmd(config.email_writing_sign_cmd.clone())
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.clone())
.compile()
.await?
.write_to_vec()?;
let id = backend.add_email(folder, &email, &Flags::default()).await?;
id_mapper.create_alias(id)?;
printer.print("Template successfully saved!")
}
pub async fn send<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &mut dyn Backend,
sender: &mut dyn Sender,
folder: &str,
tpl: String,
) -> Result<()> {
let email = Tpl::from(if atty::is(Stream::Stdin) || printer.is_json() {
tpl.replace("\r", "")
} else {
stdin()
.lock()
.lines()
.filter_map(Result::ok)
.collect::<Vec<String>>()
.join("\n")
})
.some_pgp_sign_cmd(config.email_writing_sign_cmd.clone())
.some_pgp_encrypt_cmd(config.email_writing_encrypt_cmd.clone())
.compile()
.await?
.write_to_vec()?;
sender.send(&email).await?;
if config.email_sending_save_copy {
backend.add_email(folder, &email, &Flags::default()).await?;
}
printer.print("Template successfully sent!")?;
Ok(())
}
pub async fn write<P: Printer>(
config: &AccountConfig,
printer: &mut P,
headers: Option<Vec<(&str, &str)>>,
body: Option<&str>,
) -> Result<()> {
let tpl: String = Message::new_tpl_builder(config)
.with_some_headers(headers)
.with_some_body(body)
.build()
.await?
.into();
printer.print(tpl)
}

View file

@ -1,2 +0,0 @@
pub mod args;
pub mod handlers;

View file

@ -0,0 +1,17 @@
use clap::Parser;
/// The envelope id argument parser.
#[derive(Debug, Parser)]
pub struct EnvelopeIdArg {
/// The envelope id.
#[arg(value_name = "ID", required = true)]
pub id: usize,
}
/// The envelopes ids arguments parser.
#[derive(Debug, Parser)]
pub struct EnvelopeIdsArgs {
/// The list of envelopes ids.
#[arg(value_name = "ID", required = true)]
pub ids: Vec<usize>,
}

View file

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

View file

@ -0,0 +1,218 @@
use std::{process::exit, sync::Arc};
use ariadne::{Color, Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource, config::Config, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// Search and sort envelopes as a list.
///
/// This command allows you to list envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct EnvelopeListCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
/// The page number.
///
/// The page number starts from 1 (which is the default). Giving a
/// page number to big will result in a out of bound error.
#[arg(long, short, value_name = "NUMBER", default_value = "1")]
pub page: usize,
/// The page size.
///
/// Determine the amount of envelopes a page should contain.
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[command(flatten)]
pub account: AccountNameFlag,
/// 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>,
/// The list envelopes filter and sort query.
///
/// The query can be a filter query, a sort query or both
/// together.
///
/// A filter query is composed of operators and conditions. There
/// is 3 operators and 8 conditions:
///
/// • not <condition> → filter envelopes that do not match the
/// condition
///
/// • <condition> and <condition> → filter envelopes that match
/// both conditions
///
/// • <condition> or <condition> → filter envelopes that match
/// one of the conditions
///
/// ◦ date <yyyy-mm-dd> → filter envelopes that match the given
/// date
///
/// ◦ before <yyyy-mm-dd> → filter envelopes with date strictly
/// before the given one
///
/// ◦ after <yyyy-mm-dd> → filter envelopes with date stricly
/// after the given one
///
/// ◦ from <pattern> → filter envelopes with senders matching the
/// given pattern
///
/// ◦ to <pattern> → filter envelopes with recipients matching
/// the given pattern
///
/// ◦ subject <pattern> → filter envelopes with subject matching
/// the given pattern
///
/// ◦ body <pattern> → filter envelopes with text bodies matching
/// the given pattern
///
/// ◦ flag <flag> → filter envelopes matching the given flag
///
/// A sort query starts by "order by", and is composed of kinds
/// and orders. There is 4 kinds and 2 orders:
///
/// • date [order] → sort envelopes by date
///
/// • from [order] → sort envelopes by sender
///
/// • to [order] → sort envelopes by recipient
///
/// • subject [order] → sort envelopes by subject
///
/// ◦ <kind> asc → sort envelopes by the given kind in ascending
/// order
///
/// ◦ <kind> desc → sort envelopes by the given kind in
/// descending order
///
/// Examples:
///
/// subject foo and body bar → filter envelopes containing "foo"
/// in their subject and "bar" in their text bodies
///
/// order by date desc subject → sort envelopes by descending date
/// (most recent first), then by ascending subject
///
/// subject foo and body bar order by date desc subject →
/// combination of the 2 previous examples
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl Default for EnvelopeListCommand {
fn default() -> Self {
Self {
folder: Default::default(),
page: 1,
page_size: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
impl EnvelopeListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list envelopes command");
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let toml_account_config = Arc::new(toml_account_config);
let folder = &self.folder.name;
let page = 1.max(self.page) - 1;
let page_size = self
.page_size
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
let backend = BackendBuilder::new(
toml_account_config.clone(),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self
.query
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
let query = match query {
None => None,
Some(Ok(query)) => Some(query),
Some(Err(main_err)) => {
let source = "query";
let search_query::error::Error::ParseError(errs, query) = &main_err;
for err in errs {
Report::build(ReportKind::Error, source, err.span().start)
.with_message(main_err.to_string())
.with_label(
Label::new((source, err.span().into_range()))
.with_message(err.reason().to_string())
.with_color(Color::Red),
)
.finish()
.eprint((source, Source::from(&query)))
.unwrap();
}
exit(0)
}
};
let opts = ListEnvelopesOptions {
page,
page_size,
query,
};
let envelopes = backend.list_envelopes(folder, opts).await?;
let table = EnvelopesTable::from(envelopes)
.with_some_width(self.table_max_width)
.with_some_preset(toml_account_config.envelope_list_table_preset())
.with_some_unseen_char(toml_account_config.envelope_list_table_unseen_char())
.with_some_replied_char(toml_account_config.envelope_list_table_replied_char())
.with_some_flagged_char(toml_account_config.envelope_list_table_flagged_char())
.with_some_attachment_char(toml_account_config.envelope_list_table_attachment_char())
.with_some_id_color(toml_account_config.envelope_list_table_id_color())
.with_some_flags_color(toml_account_config.envelope_list_table_flags_color())
.with_some_subject_color(toml_account_config.envelope_list_table_subject_color())
.with_some_sender_color(toml_account_config.envelope_list_table_sender_color())
.with_some_date_color(toml_account_config.envelope_list_table_date_color());
printer.out(table)
}
}

View file

@ -0,0 +1,35 @@
pub mod list;
pub mod thread;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
/// List, search and sort your envelopes.
///
/// An envelope is a small representation of a message. It contains an
/// identifier (given by the backend), some flags as well as few
/// headers from the message itself. This subcommand allows you to
/// manage them.
#[derive(Debug, Subcommand)]
pub enum EnvelopeSubcommand {
#[command(alias = "lst")]
List(EnvelopeListCommand),
#[command()]
Thread(EnvelopeThreadCommand),
}
impl EnvelopeSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

@ -0,0 +1,201 @@
use ariadne::{Label, Report, ReportKind, Source};
use clap::Parser;
use color_eyre::Result;
use email::{
backend::feature::BackendFeatureSource, config::Config, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTree},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{process::exit, sync::Arc};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
};
/// Search and sort envelopes as a thread.
///
/// This command allows you to thread envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct EnvelopeThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub account: AccountNameFlag,
/// Show only threads that contain the given envelope identifier.
#[arg(long, short)]
pub id: Option<usize>,
/// The list envelopes filter and sort query.
///
/// See `envelope list --help` for more information.
#[arg(allow_hyphen_values = true, trailing_var_arg = true)]
pub query: Option<Vec<String>>,
}
impl EnvelopeThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread envelopes command");
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);
let folder = &self.folder.name;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self
.query
.map(|query| query.join(" ").parse::<SearchEmailsQuery>());
let query = match query {
None => None,
Some(Ok(query)) => Some(query),
Some(Err(main_err)) => {
let source = "query";
let search_query::error::Error::ParseError(errs, query) = &main_err;
for err in errs {
Report::build(ReportKind::Error, source, err.span().start)
.with_message(main_err.to_string())
.with_label(
Label::new((source, err.span().into_range()))
.with_message(err.reason().to_string())
.with_color(ariadne::Color::Red),
)
.finish()
.eprint((source, Source::from(&query)))
.unwrap();
}
exit(0)
}
};
let opts = ListEnvelopesOptions {
page: 0,
page_size: 0,
query,
};
let envelopes = match self.id {
Some(id) => backend.thread_envelope(folder, id, opts).await,
None => backend.thread_envelopes(folder, opts).await,
}?;
let tree = EnvelopesTree::new(account_config, envelopes);
printer.out(tree)
}
}
// #[cfg(test)]
// mod test {
// use email::{account::config::AccountConfig, envelope::ThreadedEnvelope};
// use petgraph::graphmap::DiGraphMap;
// use super::write_tree;
// macro_rules! e {
// ($id:literal) => {
// ThreadedEnvelope {
// id: $id,
// message_id: $id,
// from: "",
// subject: "",
// date: Default::default(),
// }
// };
// }
// #[test]
// fn tree_1() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("0"), e!("2"), 0);
// graph.add_edge(e!("0"), e!("3"), 0);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_2() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("1"), e!("3"), 1);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// └─ 1
// ├─ 2
// └─ 3
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// #[test]
// fn tree_3() {
// let config = AccountConfig::default();
// let mut buf = Vec::new();
// let mut graph = DiGraphMap::new();
// graph.add_edge(e!("0"), e!("1"), 0);
// graph.add_edge(e!("1"), e!("2"), 1);
// graph.add_edge(e!("2"), e!("22"), 2);
// graph.add_edge(e!("1"), e!("3"), 1);
// graph.add_edge(e!("0"), e!("4"), 0);
// graph.add_edge(e!("4"), e!("5"), 1);
// graph.add_edge(e!("5"), e!("6"), 2);
// write_tree(&config, &mut buf, &graph, e!("0"), String::new(), 0).unwrap();
// let buf = String::from_utf8_lossy(&buf);
// let expected = "
// 0
// ├─ 1
// │ ├─ 2
// │ │ └─ 22
// │ └─ 3
// └─ 4
// └─ 5
// └─ 6
// ";
// assert_eq!(expected.trim_start(), buf)
// }
// }

View file

@ -0,0 +1,48 @@
use clap::Parser;
use email::flag::{Flag, Flags};
use tracing::debug;
/// The ids and/or flags arguments parser.
#[derive(Debug, Parser)]
pub struct IdsAndFlagsArgs {
/// The list of ids and/or flags.
///
/// Every argument that can be parsed as an integer is considered
/// an id, otherwise it is considered as a flag.
#[arg(value_name = "ID-OR-FLAG", required = true)]
pub ids_and_flags: Vec<IdOrFlag>,
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum IdOrFlag {
Id(usize),
Flag(Flag),
}
impl From<&str> for IdOrFlag {
fn from(value: &str) -> Self {
value.parse::<usize>().map(Self::Id).unwrap_or_else(|err| {
let flag = Flag::from(value);
debug!("cannot parse {value} as usize, parsing it as flag {flag}");
debug!("{err:?}");
Self::Flag(flag)
})
}
}
pub fn into_tuple(ids_and_flags: &[IdOrFlag]) -> (Vec<usize>, Flags) {
ids_and_flags.iter().fold(
(Vec::default(), Flags::default()),
|(mut ids, mut flags), arg| {
match arg {
IdOrFlag::Id(id) => {
ids.push(*id);
}
IdOrFlag::Flag(flag) => {
flags.insert(flag.to_owned());
}
};
(ids, flags)
},
)
}

View file

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

View file

@ -0,0 +1,64 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
};
/// Add flag(s) to the given envelope.
///
/// This command allows you to attach the given flag(s) to the given
/// envelope(s).
#[derive(Debug, Parser)]
pub struct FlagAddCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl FlagAddCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing add flag(s) command");
let folder = &self.folder.name;
let (ids, flags) = into_tuple(&self.args.ids_and_flags);
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.add_flags(folder, &ids, &flags).await?;
printer.out(format!("Flag(s) {flags} successfully added!\n"))
}
}

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