Compare commits

..

294 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
118 changed files with 5189 additions and 9179 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

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,74 +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-releases:
runs-on: ${{ matrix.os }}
needs: create-release
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-linux
os: ubuntu-latest
- target: arm64-linux
os: ubuntu-latest
- target: x86_64-windows
os: ubuntu-latest
- target: x86_64-macos
os: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install Nix
uses: cachix/install-nix-action@v24
with:
nix_path: nixpkgs=channel:nixos-23.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 archive
run: |
nix build -L .#${{ matrix.target }}
cp result/bin/himalaya* .
- name: Upload tgz release archive
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 archive
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

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

@ -7,6 +7,148 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.1.0] - 2025-01-11
### Added
- Added `-y|--yes` flag for `folder purge` and `folder delete` commands. [#469]
### Changed
- Put back `warn` the default log level. [#522]
Since logs are sent to `stderr`, warnings can be easily discarded by prepending commands with `RUST_LOG=off` or by appending commands with `2>/dev/null`.
- Changed `message.send.save-copy` default to `true`. [#536]
- Changed default downloads directory. [core#1]
### Fixed
- Fixed permissions issues when using `install.sh`. [#515]
- Fixed de/serialization issues of backends' `none` variant. [#523]
- Fixed list envelopes out of bound error when empty result. [#535]
- Fixed macOS x86_64 builds. [#538]
## [1.0.0] - 2024-12-09
The Himalaya CLI scope has changed. It does not include anymore the synchronization, nor the envelope watching. These scopes have moved to dedicated projects:
- [Neverest CLI](https://github.com/pimalaya/neverest), CLI to synchronize, backup and restore emails
- [Mirador CLI](https://github.com/pimalaya/mirador), CLI to watch mailbox changes
Due to the long time difference with the previous `v1.0.0-beta.4` release, this changelog may be incomplete. The simplest way to upgrade is to reconfigure Himalaya CLI from scratch, using the wizard or the [`config.sample.toml`](./config.sample.toml).
Himalaya CLI will now try to adopt the [conventional commits specification](https://github.com/conventional-commits/conventionalcommits.org). Tools like [`git-cliff`](https://git-cliff.org/) may help us generating more accurate changelogs in the future.
### Added
- Added `message edit` command to edit a message. To edit on place (replace a message), use `--on-place`.
- Added `account.list.table.preset` global config option, `accounts.<name>.folder.list.table.preset` and `accounts.<name>.envelope.list.table.preset` account config options.
These options customize the shape of tables, see examples at [`comfy_table::presets`](https://docs.rs/comfy-table/latest/comfy_table/presets/index.html). Defaults to `"|| |-||| "`, which corresponds to [`comfy_table::presets::ASCII_MARKDOWN`](https://docs.rs/comfy-table/latest/comfy_table/presets/constant.ASCII_MARKDOWN.html).
- Added `account.list.table.name-color` config option to customize the color used for the accounts' `NAME` column (defaults to `green`).
- Added `account.list.table.backends-color` config option to customize the color used for the folders' `BACKENDS` column (defaults to `blue`).
- Added `account.list.table.default-color` config option to customize the color used for the folders' `DEFAULT` column (defaults to `reset`).
- Added `accounts.<name>.folder.list.table.name-color` account config option to customize the color used for the folders' `NAME` column (defaults to `blue`).
- Added `accounts.<name>.folder.list.table.desc-color` account config option to customize the color used for the folders' `DESC` column (defaults to `green`).
- Added `accounts.<name>.envelope.list.table.id-color` account config option to customize the color used for the envelopes' `ID` column (defaults to `red`).
- Added `accounts.<name>.envelope.list.table.flags-color` account config option to customize the color used for the envelopes' `FLAGS` column (defaults to `reset`).
- Added `accounts.<name>.envelope.list.table.subject-color` account config option to customize the color used for the envelopes' `SUBJECT` column (defaults to `green`).
- Added `accounts.<name>.envelope.list.table.sender-color` account config option to customize the color used for the envelopes' `FROM` column (defaults to `blue`).
- Added `accounts.<name>.envelope.list.table.date-color` account config option to customize the color used for the envelopes' `DATE` column (defaults to `dark_yellow`).
- Added `accounts.<name>.envelope.list.table.unseen-char` account config option to customize the char used for unseen envelopes (defaults to `*`).
- Added `accounts.<name>.envelope.list.table.replied-char` account config option to customize the char used for replied envelopes (defaults to `R`).
- Added `accounts.<name>.envelope.list.table.flagged-char` account config option to customize the char used for flagged envelopes (defaults to `!`).
- Added `accounts.<name>.envelope.list.table.attachment-char` account config option to customize the char used for envelopes with at least one attachment (defaults to `@`).
### Changed
- Refactored the `account configure` command: this command stands now for creating or editing account configurations from the wizard. The command requires the `wizard` cargo feature.
- Improved the `account doctor` command: it now checks the state of the config, and the new `--fix` argument allows you to configure keyring, OAuth 2.0 etc.
- Improved long version `--version`. [#496]
- Improved error messages when missing cargo features. For example, if a TOML configuration uses the IMAP backend without the `imap` cargo features, the error `missing "imap" feature` is displayed. [#20](https://github.com/pimalaya/core/issues/20)
- Normalized enum-based configurations, using the [internally tagged representation](https://serde.rs/enum-representations.html#internally-tagged) `type =`. It should reduce issues due to misconfiguration, and improve othe error messages. Yet it is not perfect, see [#802](https://github.com/toml-rs/toml/issues/802):
- `imap.*`, `maildir.*` and `notmuch.*` moved to `backend.*`:
```toml
# before
imap.host = "localhost"
imap.port = 143
# after
backend.type = "imap"
backend.host = "localhost"
backend.port = 143
```
- `smtp.*` and `sendmail.*` moved to `message.send.backend.*`:
```toml
# before
smtp.host = "localhost"
smtp.port = 25
# after
message.send.backend.type = "smtp"
message.send.backend.host = "localhost"
message.send.backend.port = 25
```
- `pgp.backend` renamed `pgp.type`:
```toml
# before
pgp.backend = "commands"
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
# after
pgp.type = "commands"
pgp.encrypt-cmd = "gpg --encrypt --quiet --armor <recipients>"
```
- `{imap,smtp}.auth` moved as well:
```toml
# before
imap.password.cmd = "pass show example"
smtp.oauth2.method = "xoauth2"
# after
backend.auth.type = "password"
backend.auth.cmd = "pass show example"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.method = "xoauth2"
```
- Moved IMAP and SMTP `encryption` to `encryption.type`.
This change prepares the config to accept different TLS providers with their options. The `true` and `false` variant have been removed as well:
```toml
# before
backend.encryption = "none" # or false
backend.encryption = "start-tls"
message.send.backend.encryption = "tls" # or true
# after
backend.encryption.type = "none"
backend.encryption.type = "start-tls"
message.send.backend.encryption.type = "tls"
```
### Fixed
- Fixed pre-release archives issue. [#492]
- Fixed mailto parsing issue. [core#10]
- Fixed `Answered` flag not set when replying to a message. [#508]
### Removed
- Removed systemd service from `assets/` folder, as Himalaya CLI scope does not include synchronization nor watching anymore.
## [1.0.0-beta.4] - 2024-04-16
### Added
@ -771,7 +913,10 @@ Few major concepts changed:
- Password from command
- Set up README
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...HEAD
[Unreleased]: https://github.com/soywod/himalaya/compare/v1.0.0...HEAD
[1.0.0]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.4...v1.0.0
[1.0.0-beta.4]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.3...v1.0.0-beta.4
[1.0.0-beta.3]: https://github.com/soywod/himalaya/compare/v1.0.0-beta.2...v1.0.0-beta.3
[1.0.0-beta.2]: https://github.com/soywod/himalaya/compare/v1.0.0-beta...v1.0.0-beta.2
[1.0.0-beta]: https://github.com/soywod/himalaya/compare/v0.9.0...v1.0.0-beta
[0.9.0]: https://github.com/soywod/himalaya/compare/v0.8.4...v0.9.0
@ -811,17 +956,17 @@ Few major concepts changed:
[0.2.0]: https://github.com/soywod/himalaya/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/soywod/himalaya/releases/tag/v0.1.0
[#39]: https://todo.sr.ht/~soywod/pimalaya/39
[#41]: https://todo.sr.ht/~soywod/pimalaya/41
[#43]: https://todo.sr.ht/~soywod/pimalaya/43
[#54]: https://todo.sr.ht/~soywod/pimalaya/54
[#58]: https://todo.sr.ht/~soywod/pimalaya/58
[#59]: https://todo.sr.ht/~soywod/pimalaya/59
[#60]: https://todo.sr.ht/~soywod/pimalaya/60
[#95]: https://todo.sr.ht/~soywod/pimalaya/95
[#172]: https://todo.sr.ht/~soywod/pimalaya/172
[#173]: https://todo.sr.ht/~soywod/pimalaya/173
[#184]: https://todo.sr.ht/~soywod/pimalaya/184
[#188]: https://todo.sr.ht/~soywod/pimalaya/188
[#194]: https://todo.sr.ht/~soywod/pimalaya/194
[#195]: https://todo.sr.ht/~soywod/pimalaya/195
[#469]: https://github.com/pimalaya/himalaya/issues/469
[#492]: https://github.com/pimalaya/himalaya/issues/492
[#496]: https://github.com/pimalaya/himalaya/issues/496
[#508]: https://github.com/pimalaya/himalaya/issues/508
[#515]: https://github.com/pimalaya/himalaya/issues/515
[#518]: https://github.com/pimalaya/himalaya/issues/518
[#522]: https://github.com/pimalaya/himalaya/issues/522
[#523]: https://github.com/pimalaya/himalaya/issues/523
[#535]: https://github.com/pimalaya/himalaya/issues/535
[#536]: https://github.com/pimalaya/himalaya/issues/536
[#538]: https://github.com/pimalaya/himalaya/issues/538
[core#1]: https://github.com/pimalaya/core/issues/1
[core#10]: https://github.com/pimalaya/core/issues/10

View file

@ -4,28 +4,66 @@ 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 lib: `cargo`, `cargo-watch`, `rust-bin`, `rust-analyzer`, `notmuch`
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
# Start a Nix shell
$ nix-shell
If you do not want to use Nix, you can either use [rustup](https://rust-lang.github.io/rustup/index.html):
# then build the CLI
$ cargo build
# run the CLI
$ cargo run --feature pgp-gpg -- envelope list
```text
rustup update
```
## Contributing
or install manually the following dependencies:
Himalaya CLI supports open-source, hence the choice of using [SourceHut](https://sourcehut.org/) for managing the project. The only reason why the source code is hosted on GitHub is to build releases for all major platforms (using GitHub Actions). Don't worry, contributing on SourceHut is not a big deal: you just need to send emails! You don't need to create any account. Here a small comparison guide with GitHub:
- [cargo](https://doc.rust-lang.org/cargo/) (`v1.82`)
- [rustc](https://doc.rust-lang.org/stable/rustc/platform-support.html) (`v1.82`)
The equivalent of **GitHub Discussions** are:
## Build
- The [Matrix](https://matrix.org/) chat room [#pimalaya.himalaya](https://matrix.to/#/#pimalaya.himalaya:matrix.org)
- The SourceHut mailing list. You can consult existing messages [here](https://lists.sr.ht/~soywod/pimalaya). You can "open a new discussion" by sending an email at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). You can also [subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht) and [unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht) to the mailing list, so you can receive a copy of all discussions.
```text
cargo build
```
The equivalent of **GitHub Issues** is the SourceHut bug tracker. You can consult existing bugs [here](https://todo.sr.ht/~soywod/pimalaya), and you can "open a new issue" by sending an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.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`.
The equivalent of **GitHub Pull requests** is the SourceHut mailing list. You can "open a new pull request" by sending an email containing a git patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly.
Finally, you can build a release with `--release`:
```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).

3227
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,84 +1,70 @@
[package]
name = "himalaya"
description = "CLI to manage emails"
version = "1.0.0-beta.4"
version = "1.1.0"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021"
license = "MIT"
categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "smtp", "sync"]
categories = ["command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "maildir", "smtp"]
homepage = "https://pimalaya.org/"
documentation = "https://pimalaya.org/himalaya/cli/latest/"
repository = "https://github.com/soywod/himalaya/"
documentation = "https://github.com/pimalaya/himalaya/"
repository = "https://github.com/pimalaya/himalaya/"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--document-private-items"]
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
default = [
"imap",
"maildir",
# "notmuch",
"smtp",
"sendmail",
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"]
"account-discovery",
"account-sync",
[build-dependencies]
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
# "pgp-commands",
# "pgp-gpg",
# "pgp-native",
]
imap = ["email-lib/imap"]
maildir = ["email-lib/maildir"]
notmuch = ["email-lib/notmuch"]
smtp = ["email-lib/smtp"]
sendmail = ["email-lib/sendmail"]
account-discovery = ["email-lib/account-discovery"]
account-sync = ["email-lib/account-sync", "maildir"]
pgp = []
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pgp"]
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pgp"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pgp"]
[dev-dependencies]
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
[dependencies]
ariadne = "0.2"
async-trait = "0.1"
clap = { version = "4.4", features = ["derive", "wrap_help"] }
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6.3"
console = "0.15.2"
dialoguer = "0.10.2"
dirs = "4"
email-lib = { version = "=0.24.1", default-features = false, features = ["derive"] }
email_address = "0.2.4"
erased-serde = "0.3"
indicatif = "0.17"
mail-builder = "0.3"
md5 = "0.7"
mml-lib = { version = "=1.0.12", default-features = false, features = ["derive"] }
oauth-lib = "=0.1.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"
process-lib = { version = "=0.4.2", features = ["derive"] }
secret-lib = { version = "=0.4.4", features = ["derive"] }
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-toml-merge = "0.3"
serde_json = "1"
shellexpand-utils = "=0.2.1"
sled = "=0.34.7"
termcolor = "1"
terminal_size = "0.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
toml_edit = "0.22"
tracing = "0.1.40"
tracing-error = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-width = "0.1"
tracing = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
[patch.crates-io]
imap-codec.git = "https://github.com/duesee/imap-codec"
email-lib.git = "https://github.com/pimalaya/core"
imap-client.git = "https://github.com/pimalaya/imap-client"
keyring-lib.git = "https://github.com/pimalaya/core"
mml-lib.git = "https://github.com/pimalaya/core"
oauth-lib.git = "https://github.com/pimalaya/core"
pgp-lib.git = "https://github.com/pimalaya/core"
pimalaya-tui.git = "https://github.com/pimalaya/tui"
process-lib.git = "https://github.com/pimalaya/core"
secret-lib.git = "https://github.com/pimalaya/core"

714
README.md
View file

@ -1,98 +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>
Welcome to [**Himalaya CLI**](https://pimalaya.org/himalaya/cli/latest/), the Command-Line Interface to manage emails based on [email-lib](https://crates.io/crates/email-lib).
```
himalaya envelope list --account posteo --folder Archives.FOSS --page 2
```
![screenshot](https://github.com/soywod/himalaya/assets/10437171/8a62cf1d-920e-4110-9849-170db6dc51ce)
*Disclaimer: the project is under active development, do not use in production before the final `v1.0.0`.*
![screenshot](./screenshot.jpeg)
## Features
- [Folder (aka mailbox) management](https://pimalaya.org/himalaya/cli/latest/usage/advanced/folder/)
- Envelope [listing](https://pimalaya.org/himalaya/cli/latest/usage/basic/envelope/list.html), [filtering and sorting](https://pimalaya.org/himalaya/cli/latest/usage/advanced/envelope/list.html)
- [Message composition](https://pimalaya.org/himalaya/cli/latest/usage/basic/message/send.html) based on `$EDITOR`
- Message manipulation ([copy](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/copy.html)/[move](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/move.html)/[delete](https://pimalaya.org/himalaya/cli/latest/usage/advanced/message/delete.html))
- [Multi-accounting](https://pimalaya.org/himalaya/cli/latest/configuration/)
- [Account synchronization](https://pimalaya.org/himalaya/cli/latest/usage/basic/account/sync.html) for offline usage
- Support multiple backends: [IMAP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/imap.html), [Maildir](https://pimalaya.org/himalaya/cli/latest/usage/advanced/maildir.html), [Notmuch](https://pimalaya.org/himalaya/cli/latest/usage/advanced/notmuch.html), [SMTP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/smtp.html), [Sendmail](https://pimalaya.org/himalaya/cli/latest/usage/advanced/sendmail.html).
- [PGP](https://pimalaya.org/himalaya/cli/latest/usage/advanced/pgp/) end-to-end encryption
- Generate [man pages](https://pimalaya.org/himalaya/cli/latest/usage/advanced/man.html)
- Generate [completion scripts](https://pimalaya.org/himalaya/cli/latest/usage/advanced/completion.html) for various shells
- [JSON output](https://pimalaya.org/himalaya/cli/latest/usage/advanced/#-o--output)
- …and more! [Get started now](https://pimalaya.org/himalaya/cli/latest/quickstart.html)
- 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)
*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.
# Fedora/CentOS
$ dnf copr enable atim/himalaya
$ dnf install himalaya
```
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.
*See the [documentation](https://pimalaya.org/himalaya/cli/latest/installation.html) for other installation methods.*
*Such binaries are built with the default cargo features. If you want to enable or disable a feature, please use another installation method.*
</details>
</td>
</tr>
</table>
<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/latest/configuration/).*
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>`.
*Please read the [contributing guide](https://github.com/soywod/himalaya/blob/master/CONTRIBUTING.md) for more detailed information.*
You can also manually edit your own configuration, from scratch:
A **bug tracker** is available on [SourceHut](https://todo.sr.ht/~soywod/pimalaya). <sup>[[send an email](mailto:~soywod/pimalaya@todo.sr.ht)]</sup>
- 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
A **mailing list** is available on [SourceHut](https://lists.sr.ht/~soywod/pimalaya). <sup>[[send an email](mailto:~soywod/pimalaya@lists.sr.ht)] [[subscribe](mailto:~soywod/pimalaya+subscribe@lists.sr.ht)] [[unsubscribe](mailto:~soywod/pimalaya+unsubscribe@lists.sr.ht)]</sup>
<details>
<summary>Proton Mail (Bridge)</summary>
If you want to **report a bug**, please send an email at [~soywod/pimalaya@todo.sr.ht](mailto:~soywod/pimalaya@todo.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 **propose a feature** or **fix a bug**, please send a patch at [~soywod/pimalaya@lists.sr.ht](mailto:~soywod/pimalaya@lists.sr.ht). The simplest way to send a patch is to use [git send-email](https://git-scm.com/docs/git-send-email), follow [this guide](https://git-send-email.io/) to configure git properly.
- 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 just 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). You can also use the mailing list.
```toml
[accounts.proton]
email = "example@proton.me"
backend.type = "imap"
backend.host = "127.0.0.1"
backend.port = 1143
backend.encryption.type = "none"
backend.login = "example@proton.me"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "127.0.0.1"
message.send.backend.port = 1025
message.send.backend.encryption.type = "none"
message.send.backend.login = "example@proton.me"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show proton"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "proton-example"
```
Running `himalaya configure -a proton` will ask for your IMAP password, just paste the one generated previously.
</details>
<details>
<summary>Gmail</summary>
Google passwords cannot be used directly. There is two ways to authenticate yourself:
### Using [App Passwords](https://support.google.com/mail/answer/185833)
This option is the simplest and the fastest. First, be sure that:
- IMAP is enabled
- Two-step authentication is enabled
- Less secure app access is enabled
First create a [dedicated password](https://myaccount.google.com/apppasswords) for Himalaya.
```toml
[accounts.gmail]
email = "example@gmail.com"
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/Sent Mail"
folder.aliases.drafts = "[Gmail]/Drafts"
folder.aliases.trash = "[Gmail]/Trash"
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.login = "example@gmail.com"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 465
message.send.backend.login = "example@gmail.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.cmd = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show gmail"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "gmail-example"
```
Running `himalaya configure -a gmail` will ask for your IMAP password, just paste the one generated previously.
### Using OAuth 2.0
This option is the most secure but the hardest to configure. It requires the `oauth2` and `keyring` cargo features.
First, you need to get your OAuth 2.0 credentials by following [this guide](https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name-.). Once you get your client id and your client secret, you can configure your Himalaya account this way:
```toml
[accounts.gmail]
email = "example@gmail.com"
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "[Gmail]/Sent Mail"
folder.aliases.drafts = "[Gmail]/Drafts"
folder.aliases.trash = "[Gmail]/Trash"
backend.type = "imap"
backend.host = "imap.gmail.com"
backend.port = 993
backend.login = "example@gmail.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "*****"
backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
backend.auth.access-token.keyring = "gmail-oauth2-access-token"
backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
backend.auth.pkce = true
backend.auth.scope = "https://mail.google.com/"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.gmail.com"
message.send.backend.port = 465
message.send.backend.login = "example@gmail.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "*****"
message.send.backend.auth.client-secret.keyring = "gmail-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "gmail-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "gmail-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://accounts.google.com/o/oauth2/v2/auth"
message.send.backend.auth.token-url = "https://www.googleapis.com/oauth2/v3/token"
message.send.backend.auth.pkce = true
message.send.backend.auth.scope = "https://mail.google.com/"
```
Running `himalaya configure -a gmail` will complete your OAuth 2.0 setup and ask for your client secret.
</details>
<details>
<summary>Outlook</summary>
```toml
[accounts.outlook]
email = "example@outlook.com"
backend.type = "imap"
backend.host = "outlook.office365.com"
backend.port = 993
backend.login = "example@outlook.com"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp-mail.outlook.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "example@outlook.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show outlook"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "outlook-example"
```
Running `himalaya configure -a outlook` will ask for your IMAP password, just paste the one generated previously.
### Using OAuth 2.0
This option is the most secure but the hardest to configure. First, you need to get your OAuth 2.0 credentials by following [this guide](https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth). Once you get your client id and your client secret, you can configure your Himalaya account this way:
```toml
[accounts.outlook]
email = "example@outlook.com"
backend.type = "imap"
backend.host = "outlook.office365.com"
backend.port = 993
backend.login = "example@outlook.com"
backend.auth.type = "oauth2"
backend.auth.client-id = "*****"
backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
backend.auth.access-token.keyring = "outlook-oauth2-access-token"
backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
backend.auth.pkce = true
backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.mail.outlook.com"
message.send.backend.port = 587
message.send.backend.starttls = true
message.send.backend.login = "example@outlook.com"
message.send.backend.auth.type = "oauth2"
message.send.backend.auth.client-id = "*****"
message.send.backend.auth.client-secret.keyring = "outlook-oauth2-client-secret"
message.send.backend.auth.access-token.keyring = "outlook-oauth2-access-token"
message.send.backend.auth.refresh-token.keyring = "outlook-oauth2-refresh-token"
message.send.backend.auth.auth-url = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
message.send.backend.auth.token-url = "https://login.microsoftonline.com/common/oauth2/v2.0/token"
message.send.backend.auth.pkce = true
message.send.backend.auth.scopes = ["https://outlook.office.com/IMAP.AccessAsUser.All", "https://outlook.office.com/SMTP.Send"]
```
Running `himalaya configure -a outlook` will complete your OAuth 2.0 setup and ask for your client secret.
</details>
<details>
<summary>iCloud Mail</summary>
From the [iCloud Mail](https://support.apple.com/en-us/HT202304) support page:
- IMAP port = `993`.
- IMAP login = name of your iCloud Mail email address (for example, `johnappleseed`, not `johnappleseed@icloud.com`)
- SMTP port = `587` with `STARTTLS`
- SMTP login = full iCloud Mail email address (for example, `johnappleseed@icloud.com`, not `johnappleseed`)
```toml
[accounts.icloud]
email = "johnappleseed@icloud.com"
backend.type = "imap"
backend.host = "imap.mail.me.com"
backend.port = 993
backend.login = "johnappleseed"
backend.auth.type = "password"
backend.auth.raw = "*****"
message.send.backend.type = "smtp"
message.send.backend.host = "smtp.mail.me.com"
message.send.backend.port = 587
message.send.backend.encryption.type = "start-tls"
message.send.backend.login = "johnappleseed@icloud.com"
message.send.backend.auth.type = "password"
message.send.backend.auth.raw = "*****"
```
Keeping your password inside the configuration file is good for testing purpose, but it is not safe. You have 2 better alternatives:
- Save your password in any password manager that can be queried via the CLI:
```toml
backend.auth.cmd = "pass show icloud"
```
- Use the global keyring of your system (requires the `keyring` cargo feature):
```toml
backend.auth.keyring = "icloud-example"
```
Running `himalaya configure -a icloud` will ask for your IMAP password, just paste the one generated previously.
</details>
## Other interfaces
- [pimalaya/himalaya-vim](https://github.com/pimalaya/himalaya-vim), a Vim plugin sitting at the top of Himalaya CLI
- [dantecatalfamo/himalaya-emacs](https://github.com/dantecatalfamo/himalaya-emacs), an Emacs plugin sitting at the top of Himalaya CLI
- [jns/himalaya-raycast](https://www.raycast.com/jns/himalaya), a Raycast extension for Himalaya CLI
- [pimalaya/himalaya-repl](https://github.com/pimalaya/himalaya-repl), an experimental Read-Eval-Print-Loop variant of Himalaya CLI
## FAQ
<details>
<summary>How different is it from aerc, mutt or alpine?</summary>
Aerc, mutt and alpine can be categorized as Terminal User Interfaces (TUI). When the program is executed, your terminal is locked into an event loop and you interact with your emails using keybinds.
Himalaya is also a TUI, but more specifically a Command-Line Interface (CLI). There is no event loop: you interact with your emails using shell commands, in a stateless way.
Additionaly, Himalaya CLI is based on `email-lib`, which is also part of the Pimalaya project. The aim is not just to propose a new terminal interface, but also to expose Rust tools to deal with emails. Anyone who knows Rust language can build his own email interface, without re-inventing the wheel.
</details>
<details>
<summary>How to compose a message?</summary>
An email message is a list of **headers** (`key: val`) followed by a **body**. They form together a template:
```eml
Header: value
Header: value
Header: value
Body
```
***Headers and body must be separated by an empty line.***
### Headers
Here a non-exhaustive list of valid email message template headers:
- `Message-ID`: represents the message identifier (you usually do not need to set up it manually)
- `In-Reply-To`: represents the identifier of the replied message
- `Date`: represents the date of the message
- `Subject`: represents the subject of the message
- `From`: represents the address of the sender
- `To`: represents the addresses of the receivers
- `Reply-To`: represents the address the receiver should reply to instead of the `From` header
- `Cc`: represents the addresses of the other receivers (carbon copy)
- `Bcc`: represents the addresses of the other hidden receivers (blind carbon copy)
An address can be:
- a single email address `user@domain`
- a named address `Name <user@domain>`
- a quoted named address `"Name" <user@domain>`
Multiple address are separated by a coma `,`: `user@domain, Name <user@domain>, "Name" <user@domain>`.
### Plain text body
Email message template body can be written in plain text. The result will be compiled into a single `text/plain` MIME part:
```eml
From: alice@localhost
To: Bob <bob@localhost>
Subject: Hello from Himalaya
Hello, world!
```
### MML body
Email message template body can also be written in MML. The MIME Meta Language was introduced by the Emacs [`mml`](https://www.gnu.org/software/emacs/manual/html_node/emacs-mime/Composing.html) ELisp module. Pimalaya [ported it](https://github.com/pimalaya/core/tree/master/mml) in Rust.
A raw email message is structured according to the [MIME](https://www.rfc-editor.org/rfc/rfc2045) standard. This standard produces verbose, non-friendly messages. Here comes MML: it simplifies the way email message body are structured. Thanks to its simple XML-based syntax, it allows you to easily add multiple parts, attach a binary file, or attach inline image to your body without dealing with the MIME standard.
For instance, this MML template:
```eml
From: alice@localhost
To: bob@localhost
Subject: MML simple
<#multipart type=alternative>
This is a plain text part.
<#part type=text/enriched>
<center>This is a centered enriched part</center>
<#/multipart>
```
compiles into the following MIME Message:
```eml
Subject: MML simple
To: bob@localhost
From: alice@localhost
MIME-Version: 1.0
Date: Tue, 29 Nov 2022 13:07:01 +0000
Content-Type: multipart/alternative;
boundary="4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe"
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: 7bit
This is a plain text part.
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe
Content-Type: text/enriched
Content-Transfer-Encoding: 7bit
<center>This is a centered enriched part</center>
--4CV1Cnp7mXkDyvb55i77DcNSkKzB8HJzaIT84qZe--
```
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
</details>
<details>
<summary>How to add attachments to a message?</summary>
*Read first about the FAQ: How to compose a message?*.
```eml
From: alice@localhost
To: bob@localhost
Subject: How to attach stuff
Regular binary attachment:
<#part filename=/path/to/file.pdf><#/part>
Custom file name:
<#part filename=/path/to/file.pdf name=custom.pdf><#/part>
Inline image:
<#part disposition=inline filename=/path/to/image.png><#/part>
```
*See more examples at [pimalaya/core/mml](https://github.com/pimalaya/core/tree/master/mml/examples).*
</details>
<details>
<summary>How to debug Himalaya CLI?</summary>
The simplest way is to use `--debug` and `--trace` arguments.
The advanced way is based on environment variables:
- `RUST_LOG=<level>`: determines the log level filter, can be one of `off`, `error`, `warn`, `info`, `debug` and `trace`.
- `RUST_SPANTRACE=1`: enables the spantrace (a span represent periods of time in which a program was executing in a particular context).
- `RUST_BACKTRACE=1`: enables the error backtrace.
- `RUST_BACKTRACE=full`: enables the full error backtrace, which include source lines where the error originated from.
Logs are written to the `stderr`, which means that you can redirect them easily to a file:
```
RUST_LOG=debug himalaya 2>/tmp/himalaya.log
```
</details>
<details>
<summary>How the wizard discovers IMAP/SMTP configs?</summary>
All the lookup mechanisms use the email address domain as base for the lookup. It is heavily inspired from the Thunderbird [Autoconfiguration](https://udn.realityripple.com/docs/Mozilla/Thunderbird/Autoconfiguration) protocol. For example, for the email address `test@example.com`, the lookup is performed as (in this order):
1. check for `autoconfig.example.com`
2. look up of `example.com` in the ISPDB (the Thunderbird central database)
3. look up `MX example.com` in DNS, and for `mx1.mail.hoster.com`, look up `hoster.com` in the ISPDB
4. look up `SRV example.com` in DNS
5. try to guess (`imap.example.com`, `smtp.example.com`…)
</details>
<details>
<summary>How to disable color output?</summary>
Simply set the environment variable NO_COLOR=1
</details>
## Sponsoring
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/project/Himalaya/index.html)
[![nlnet](https://nlnet.nl/logo/banner-160x60.png)](https://nlnet.nl/)
Special thanks to the [NLnet foundation](https://nlnet.nl/project/Himalaya/index.html) and the [European Commission](https://www.ngi.eu/) that helped the project to receive financial support from:
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/assure/) in 2022
- [NGI Zero Entrust](https://nlnet.nl/entrust/) in 2023
- [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

@ -1,13 +0,0 @@
[Unit]
Description=Email client Himalaya CLI envelopes watcher service
After=network.target
[Service]
Type=exec
ExecStart=%install_dir%/himalaya envelopes watch --account %i
ExecSearchPath=/bin
Restart=always
RestartSec=10
[Install]
WantedBy=default.target

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,98 +1,651 @@
# The account name.
[accounts.example]
################################################################################
###[ Global configuration ]#####################################################
################################################################################
# The current account will be used by default for all other commands.
default = true
# Default display name for all accounts. It is used to build the full
# email address of an account: "Example" <example@localhost>
#
display-name = "Example"
# The display-name and the email are used to build the full email
# address: "My example account" <example@localhost>
display-name = "My example account"
email = "example@localhost"
# 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"
# The signature can be a string or a path to a file.
signature = "Regards,"
# Default signature delimiter for all accounts. It delimits the end of
# the message body from the signature.
#
signature-delim = "-- \n"
# Enable the synchronization for this account. Running the command
# `account sync example` will synchronize all folders and all emails
# to a local Maildir at `$XDG_DATA_HOME/himalaya/example`.
sync.enable = false
# Default downloads directory path for all accounts. It is mostly used
# for downloading attachments. Defaults to the system temporary
# directory.
#
downloads-dir = "~/Downloads"
# Override the default Maildir path for synchronization.
sync.dir = "/tmp/himalaya-sync-example"
# 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 = "|| |-||| "
# Filter folders to sync
folder.sync.filter.include = ["INBOX"]
# folder.sync.filter.exclude = ["All mails"]
# folder.sync.filter = "all"
# Customizes the color of the NAME column of the account listing
# table.
#
account.list.table.name-color = "green"
# Define main folder aliases
folder.alias.inbox = "INBOX"
folder.alias.sent = "Sent"
folder.alias.drafts = "Drafts"
folder.alias.trash = "Trash"
# Customizes the color of the BACKENDS column of the account listing
# table.
#
account.list.table.backends-color = "blue"
# Also define custom folder aliases
folder.alias.prev-year = "Archives/2023"
# Customizes the color of the DEFAULT column of the account listing
# table.
#
account.list.table.default-color = "black"
# Default backend used for all the features like adding folders,
# listing envelopes or copying messages.
backend = "imap"
################################################################################
###[ Account configuration ]####################################################
################################################################################
# The account name should be unique.
#
[accounts.example]
# Defaultness of the account. The current account will be used by
# default in all commands.
#
default = true
# The email address associated to the current account.
#
email = "example@localhost"
# The display name of the account. This and the email are used to
# build the full email address: "Example" <example@localhost>
#
display-name = "Example"
# The signature put at the bottom of composed messages. It can be a
# path or a string. Supports TOML multilines.
#
#signature = "/path/to/signature/file"
#signature = """
# Thanks you,
# Regards
#"""
signature = "Regards,\n"
# Signature delimiter. It delimits the end of the message body from
# the signature.
#
signature-delim = "-- \n"
# Downloads directory path. It is mostly used for downloading
# attachments. Defaults to the system temporary directory.
#
downloads-dir = "~/downloads"
# Defines aliases for your mailboxes. There are 4 special aliases used
# by the tool: inbox, sent, drafts and trash. Other aliases can be
# defined as well.
#
folder.aliases.inbox = "INBOX"
folder.aliases.sent = "Sent"
folder.aliases.drafts = "Drafts"
folder.aliases.trash = "Trash"
folder.aliases.a23 = "Archives/2023"
# Customizes the number of folders to show by page.
#
folder.list.page-size = 10
# Customizes the charset used to build the table. Defaults to markdown
# table style.
#
# See <https://docs.rs/comfy-table/latest/comfy_table/presets/index.html>.
#
folder.list.table.preset = "|| |-||| "
# Customizes the color of the NAME column of the folder listing table.
#
folder.list.table.name-color = "blue"
# Customizes the color of the DESC column of the folder listing table.
#
folder.list.table.desc-color = "green"
# Customizes the number of envelopes to show by page.
#
envelope.list.page-size = 10
# Customizes the format of the envelope date.
#
# See supported formats at <https://docs.rs/chrono/latest/chrono/format/strftime/>.
#
envelope.list.datetime-fmt = "%F %R%:z"
# Date are converted to the user's local timezone.
# 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
# Override the backend used for listing envelopes.
# envelope.list.backend = "imap"
# 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 = "|| |-||| "
# Send notification on receiving new envelopes
envelope.watch.received.notify.summary = "📬 New message from {sender}"
# Customizes the character of the unseen flag of the envelope listing
# table.
#
envelope.list.table.unseen-char = "*"
# Available placeholders: id, subject, sender, sender.name,
# sender.address, recipient, recipient.name, recipient.address.
envelope.watch.received.notify.body = "{subject}"
# Customizes the character of the replied flag of the envelope listing
# table.
#
envelope.list.table.replied-char = "R"
# Shell commands can also be executed when envelopes change
# envelope.watch.any.cmd = "mbsync -a"
# Customizes the character of the flagged flag of the envelope listing
# table.
#
envelope.list.table.flagged-char = "!"
# Override the backend used for sending messages.
message.send.backend = "smtp"
# Customizes the character of the attachment property of the envelope
# listing table.
#
envelope.list.table.attachment-char = "@"
# Save a copy of sent messages to the sent folder.
message.send.save-copy = false
# Customizes the color of the ID column of the envelope listing table.
#
envelope.list.table.id-color = "red"
# IMAP config
imap.host = "localhost"
imap.port = 3143
imap.login = "example@localhost"
# Customizes the color of the FLAGS column of the envelope listing
# table.
#
envelope.list.table.flags-color = "black"
# Encryption can be either "tls" (or true), "start-tls" or "none" (or false).
imap.encryption = "none"
# Customizes the color of the SUBJECT column of the envelope listing
# table.
#
envelope.list.table.subject-color = "green"
# Get password from a raw string (not safe)
imap.passwd.raw = "password"
# Customizes the color of the SENDER column of the envelope listing
# table.
#
envelope.list.table.sender-color = "blue"
# Get password from a shell command
# imap.passwd.cmd = "echo password"
# Customizes the color of the DATE column of the envelope listing
# table.
#
envelope.list.table.date-color = "yellow"
# Get password from your global system keyring using secret service
# Keyring secrets can be (re)set with the command `account configure example`
# imap.passwd.keyring = "example-imap-password"
# Customize at which period, in seconds, the IMAP IDLE mode should refresh.
# Defaults to 1740 (29 min), as defined in the RFC.
# imap.watch.timeout = 25
# SMTP config
smtp.host = "localhost"
smtp.port = 3025
smtp.login = "example@localhost"
smtp.encryption = false
smtp.passwd.raw = "password"
# Defines headers to show at the top of messages when reading them.
#
message.read.headers = ["From", "To", "Cc", "Subject"]
# PGP needs to be enabled with one of those cargo feature:
# pgp-commands, pgp-gpg or pgp-native
# pgp.backend = "gpg"
# 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" ]
)

98
flake.lock generated
View file

@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1713081044,
"narHash": "sha256-ZwbJDrizU+nzU7wTgokYuu5yK71wLPmOLukiunm5B6Y=",
"lastModified": 1732405626,
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
"owner": "soywod",
"repo": "fenix",
"rev": "af99e7e9c87389c0a5aaf953478664d7126c2b14",
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
"type": "github"
},
"original": {
@ -21,95 +21,53 @@
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1698420672,
"narHash": "sha256-/TdeHMPRjjdJub7p7+w55vyABrsJlt5QkznPYy55vKA=",
"owner": "nix-community",
"repo": "naersk",
"rev": "aeb58d5e8faead8980a807c840232697982d47b9",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1713145326,
"narHash": "sha256-m7+IWM6mkWOg22EC5kRUFCycXsXLSU7hWmHdmBfmC3s=",
"lastModified": 1736437047,
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "53a2c32bc66f5ae41a28d7a9a49d321172af621e",
"rev": "f17b95775191ea44bc426831235d87affb10faba",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-23.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",
"gitignore": "gitignore",
"naersk": "naersk",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"pimalaya": "pimalaya"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1712818880,
"narHash": "sha256-VDxsvgj/bNypHq48tQWtc3VRbWvzlFjzKf9ZZIVO10Y=",
"lastModified": 1732050317,
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "657b33b0cb9bd49085202e91ad5b4676532c9140",
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
"type": "github"
},
"original": {

217
flake.nix
View file

@ -2,218 +2,25 @@
description = "CLI to manage emails";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
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 = {
# 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, gitignore, fenix, naersk, ... }:
let
inherit (gitignore.lib) gitignoreSource;
staticRustFlags = [ "-Ctarget-feature=+crt-static" ];
# Map of map matching supported Nix build systems with Rust
# cross target systems.
crossBuildTargets = {
x86_64-linux = {
x86_64-linux = {
rustTarget = "x86_64-unknown-linux-musl";
override = { ... }: { };
};
arm64-linux = rec {
rustTarget = "aarch64-unknown-linux-musl";
override = { system, pkgs }:
let
inherit (mkPkgsCross system rustTarget) stdenv;
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc"; in
rec {
TARGET_CC = cc;
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" ];
postInstall = mkPostInstall {
inherit pkgs;
bin = "${pkgs.qemu}/bin/qemu-aarch64 ./himalaya";
};
};
};
x86_64-windows = {
rustTarget = "x86_64-pc-windows-gnu";
override = { system, pkgs }:
let
inherit (pkgs) pkgsCross zip;
inherit (pkgsCross.mingwW64) stdenv windows;
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";
wine = pkgs.wine.override { wineBuild = "wine64"; };
postInstall = mkPostInstall {
inherit pkgs;
bin = "${wine}/bin/wine64 ./himalaya.exe";
};
in
{
depsBuildBuild = [ stdenv.cc windows.pthreads ];
TARGET_CC = cc;
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" ];
postInstall = ''
export WINEPREFIX="$(mktemp -d)"
${postInstall}
'';
};
};
};
x86_64-darwin = {
x86_64-macos = {
rustTarget = "x86_64-apple-darwin";
override = { pkgs, ... }:
let inherit (pkgs.darwin.apple_sdk.frameworks) AppKit Cocoa; in
{
buildInputs = [ Cocoa ];
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit";
};
};
# FIXME: infinite recursion in stdenv?!
arm64-macos = {
rustTarget = "aarch64-apple-darwin";
override = { system, pkgs }:
let
# inherit (mkPkgsCross system "aarch64-darwin") stdenv;
inherit ((mkPkgsCross system "aarch64-darwin").pkgsStatic) stdenv darwin;
inherit (darwin.apple_sdk.frameworks) AppKit Cocoa;
cc = "${stdenv.cc}/bin/${stdenv.cc.targetPrefix}cc";
in
rec {
buildInputs = [ Cocoa ];
NIX_LDFLAGS = "-F${AppKit}/Library/Frameworks -framework AppKit -F${Cocoa}/Library/Frameworks -framework Cocoa";
NIX_CFLAGS_COMPILE = "-F${AppKit}/Library/Frameworks -framework AppKit -F${Cocoa}/Library/Frameworks -framework Cocoa";
TARGET_CC = cc;
CARGO_BUILD_RUSTFLAGS = staticRustFlags ++ [ "-Clinker=${cc}" "-lframework=${Cocoa}/Library/Frameworks" ];
postInstall = mkPostInstall {
inherit pkgs;
bin = "${pkgs.qemu}/bin/qemu-aarch64 ./himalaya";
};
};
};
};
};
mkToolchain = import ./rust-toolchain.nix fenix;
mkPkgsCross = buildSystem: crossSystem: import nixpkgs {
system = buildSystem;
crossSystem.config = crossSystem;
};
mkPostInstall = { pkgs, bin ? "./himalaya" }: with pkgs; ''
cd $out/bin
mkdir -p {man,completions}
${bin} man ./man
${bin} completion bash > ./completions/himalaya.bash
${bin} completion elvish > ./completions/himalaya.elvish
${bin} completion fish > ./completions/himalaya.fish
${bin} completion powershell > ./completions/himalaya.powershell
${bin} completion zsh > ./completions/himalaya.zsh
tar -czf himalaya.tgz himalaya* man completions
${zip}/bin/zip -r himalaya.zip himalaya* man completions
'';
mkDevShells = buildPlatform:
let
pkgs = import nixpkgs { system = buildPlatform; };
rust-toolchain = mkToolchain.fromFile { system = buildPlatform; };
in
{
default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [ pkg-config ];
buildInputs = with pkgs; [
# Nix
# rnix-lsp
nixpkgs-fmt
# Rust
rust-toolchain
cargo-watch
# Email env
gnupg
gpgme
msmtp
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 = false;
auditable = false;
strictDeps = true;
CARGO_BUILD_TARGET = targetPlatform;
CARGO_BUILD_RUSTFLAGS = staticRustFlags;
postInstall = mkPostInstall { inherit pkgs; };
} // package;
in
naersk'.buildPackage package';
mkPackages = system:
let
pkgs = import nixpkgs { inherit system; };
mkPackage' = target: package: mkPackage pkgs system package.rustTarget (package.override { inherit system pkgs; });
in
builtins.mapAttrs mkPackage' crossBuildTargets.${system};
mkApp = drv:
let exePath = drv.passthru.exePath or "/bin/himalaya"; in
{
type = "app";
program = "${drv}${exePath}";
};
mkApps = buildPlatform:
let
pkgs = import nixpkgs { system = buildPlatform; };
mkApp' = target: package: mkApp self.packages.${buildPlatform}.${target};
in
builtins.mapAttrs mkApp' crossBuildTargets.${buildPlatform};
supportedSystems = builtins.attrNames crossBuildTargets;
mapSupportedSystem = nixpkgs.lib.genAttrs supportedSystems;
in
{
apps = mapSupportedSystem mkApps;
packages = mapSupportedSystem mkPackages;
devShells = mapSupportedSystem mkDevShells;
outputs =
inputs:
(import inputs.pimalaya).mkFlakeOutputs inputs {
shell = ./shell.nix;
default = ./default.nix;
};
}

View file

@ -9,7 +9,7 @@ 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:])
@ -23,14 +23,17 @@ case $system in
linux|freebsd)
case $machine in
x86_64) target=x86_64-linux;;
arm64|aarch64) target=arm64-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-macos;;
arm64|aarch64) target=arm64-macos;;
x86_64) target=x86_64-darwin;;
arm64|aarch64) target=aarch64-darwin;;
*) die "Unsupported machine $machine for system $system";;
esac;;

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,24 +0,0 @@
fenix:
let
file = ./rust-toolchain.toml;
sha256 = "+syqAd2kX8KVa8/U2gz3blIQTTsYYt3U63xBWaGOSc8=";
in
{
fromFile = { system }: fenix.packages.${system}.fromToolchainFile {
inherit file sha256;
};
fromTarget = { pkgs, buildPlatform, targetPlatform }:
let
name = (pkgs.lib.importTOML file).toolchain.channel;
fenixPackage = fenix.packages.${buildPlatform};
toolchain = fenixPackage.fromToolchainName { inherit name sha256; };
targetToolchain = fenixPackage.targets.${targetPlatform}.fromToolchainName { inherit name sha256; };
in
fenixPackage.combine [
toolchain.rustc
toolchain.cargo
targetToolchain.rust-std
];
}

View file

@ -1,4 +1,4 @@
[toolchain]
channel = "1.77.0"
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" ])

View file

@ -1,124 +0,0 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::context::BackendContextBuilder;
use tracing::info;
use crate::{
account::arg::name::OptionalAccountNameArg, backend, config::TomlConfig, printer::Printer,
};
/// Check up the given account.
///
/// This command performs a checkup of the given account. It checks if
/// the configuration is valid, if backend can be created and if
/// sessions work as expected.
#[derive(Debug, Parser)]
pub struct AccountCheckUpCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
}
impl AccountCheckUpCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing check up account command");
let account = self.account.name.as_ref().map(String::as_str);
printer.print_log("Checking configuration integrity…")?;
let (toml_account_config, account_config) = config.clone().into_account_configs(
account,
#[cfg(feature = "account-sync")]
true,
)?;
let used_backends = toml_account_config.get_used_backends();
printer.print_log("Checking backend context integrity…")?;
let ctx_builder = backend::BackendContextBuilder::new(
toml_account_config.clone(),
account_config,
Vec::from_iter(used_backends),
)
.await?;
let ctx = ctx_builder.clone().build().await?;
#[cfg(feature = "maildir")]
{
printer.print_log("Checking Maildir integrity…")?;
let maildir = ctx_builder
.maildir
.as_ref()
.and_then(|maildir| maildir.check_up())
.and_then(|f| ctx.maildir.as_ref().and_then(|ctx| f(ctx)));
if let Some(maildir) = maildir.as_ref() {
maildir.check_up().await?;
}
}
#[cfg(feature = "imap")]
{
printer.print_log("Checking IMAP integrity…")?;
let imap = ctx_builder
.imap
.as_ref()
.and_then(|imap| imap.check_up())
.and_then(|f| ctx.imap.as_ref().and_then(|ctx| f(ctx)));
if let Some(imap) = imap.as_ref() {
imap.check_up().await?;
}
}
#[cfg(feature = "notmuch")]
{
printer.print_log("Checking Notmuch integrity…")?;
let notmuch = ctx_builder
.notmuch
.as_ref()
.and_then(|notmuch| notmuch.check_up())
.and_then(|f| ctx.notmuch.as_ref().and_then(|ctx| f(ctx)));
if let Some(notmuch) = notmuch.as_ref() {
notmuch.check_up().await?;
}
}
#[cfg(feature = "smtp")]
{
printer.print_log("Checking SMTP integrity…")?;
let smtp = ctx_builder
.smtp
.as_ref()
.and_then(|smtp| smtp.check_up())
.and_then(|f| ctx.smtp.as_ref().and_then(|ctx| f(ctx)));
if let Some(smtp) = smtp.as_ref() {
smtp.check_up().await?;
}
}
#[cfg(feature = "sendmail")]
{
printer.print_log("Checking Sendmail integrity…")?;
let sendmail = ctx_builder
.sendmail
.as_ref()
.and_then(|sendmail| sendmail.check_up())
.and_then(|f| ctx.sendmail.as_ref().and_then(|ctx| f(ctx)));
if let Some(sendmail) = sendmail.as_ref() {
sendmail.check_up().await?;
}
}
printer.print("Checkup successfully completed!")
}
}

View file

@ -1,113 +1,52 @@
use std::path::PathBuf;
use clap::Parser;
use color_eyre::Result;
#[cfg(feature = "imap")]
use email::imap::config::ImapAuthConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpAuthConfig;
use tracing::info;
#[cfg(any(feature = "imap", feature = "smtp"))]
use tracing::{debug, warn};
#[cfg(any(feature = "imap", feature = "smtp", feature = "pgp"))]
use crate::ui::prompt;
use crate::{account::arg::name::AccountNameArg, config::TomlConfig, printer::Printer};
use crate::{account::arg::name::AccountNameArg, config::TomlConfig};
/// Configure an account.
/// Configure the given account.
///
/// This command is mostly used to define or reset passwords managed
/// by your global keyring. If you do not use the keyring system, you
/// can skip this command.
/// 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,
/// Reset keyring passwords.
///
/// This argument will force passwords to be prompted again, then
/// saved to your global keyring.
#[arg(long, short)]
pub reset: bool,
}
impl AccountConfigureCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing configure account command");
#[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;
let account = &self.account.name;
let (_, account_config) = config.into_toml_account_config(Some(account))?;
info!("executing account configure command");
if self.reset {
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
let reset = match &config.auth {
ImapAuthConfig::Passwd(config) => config.reset().await,
ImapAuthConfig::OAuth2(config) => config.reset().await,
};
if let Err(err) = reset {
warn!("error while resetting imap secrets: {err}");
debug!("error while resetting imap secrets: {err:?}");
}
}
let path = match config_path {
Some(path) => path.clone(),
None => TomlConfig::default_path()?,
};
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
let reset = match &config.auth {
SmtpAuthConfig::Passwd(config) => config.reset().await,
SmtpAuthConfig::OAuth2(config) => config.reset().await,
};
if let Err(err) = reset {
warn!("error while resetting smtp secrets: {err}");
debug!("error while resetting smtp secrets: {err:?}");
}
}
let account_name = Some(self.account.name.as_str());
#[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp {
config.reset().await?;
}
}
let account_config = config
.accounts
.remove(&self.account.name)
.unwrap_or_default();
#[cfg(feature = "imap")]
if let Some(ref config) = account_config.imap {
match &config.auth {
ImapAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("IMAP password")).await
}
ImapAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("IMAP OAuth 2.0 client secret"))
.await
}
}?;
}
wizard::edit(path, config, account_name, account_config).await?;
#[cfg(feature = "smtp")]
if let Some(ref config) = account_config.smtp {
match &config.auth {
SmtpAuthConfig::Passwd(config) => {
config.configure(|| prompt::passwd("SMTP password")).await
}
SmtpAuthConfig::OAuth2(config) => {
config
.configure(|| prompt::secret("SMTP OAuth 2.0 client secret"))
.await
}
}?;
}
Ok(())
}
#[cfg(feature = "pgp")]
if let Some(ref config) = account_config.pgp {
config
.configure(&account_config.email, || {
prompt::passwd("PGP secret key password")
})
.await?;
}
printer.print(format!(
"Account {account} successfully {}configured!",
if self.reset { "re" } else { "" }
))
#[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

@ -1,36 +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::{
account::Accounts,
config::TomlConfig,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
};
use crate::config::TomlConfig;
/// List all accounts.
/// List all existing accounts.
///
/// This command lists all accounts defined in your TOML configuration
/// file.
/// This command lists all the accounts defined in your TOML
/// configuration file.
#[derive(Debug, Parser)]
pub struct AccountListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
/// 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 = config.accounts.iter().into();
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.print_table(
Box::new(accounts),
PrintTableOpts {
format: &Default::default(),
max_width: self.table.max_width,
},
)
printer.out(table)
}
}

View file

@ -1,50 +1,41 @@
mod check_up;
mod configure;
mod doctor;
mod list;
#[cfg(feature = "account-sync")]
mod sync;
use color_eyre::Result;
use std::path::PathBuf;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
#[cfg(feature = "account-sync")]
use self::sync::AccountSyncCommand;
use self::{
check_up::AccountCheckUpCommand, configure::AccountConfigureCommand, list::AccountListCommand,
configure::AccountConfigureCommand, doctor::AccountDoctorCommand, list::AccountListCommand,
};
/// Manage accounts.
/// Configure, list and diagnose your accounts.
///
/// An account is a set of settings, identified by an account
/// name. Settings are directly taken from your TOML configuration
/// file. This subcommand allows you to manage them.
/// 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 {
#[command(alias = "checkup")]
CheckUp(AccountCheckUpCommand),
#[command(alias = "cfg")]
Configure(AccountConfigureCommand),
#[command(alias = "lst")]
Doctor(AccountDoctorCommand),
List(AccountListCommand),
#[cfg(feature = "account-sync")]
#[command(alias = "synchronize", alias = "synchronise")]
Sync(AccountSyncCommand),
}
impl AccountSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
pub async fn execute(
self,
printer: &mut impl Printer,
config: TomlConfig,
config_path: Option<&PathBuf>,
) -> Result<()> {
match self {
Self::CheckUp(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(printer, config).await,
Self::List(cmd) => cmd.execute(printer, config).await,
#[cfg(feature = "account-sync")]
Self::Sync(cmd) => cmd.execute(printer, config).await,
Self::Configure(cmd) => cmd.execute(config, config_path).await,
Self::Doctor(cmd) => cmd.execute(&config).await,
Self::List(cmd) => cmd.execute(printer, &config).await,
}
}
}

View file

@ -1,268 +0,0 @@
use clap::{ArgAction, Parser};
use color_eyre::{eyre::bail, eyre::eyre, Result};
use email::backend::context::BackendContextBuilder;
#[cfg(feature = "imap")]
use email::imap::ImapContextBuilder;
use email::{
account::sync::AccountSyncBuilder,
backend::BackendBuilder,
folder::sync::config::FolderSyncStrategy,
sync::{hash::SyncHash, SyncEvent},
};
use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
use once_cell::sync::Lazy;
use std::{
collections::{BTreeSet, HashMap},
sync::{Arc, Mutex},
};
use tracing::info;
use crate::{
account::arg::name::OptionalAccountNameArg, backend::BackendKind, config::TomlConfig,
printer::Printer,
};
static MAIN_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {spinner:.dim} {msg:.dim}\n {wide_bar:.cyan/blue} \n").unwrap()
});
static SUB_PROGRESS_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(
" {prefix:.bold} — {wide_msg:.dim} \n {wide_bar:.black/black} {percent}% ",
)
.unwrap()
});
static SUB_PROGRESS_DONE_STYLE: Lazy<ProgressStyle> = Lazy::new(|| {
ProgressStyle::with_template(" {prefix:.bold} \n {wide_bar:.green} {percent}% ").unwrap()
});
/// Synchronize an account.
///
/// This command allows you to synchronize all folders and emails
/// (including envelopes and messages) of a given account into a local
/// Maildir folder.
#[derive(Debug, Parser)]
pub struct AccountSyncCommand {
#[command(flatten)]
pub account: OptionalAccountNameArg,
/// Run the synchronization without applying any changes.
///
/// Instead, a report will be printed to stdout containing all the
/// changes the synchronization plan to do.
#[arg(long, short)]
pub dry_run: bool,
/// Synchronize only specific folders.
///
/// Only the given folders will be synchronized (including
/// associated envelopes and messages). Useful when you need to
/// speed up the synchronization process. A good usecase is to
/// synchronize only the INBOX in order to quickly check for new
/// messages.
#[arg(long, short = 'f')]
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
#[arg(conflicts_with = "exclude_folder", conflicts_with = "all_folders")]
pub include_folder: Vec<String>,
/// Omit specific folders from the synchronization.
///
/// The given folders will be excluded from the synchronization
/// (including associated envelopes and messages). Useful when you
/// have heavy folders that you do not want to take care of, or to
/// speed up the synchronization process.
#[arg(long, short = 'x')]
#[arg(value_name = "FOLDER", action = ArgAction::Append)]
#[arg(conflicts_with = "include_folder", conflicts_with = "all_folders")]
pub exclude_folder: Vec<String>,
/// Synchronize all exsting folders.
#[arg(long, short = 'A')]
#[arg(conflicts_with = "include_folder", conflicts_with = "exclude_folder")]
pub all_folders: bool,
}
impl AccountSyncCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing sync account command");
let account = self.account.name.as_deref();
let (toml_account_config, account_config) =
config.clone().into_account_configs(account, true)?;
let account_name = account_config.name.as_str();
match toml_account_config.sync_kind() {
Some(BackendKind::Imap) | Some(BackendKind::ImapCache) => {
let imap_config = toml_account_config
.imap
.as_ref()
.map(Clone::clone)
.map(Arc::new)
.ok_or_else(|| eyre!("imap config not found"))?;
let imap_ctx = ImapContextBuilder::new(account_config.clone(), imap_config)
.with_prebuilt_credentials()
.await?;
let imap = BackendBuilder::new(account_config.clone(), imap_ctx);
self.sync(printer, account_name, imap).await
}
Some(backend) => bail!("backend {backend:?} not supported for synchronization"),
None => bail!("no backend configured for synchronization"),
}
}
async fn sync(
self,
printer: &mut impl Printer,
account_name: &str,
right: BackendBuilder<impl BackendContextBuilder + SyncHash + 'static>,
) -> Result<()> {
let included_folders = BTreeSet::from_iter(self.include_folder);
let excluded_folders = BTreeSet::from_iter(self.exclude_folder);
let folder_filters = if !included_folders.is_empty() {
Some(FolderSyncStrategy::Include(included_folders))
} else if !excluded_folders.is_empty() {
Some(FolderSyncStrategy::Exclude(excluded_folders))
} else if self.all_folders {
Some(FolderSyncStrategy::All)
} else {
None
};
let sync_builder =
AccountSyncBuilder::try_new(right)?.with_some_folder_filters(folder_filters);
if self.dry_run {
let report = sync_builder.with_dry_run(true).sync().await?;
let mut hunks_count = report.folder.patch.len();
if !report.folder.patch.is_empty() {
printer.print_log("Folders patch:")?;
for (hunk, _) in report.folder.patch {
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
if !report.email.patch.is_empty() {
printer.print_log("Envelopes patch:")?;
for (hunk, _) in report.email.patch {
hunks_count += 1;
printer.print_log(format!(" - {hunk}"))?;
}
printer.print_log("")?;
}
printer.print(format!(
"Estimated patch length for account {account_name} to be synchronized: {hunks_count}"
))?;
} else if printer.is_json() {
sync_builder.sync().await?;
printer.print(format!("Account {account_name} 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("Listing folders…"),
);
main_progress.tick();
let report = sync_builder
.with_handler(move |evt| {
match evt {
SyncEvent::ListedAllFolders => {
main_progress.set_message("Synchronizing folders…");
}
SyncEvent::ProcessedAllFolderHunks => {
main_progress.set_message("Listing envelopes…");
}
SyncEvent::GeneratedEmailPatch(patches) => {
let patches_len = patches.values().flatten().count();
main_progress.set_length(patches_len as u64);
main_progress.set_position(0);
main_progress.set_message("Synchronizing emails…");
let mut envelopes_progresses = sub_progresses.lock().unwrap();
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());
}
}
SyncEvent::ProcessedEmailHunk(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}"));
}
}
}
SyncEvent::ProcessedAllEmailHunks => {
let mut progresses = sub_progresses.lock().unwrap();
for progress in progresses.values() {
progress.finish_and_clear()
}
progresses.clear();
main_progress.set_length(100);
main_progress.set_position(100);
main_progress.set_message("Expunging folders…");
}
SyncEvent::ExpungedAllFolders => {
main_progress.finish_and_clear();
}
_ => {
main_progress.tick();
}
};
async { Ok(()) }
})
.sync()
.await?;
let folders_patch_err = report
.folder
.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}")))?;
}
let envelopes_patch_err = report
.email
.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 envelopes_patch_err {
printer.print_log(format!(" - {hunk}: {err}"))?;
}
}
printer.print(format!("Account {account_name} successfully synchronized!"))?;
}
Ok(())
}
}

View file

@ -1,258 +1,3 @@
//! Deserialized account config module.
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
use pimalaya_tui::himalaya::config::HimalayaTomlAccountConfig;
#[cfg(feature = "pgp")]
use email::account::config::pgp::PgpConfig;
#[cfg(feature = "imap")]
use email::imap::config::ImapConfig;
#[cfg(feature = "maildir")]
use email::maildir::config::MaildirConfig;
#[cfg(feature = "notmuch")]
use email::notmuch::config::NotmuchConfig;
#[cfg(feature = "sendmail")]
use email::sendmail::config::SendmailConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpConfig;
use email::template::config::TemplateConfig;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::PathBuf};
use crate::{
backend::BackendKind, envelope::config::EnvelopeConfig, flag::config::FlagConfig,
folder::config::FolderConfig, message::config::MessageConfig,
};
#[cfg(feature = "account-sync")]
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct SyncConfig {
pub backend: Option<BackendKind>,
pub enable: Option<bool>,
pub dir: Option<PathBuf>,
}
impl From<SyncConfig> for email::account::sync::config::SyncConfig {
fn from(config: SyncConfig) -> Self {
Self {
enable: config.enable,
dir: config.dir,
..Default::default()
}
}
}
/// Represents all existing kind of account config.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct TomlAccountConfig {
pub default: Option<bool>,
pub email: String,
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub backend: Option<BackendKind>,
#[cfg(feature = "account-sync")]
pub sync: Option<SyncConfig>,
#[cfg(feature = "pgp")]
pub pgp: Option<PgpConfig>,
pub folder: Option<FolderConfig>,
pub envelope: Option<EnvelopeConfig>,
pub flag: Option<FlagConfig>,
pub message: Option<MessageConfig>,
pub template: Option<TemplateConfig>,
#[cfg(feature = "imap")]
pub imap: Option<ImapConfig>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirConfig>,
#[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchConfig>,
#[cfg(feature = "smtp")]
pub smtp: Option<SmtpConfig>,
#[cfg(feature = "sendmail")]
pub sendmail: Option<SendmailConfig>,
}
impl TomlAccountConfig {
pub fn sync_kind(&self) -> Option<&BackendKind> {
self.sync
.as_ref()
.and_then(|sync| sync.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn add_folder_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
.and_then(|folder| folder.add.as_ref())
.and_then(|add| add.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn list_folders_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
.and_then(|folder| folder.list.as_ref())
.and_then(|list| list.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn expunge_folder_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
.and_then(|folder| folder.expunge.as_ref())
.and_then(|expunge| expunge.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn purge_folder_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
.and_then(|folder| folder.purge.as_ref())
.and_then(|purge| purge.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn delete_folder_kind(&self) -> Option<&BackendKind> {
self.folder
.as_ref()
.and_then(|folder| folder.delete.as_ref())
.and_then(|delete| delete.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn get_envelope_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.get.as_ref())
.and_then(|get| get.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn list_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.list.as_ref())
.and_then(|list| list.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn watch_envelopes_kind(&self) -> Option<&BackendKind> {
self.envelope
.as_ref()
.and_then(|envelope| envelope.watch.as_ref())
.and_then(|watch| watch.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn add_flags_kind(&self) -> Option<&BackendKind> {
self.flag
.as_ref()
.and_then(|flag| flag.add.as_ref())
.and_then(|add| add.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn set_flags_kind(&self) -> Option<&BackendKind> {
self.flag
.as_ref()
.and_then(|flag| flag.set.as_ref())
.and_then(|set| set.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn remove_flags_kind(&self) -> Option<&BackendKind> {
self.flag
.as_ref()
.and_then(|flag| flag.remove.as_ref())
.and_then(|remove| remove.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn add_message_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|msg| msg.write.as_ref())
.and_then(|add| add.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn peek_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.peek.as_ref())
.and_then(|peek| peek.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn get_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.read.as_ref())
.and_then(|get| get.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn copy_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.copy.as_ref())
.and_then(|copy| copy.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn move_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.r#move.as_ref())
.and_then(|move_| move_.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn delete_messages_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|message| message.delete.as_ref())
.and_then(|delete| delete.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn send_message_kind(&self) -> Option<&BackendKind> {
self.message
.as_ref()
.and_then(|msg| msg.send.as_ref())
.and_then(|send| send.backend.as_ref())
.or(self.backend.as_ref())
}
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut used_backends = HashSet::default();
if let Some(ref kind) = self.backend {
used_backends.insert(kind);
}
if let Some(ref folder) = self.folder {
used_backends.extend(folder.get_used_backends());
}
if let Some(ref envelope) = self.envelope {
used_backends.extend(envelope.get_used_backends());
}
if let Some(ref flag) = self.flag {
used_backends.extend(flag.get_used_backends());
}
if let Some(ref msg) = self.message {
used_backends.extend(msg.get_used_backends());
}
used_backends
}
}
pub type TomlAccountConfig = HimalayaTomlAccountConfig;

View file

@ -1,135 +1,3 @@
pub mod arg;
pub mod command;
pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
use serde::Serialize;
use std::{collections::hash_map::Iter, fmt, ops::Deref};
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::table::{Cell, Row, Table},
};
use self::config::TomlAccountConfig;
/// 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("BACKENDS").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())
}
}
/// 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, TomlAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, TomlAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, account)| {
#[allow(unused_mut)]
let mut backends = String::new();
#[cfg(feature = "imap")]
if account.imap.is_some() {
backends.push_str("imap");
}
#[cfg(feature = "maildir")]
if account.maildir.is_some() {
if !backends.is_empty() {
backends.push_str(", ")
}
backends.push_str("maildir");
}
#[cfg(feature = "notmuch")]
if account.imap.is_some() {
if !backends.is_empty() {
backends.push_str(", ")
}
backends.push_str("notmuch");
}
#[cfg(feature = "smtp")]
if account.smtp.is_some() {
if !backends.is_empty() {
backends.push_str(", ")
}
backends.push_str("smtp");
}
#[cfg(feature = "sendmail")]
if account.sendmail.is_some() {
if !backends.is_empty() {
backends.push_str(", ")
}
backends.push_str("sendmail");
}
Account::new(name, &backends, account.default.unwrap_or_default())
})
.collect();
// sort accounts by name
accounts.sort_by(|a, b| a.name.partial_cmp(&b.name).unwrap());
Self(accounts)
}
}

View file

@ -1,161 +0,0 @@
#[cfg(feature = "account-sync")]
use crate::account::config::SyncConfig;
use color_eyre::{eyre::bail, Result};
#[cfg(feature = "account-sync")]
use dialoguer::Confirm;
use dialoguer::Input;
use email_address::EmailAddress;
use std::str::FromStr;
#[cfg(feature = "account-sync")]
use crate::wizard_prompt;
#[cfg(feature = "account-discovery")]
use crate::wizard_warn;
use crate::{
backend::{self, config::BackendConfig, BackendKind},
message::config::{MessageConfig, MessageSendConfig},
ui::THEME,
};
use super::TomlAccountConfig;
pub(crate) async fn configure() -> Result<Option<(String, TomlAccountConfig)>> {
let mut config = TomlAccountConfig::default();
config.email = Input::with_theme(&*THEME)
.with_prompt("Email address")
.validate_with(|email: &String| {
if EmailAddress::is_valid(email) {
Ok(())
} else {
bail!("Invalid email address: {email}")
}
})
.interact()?;
let addr = EmailAddress::from_str(&config.email).unwrap();
#[cfg(feature = "account-discovery")]
let autoconfig_email = config.email.to_owned();
#[cfg(feature = "account-discovery")]
let autoconfig = tokio::spawn(async move {
email::account::discover::from_addr(&autoconfig_email)
.await
.ok()
});
let account_name = Input::with_theme(&*THEME)
.with_prompt("Account name")
.default(addr.domain().split_once('.').unwrap().0.to_owned())
.interact()?;
config.display_name = Some(
Input::with_theme(&*THEME)
.with_prompt("Full display name")
.default(addr.local_part().to_owned())
.interact()?,
);
config.downloads_dir = Some(
Input::with_theme(&*THEME)
.with_prompt("Downloads directory")
.default(String::from("~/Downloads"))
.interact()?
.into(),
);
let email = &config.email;
#[cfg(feature = "account-discovery")]
let autoconfig = autoconfig.await?;
#[cfg(feature = "account-discovery")]
let autoconfig = autoconfig.as_ref();
#[cfg(feature = "account-discovery")]
if let Some(config) = autoconfig {
if config.is_gmail() {
println!();
wizard_warn!("Warning: Google passwords cannot be used directly, see:");
wizard_warn!("https://pimalaya.org/himalaya/cli/latest/configuration/gmail.html");
println!();
}
}
match backend::wizard::configure(
&account_name,
email,
#[cfg(feature = "account-discovery")]
autoconfig,
)
.await?
{
#[cfg(feature = "imap")]
Some(BackendConfig::Imap(imap_config)) => {
config.imap = Some(imap_config);
config.backend = Some(BackendKind::Imap);
}
#[cfg(feature = "maildir")]
Some(BackendConfig::Maildir(mdir_config)) => {
config.maildir = Some(mdir_config);
config.backend = Some(BackendKind::Maildir);
}
#[cfg(feature = "notmuch")]
Some(BackendConfig::Notmuch(notmuch_config)) => {
config.notmuch = Some(notmuch_config);
config.backend = Some(BackendKind::Notmuch);
}
_ => (),
};
match backend::wizard::configure_sender(
&account_name,
email,
#[cfg(feature = "account-discovery")]
autoconfig,
)
.await?
{
#[cfg(feature = "smtp")]
Some(BackendConfig::Smtp(smtp_config)) => {
config.smtp = Some(smtp_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Smtp),
..Default::default()
}),
..Default::default()
});
}
#[cfg(feature = "sendmail")]
Some(BackendConfig::Sendmail(sendmail_config)) => {
config.sendmail = Some(sendmail_config);
config.message = Some(MessageConfig {
send: Some(MessageSendConfig {
backend: Some(BackendKind::Sendmail),
..Default::default()
}),
..Default::default()
});
}
_ => (),
};
#[cfg(feature = "account-sync")]
{
let should_configure_sync = Confirm::new()
.with_prompt(wizard_prompt!(
"Do you need offline access for your account?"
))
.default(false)
.interact_opt()?
.unwrap_or_default();
if should_configure_sync {
config.sync = Some(SyncConfig {
enable: Some(true),
..Default::default()
});
}
}
Ok(Some((account_name, config)))
}

View file

@ -1,24 +0,0 @@
#[cfg(feature = "imap")]
use email::imap::config::ImapConfig;
#[cfg(feature = "maildir")]
use email::maildir::config::MaildirConfig;
#[cfg(feature = "notmuch")]
use email::notmuch::config::NotmuchConfig;
#[cfg(feature = "sendmail")]
use email::sendmail::config::SendmailConfig;
#[cfg(feature = "smtp")]
use email::smtp::config::SmtpConfig;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BackendConfig {
#[cfg(feature = "imap")]
Imap(ImapConfig),
#[cfg(feature = "maildir")]
Maildir(MaildirConfig),
#[cfg(feature = "notmuch")]
Notmuch(NotmuchConfig),
#[cfg(feature = "smtp")]
Smtp(SmtpConfig),
#[cfg(feature = "sendmail")]
Sendmail(SendmailConfig),
}

View file

@ -1,831 +0,0 @@
pub mod config;
pub(crate) mod wizard;
use color_eyre::Result;
use async_trait::async_trait;
use std::{ops::Deref, sync::Arc};
#[cfg(feature = "imap")]
use email::imap::{ImapContextBuilder, ImapContextSync};
#[cfg(any(feature = "account-sync", feature = "maildir"))]
use email::maildir::{MaildirContextBuilder, MaildirContextSync};
#[cfg(feature = "notmuch")]
use email::notmuch::{NotmuchContextBuilder, NotmuchContextSync};
#[cfg(feature = "sendmail")]
use email::sendmail::{SendmailContextBuilder, SendmailContextSync};
#[cfg(feature = "smtp")]
use email::smtp::{SmtpContextBuilder, SmtpContextSync};
use email::{
account::config::AccountConfig,
backend::{
feature::BackendFeature, macros::BackendContext, mapper::SomeBackendContextBuilderMapper,
},
envelope::{
get::GetEnvelope,
list::{ListEnvelopes, ListEnvelopesOptions},
watch::WatchEnvelopes,
Id, SingleId,
},
flag::{add::AddFlags, remove::RemoveFlags, set::SetFlags, Flag, Flags},
folder::{
add::AddFolder, delete::DeleteFolder, expunge::ExpungeFolder, list::ListFolders,
purge::PurgeFolder,
},
message::{
add::AddMessage,
copy::CopyMessages,
delete::DeleteMessages,
get::GetMessages,
peek::PeekMessages,
r#move::MoveMessages,
send::{SendMessage, SendMessageThenSaveCopy},
Messages,
},
AnyResult,
};
use serde::{Deserialize, Serialize};
use crate::{account::config::TomlAccountConfig, cache::IdMapper, envelope::Envelopes};
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BackendKind {
None,
#[cfg(feature = "imap")]
Imap,
#[cfg(all(feature = "imap", feature = "account-sync"))]
ImapCache,
#[cfg(feature = "maildir")]
Maildir,
#[cfg(feature = "notmuch")]
Notmuch,
#[cfg(feature = "smtp")]
Smtp,
#[cfg(feature = "sendmail")]
Sendmail,
}
impl ToString for BackendKind {
fn to_string(&self) -> String {
let kind = match self {
Self::None => "None",
#[cfg(feature = "imap")]
Self::Imap => "IMAP",
#[cfg(all(feature = "imap", feature = "account-sync"))]
Self::ImapCache => "IMAP cache",
#[cfg(feature = "maildir")]
Self::Maildir => "Maildir",
#[cfg(feature = "notmuch")]
Self::Notmuch => "Notmuch",
#[cfg(feature = "smtp")]
Self::Smtp => "SMTP",
#[cfg(feature = "sendmail")]
Self::Sendmail => "Sendmail",
};
kind.to_string()
}
}
#[derive(Clone, Default)]
pub struct BackendContextBuilder {
pub toml_account_config: Arc<TomlAccountConfig>,
pub account_config: Arc<AccountConfig>,
#[cfg(feature = "imap")]
pub imap: Option<ImapContextBuilder>,
#[cfg(all(feature = "imap", feature = "account-sync"))]
pub imap_cache: Option<MaildirContextBuilder>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextBuilder>,
#[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchContextBuilder>,
#[cfg(feature = "smtp")]
pub smtp: Option<SmtpContextBuilder>,
#[cfg(feature = "sendmail")]
pub sendmail: Option<SendmailContextBuilder>,
}
impl BackendContextBuilder {
pub async fn new(
toml_account_config: Arc<TomlAccountConfig>,
account_config: Arc<AccountConfig>,
kinds: Vec<&BackendKind>,
) -> Result<Self> {
Ok(Self {
toml_account_config: toml_account_config.clone(),
account_config: account_config.clone(),
#[cfg(feature = "imap")]
imap: {
let builder = toml_account_config
.imap
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Imap))
.map(Clone::clone)
.map(Arc::new)
.map(|imap_config| {
ImapContextBuilder::new(account_config.clone(), imap_config)
.with_prebuilt_credentials()
});
match builder {
Some(builder) => Some(builder.await?),
None => None,
}
},
#[cfg(all(feature = "imap", feature = "account-sync"))]
imap_cache: {
let builder = toml_account_config
.imap
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::ImapCache))
.map(Clone::clone)
.map(Arc::new)
.map(|imap_config| {
email::backend::context::BackendContextBuilder::try_to_sync_cache_builder(
&ImapContextBuilder::new(account_config.clone(), imap_config),
&account_config,
)
});
match builder {
Some(builder) => Some(builder?),
None => None,
}
},
#[cfg(feature = "maildir")]
maildir: toml_account_config
.maildir
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Maildir))
.map(Clone::clone)
.map(Arc::new)
.map(|mdir_config| MaildirContextBuilder::new(account_config.clone(), mdir_config)),
#[cfg(feature = "notmuch")]
notmuch: toml_account_config
.notmuch
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Notmuch))
.map(Clone::clone)
.map(Arc::new)
.map(|notmuch_config| {
NotmuchContextBuilder::new(account_config.clone(), notmuch_config)
}),
#[cfg(feature = "smtp")]
smtp: toml_account_config
.smtp
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Smtp))
.map(Clone::clone)
.map(Arc::new)
.map(|smtp_config| SmtpContextBuilder::new(account_config.clone(), smtp_config)),
#[cfg(feature = "sendmail")]
sendmail: toml_account_config
.sendmail
.as_ref()
.filter(|_| kinds.contains(&&BackendKind::Sendmail))
.map(Clone::clone)
.map(Arc::new)
.map(|sendmail_config| {
SendmailContextBuilder::new(account_config.clone(), sendmail_config)
}),
})
}
}
#[async_trait]
impl email::backend::context::BackendContextBuilder for BackendContextBuilder {
type Context = BackendContext;
fn add_folder(&self) -> Option<BackendFeature<Self::Context, dyn AddFolder>> {
match self.toml_account_config.add_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_folder_with_some(&self.notmuch),
_ => None,
}
}
fn list_folders(&self) -> Option<BackendFeature<Self::Context, dyn ListFolders>> {
match self.toml_account_config.list_folders_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_folders_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.list_folders()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_folders_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.list_folders_with_some(&self.notmuch),
_ => None,
}
}
fn expunge_folder(&self) -> Option<BackendFeature<Self::Context, dyn ExpungeFolder>> {
match self.toml_account_config.expunge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.expunge_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.expunge_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.expunge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.expunge_folder_with_some(&self.notmuch),
_ => None,
}
}
fn purge_folder(&self) -> Option<BackendFeature<Self::Context, dyn PurgeFolder>> {
match self.toml_account_config.purge_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.purge_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.purge_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.purge_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.purge_folder_with_some(&self.notmuch),
_ => None,
}
}
fn delete_folder(&self) -> Option<BackendFeature<Self::Context, dyn DeleteFolder>> {
match self.toml_account_config.delete_folder_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_folder_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.delete_folder()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_folder_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.delete_folder_with_some(&self.notmuch),
_ => None,
}
}
fn get_envelope(&self) -> Option<BackendFeature<Self::Context, dyn GetEnvelope>> {
match self.toml_account_config.get_envelope_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_envelope_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.get_envelope()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_envelope_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.get_envelope_with_some(&self.notmuch),
_ => None,
}
}
fn list_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn ListEnvelopes>> {
match self.toml_account_config.list_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.list_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.list_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.list_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.list_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn watch_envelopes(&self) -> Option<BackendFeature<Self::Context, dyn WatchEnvelopes>> {
match self.toml_account_config.watch_envelopes_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.watch_envelopes_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.watch_envelopes()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.watch_envelopes_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.watch_envelopes_with_some(&self.notmuch),
_ => None,
}
}
fn add_flags(&self) -> Option<BackendFeature<Self::Context, dyn AddFlags>> {
match self.toml_account_config.add_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_flags_with_some(&self.notmuch),
_ => None,
}
}
fn set_flags(&self) -> Option<BackendFeature<Self::Context, dyn SetFlags>> {
match self.toml_account_config.set_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.set_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.set_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.set_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.set_flags_with_some(&self.notmuch),
_ => None,
}
}
fn remove_flags(&self) -> Option<BackendFeature<Self::Context, dyn RemoveFlags>> {
match self.toml_account_config.remove_flags_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.remove_flags_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.remove_flags()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.remove_flags_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.remove_flags_with_some(&self.notmuch),
_ => None,
}
}
fn add_message(&self) -> Option<BackendFeature<Self::Context, dyn AddMessage>> {
match self.toml_account_config.add_message_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.add_message_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.add_message()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.add_message_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.add_message_with_some(&self.notmuch),
_ => None,
}
}
fn send_message(&self) -> Option<BackendFeature<Self::Context, dyn SendMessage>> {
match self.toml_account_config.send_message_kind() {
#[cfg(feature = "smtp")]
Some(BackendKind::Smtp) => self.send_message_with_some(&self.smtp),
#[cfg(feature = "sendmail")]
Some(BackendKind::Sendmail) => self.send_message_with_some(&self.sendmail),
_ => None,
}
}
fn peek_messages(&self) -> Option<BackendFeature<Self::Context, dyn PeekMessages>> {
match self.toml_account_config.peek_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.peek_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.peek_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.peek_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.peek_messages_with_some(&self.notmuch),
_ => None,
}
}
fn get_messages(&self) -> Option<BackendFeature<Self::Context, dyn GetMessages>> {
match self.toml_account_config.get_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.get_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.get_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.get_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.get_messages_with_some(&self.notmuch),
_ => None,
}
}
fn copy_messages(&self) -> Option<BackendFeature<Self::Context, dyn CopyMessages>> {
match self.toml_account_config.copy_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.copy_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.copy_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.copy_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.copy_messages_with_some(&self.notmuch),
_ => None,
}
}
fn move_messages(&self) -> Option<BackendFeature<Self::Context, dyn MoveMessages>> {
match self.toml_account_config.move_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.move_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.move_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.move_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.move_messages_with_some(&self.notmuch),
_ => None,
}
}
fn delete_messages(&self) -> Option<BackendFeature<Self::Context, dyn DeleteMessages>> {
match self.toml_account_config.delete_messages_kind() {
#[cfg(feature = "imap")]
Some(BackendKind::Imap) => self.delete_messages_with_some(&self.imap),
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
let f = self.imap_cache.as_ref()?.delete_messages()?;
Some(Arc::new(move |ctx| f(ctx.imap_cache.as_ref()?)))
}
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => self.delete_messages_with_some(&self.maildir),
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => self.delete_messages_with_some(&self.notmuch),
_ => None,
}
}
async fn build(self) -> AnyResult<Self::Context> {
let mut ctx = BackendContext::default();
#[cfg(feature = "imap")]
if let Some(imap) = self.imap {
ctx.imap = Some(imap.build().await?);
}
#[cfg(all(feature = "imap", feature = "account-sync"))]
if let Some(maildir) = self.imap_cache {
ctx.imap_cache = Some(maildir.build().await?);
}
#[cfg(feature = "maildir")]
if let Some(maildir) = self.maildir {
ctx.maildir = Some(maildir.build().await?);
}
#[cfg(feature = "notmuch")]
if let Some(notmuch) = self.notmuch {
ctx.notmuch = Some(notmuch.build().await?);
}
#[cfg(feature = "smtp")]
if let Some(smtp) = self.smtp {
ctx.smtp = Some(smtp.build().await?);
}
#[cfg(feature = "sendmail")]
if let Some(sendmail) = self.sendmail {
ctx.sendmail = Some(sendmail.build().await?);
}
Ok(ctx)
}
}
#[derive(BackendContext, Default)]
pub struct BackendContext {
#[cfg(feature = "imap")]
pub imap: Option<ImapContextSync>,
#[cfg(all(feature = "imap", feature = "account-sync"))]
pub imap_cache: Option<MaildirContextSync>,
#[cfg(feature = "maildir")]
pub maildir: Option<MaildirContextSync>,
#[cfg(feature = "notmuch")]
pub notmuch: Option<NotmuchContextSync>,
#[cfg(feature = "smtp")]
pub smtp: Option<SmtpContextSync>,
#[cfg(feature = "sendmail")]
pub sendmail: Option<SendmailContextSync>,
}
#[cfg(feature = "imap")]
impl AsRef<Option<ImapContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<ImapContextSync> {
&self.imap
}
}
#[cfg(feature = "maildir")]
impl AsRef<Option<MaildirContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<MaildirContextSync> {
&self.maildir
}
}
#[cfg(feature = "notmuch")]
impl AsRef<Option<NotmuchContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<NotmuchContextSync> {
&self.notmuch
}
}
#[cfg(feature = "smtp")]
impl AsRef<Option<SmtpContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<SmtpContextSync> {
&self.smtp
}
}
#[cfg(feature = "sendmail")]
impl AsRef<Option<SendmailContextSync>> for BackendContext {
fn as_ref(&self) -> &Option<SendmailContextSync> {
&self.sendmail
}
}
pub struct Backend {
pub toml_account_config: Arc<TomlAccountConfig>,
pub backend: email::backend::Backend<BackendContext>,
}
impl Backend {
pub async fn new(
toml_account_config: Arc<TomlAccountConfig>,
account_config: Arc<AccountConfig>,
backend_kinds: impl IntoIterator<Item = &BackendKind>,
with_features: impl Fn(&mut email::backend::BackendBuilder<BackendContextBuilder>),
) -> Result<Self> {
let backend_kinds = backend_kinds.into_iter().collect();
let backend_ctx_builder = BackendContextBuilder::new(
toml_account_config.clone(),
account_config.clone(),
backend_kinds,
)
.await?;
let mut backend_builder =
email::backend::BackendBuilder::new(account_config.clone(), backend_ctx_builder)
.without_features();
with_features(&mut backend_builder);
Ok(Self {
toml_account_config: toml_account_config.clone(),
backend: backend_builder.build().await?,
})
}
fn build_id_mapper(
&self,
folder: &str,
backend_kind: Option<&BackendKind>,
) -> Result<IdMapper> {
#[allow(unused_mut)]
let mut id_mapper = IdMapper::Dummy;
match backend_kind {
#[cfg(feature = "maildir")]
Some(BackendKind::Maildir) => {
if let Some(_) = &self.toml_account_config.maildir {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
}
}
#[cfg(all(feature = "imap", feature = "account-sync"))]
Some(BackendKind::ImapCache) => {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
}
#[cfg(feature = "notmuch")]
Some(BackendKind::Notmuch) => {
if let Some(_) = &self.toml_account_config.notmuch {
id_mapper = IdMapper::new(&self.backend.account_config, folder)?;
}
}
_ => (),
};
Ok(id_mapper)
}
pub async fn list_envelopes(
&self,
folder: &str,
opts: ListEnvelopesOptions,
) -> Result<Envelopes> {
let backend_kind = self.toml_account_config.list_envelopes_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let envelopes = self.backend.list_envelopes(folder, opts).await?;
let envelopes =
Envelopes::from_backend(&self.backend.account_config, &id_mapper, envelopes)?;
Ok(envelopes)
}
pub async fn add_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
let backend_kind = self.toml_account_config.add_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.add_flags(folder, &ids, flags).await?;
Ok(())
}
pub async fn add_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.add_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.add_flag(folder, &ids, flag).await?;
Ok(())
}
pub async fn set_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
let backend_kind = self.toml_account_config.set_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.set_flags(folder, &ids, flags).await?;
Ok(())
}
pub async fn set_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.set_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.set_flag(folder, &ids, flag).await?;
Ok(())
}
pub async fn remove_flags(&self, folder: &str, ids: &[usize], flags: &Flags) -> Result<()> {
let backend_kind = self.toml_account_config.remove_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.remove_flags(folder, &ids, flags).await?;
Ok(())
}
pub async fn remove_flag(&self, folder: &str, ids: &[usize], flag: Flag) -> Result<()> {
let backend_kind = self.toml_account_config.remove_flags_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.remove_flag(folder, &ids, flag).await?;
Ok(())
}
pub async fn add_message(&self, folder: &str, email: &[u8]) -> Result<SingleId> {
let backend_kind = self.toml_account_config.add_message_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = self.backend.add_message(folder, email).await?;
id_mapper.create_alias(&*id)?;
Ok(id)
}
pub async fn add_message_with_flags(
&self,
folder: &str,
email: &[u8],
flags: &Flags,
) -> Result<SingleId> {
let backend_kind = self.toml_account_config.add_message_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let id = self
.backend
.add_message_with_flags(folder, email, flags)
.await?;
id_mapper.create_alias(&*id)?;
Ok(id)
}
pub async fn peek_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
let backend_kind = self.toml_account_config.get_messages_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
let msgs = self.backend.peek_messages(folder, &ids).await?;
Ok(msgs)
}
pub async fn get_messages(&self, folder: &str, ids: &[usize]) -> Result<Messages> {
let backend_kind = self.toml_account_config.get_messages_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
let msgs = self.backend.get_messages(folder, &ids).await?;
Ok(msgs)
}
pub async fn copy_messages(
&self,
from_folder: &str,
to_folder: &str,
ids: &[usize],
) -> Result<()> {
let backend_kind = self.toml_account_config.move_messages_kind();
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend
.copy_messages(from_folder, to_folder, &ids)
.await?;
Ok(())
}
pub async fn move_messages(
&self,
from_folder: &str,
to_folder: &str,
ids: &[usize],
) -> Result<()> {
let backend_kind = self.toml_account_config.move_messages_kind();
let id_mapper = self.build_id_mapper(from_folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend
.move_messages(from_folder, to_folder, &ids)
.await?;
Ok(())
}
pub async fn delete_messages(&self, folder: &str, ids: &[usize]) -> Result<()> {
let backend_kind = self.toml_account_config.delete_messages_kind();
let id_mapper = self.build_id_mapper(folder, backend_kind)?;
let ids = Id::multiple(id_mapper.get_ids(ids)?);
self.backend.delete_messages(folder, &ids).await?;
Ok(())
}
pub async fn send_message_then_save_copy(&self, msg: &[u8]) -> Result<()> {
self.backend.send_message_then_save_copy(msg).await?;
Ok(())
}
pub async fn watch_envelopes(&self, folder: &str) -> Result<()> {
self.backend.watch_envelopes(folder).await?;
Ok(())
}
}
impl Deref for Backend {
type Target = email::backend::Backend<BackendContext>;
fn deref(&self) -> &Self::Target {
&self.backend
}
}

View file

@ -1,98 +0,0 @@
use color_eyre::Result;
use dialoguer::Select;
#[cfg(feature = "account-discovery")]
use email::account::discover::config::AutoConfig;
#[cfg(feature = "imap")]
use crate::imap;
#[cfg(feature = "maildir")]
use crate::maildir;
#[cfg(feature = "notmuch")]
use crate::notmuch;
#[cfg(feature = "sendmail")]
use crate::sendmail;
#[cfg(feature = "smtp")]
use crate::smtp;
use crate::ui::THEME;
use super::{config::BackendConfig, BackendKind};
const DEFAULT_BACKEND_KINDS: &[BackendKind] = &[
#[cfg(feature = "imap")]
BackendKind::Imap,
#[cfg(feature = "maildir")]
BackendKind::Maildir,
#[cfg(feature = "notmuch")]
BackendKind::Notmuch,
];
const SEND_MESSAGE_BACKEND_KINDS: &[BackendKind] = &[
#[cfg(feature = "smtp")]
BackendKind::Smtp,
#[cfg(feature = "sendmail")]
BackendKind::Sendmail,
];
pub(crate) async fn configure(
account_name: &str,
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Default email backend")
.items(DEFAULT_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| DEFAULT_BACKEND_KINDS.get(idx).map(Clone::clone));
let config = match kind {
#[cfg(feature = "imap")]
Some(kind) if kind == BackendKind::Imap => Some(
imap::wizard::configure(
account_name,
email,
#[cfg(feature = "account-discovery")]
autoconfig,
)
.await?,
),
#[cfg(feature = "maildir")]
Some(kind) if kind == BackendKind::Maildir => Some(maildir::wizard::configure()?),
#[cfg(feature = "notmuch")]
Some(kind) if kind == BackendKind::Notmuch => Some(notmuch::wizard::configure()?),
_ => None,
};
Ok(config)
}
pub(crate) async fn configure_sender(
account_name: &str,
email: &str,
#[cfg(feature = "account-discovery")] autoconfig: Option<&AutoConfig>,
) -> Result<Option<BackendConfig>> {
let kind = Select::with_theme(&*THEME)
.with_prompt("Backend for sending messages")
.items(SEND_MESSAGE_BACKEND_KINDS)
.default(0)
.interact_opt()?
.and_then(|idx| SEND_MESSAGE_BACKEND_KINDS.get(idx).map(Clone::clone));
let config = match kind {
#[cfg(feature = "smtp")]
Some(kind) if kind == BackendKind::Smtp => Some(
smtp::wizard::configure(
account_name,
email,
#[cfg(feature = "account-discovery")]
autoconfig,
)
.await?,
),
#[cfg(feature = "sendmail")]
Some(kind) if kind == BackendKind::Sendmail => Some(sendmail::wizard::configure()?),
_ => None,
};
Ok(config)
}

View file

@ -1,15 +0,0 @@
use clap::Parser;
/// The disable cache flag parser.
#[derive(Debug, Default, Parser)]
pub struct CacheDisableFlag {
/// Disable any sort of cache.
///
/// The action depends on commands it apply on. For example, when
/// listing envelopes using the IMAP backend, this flag will
/// ensure that envelopes are fetched from the IMAP server rather
/// than the synchronized local Maildir.
#[arg(long = "disable-cache", alias = "no-cache", global = true)]
#[arg(name = "cache_disable")]
pub disable: bool,
}

View file

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

144
src/cache/mod.rs vendored
View file

@ -1,144 +0,0 @@
pub mod arg;
use color_eyre::{eyre::eyre, eyre::Context, Result};
use dirs::data_dir;
use email::account::config::AccountConfig;
use sled::{Config, Db};
use std::collections::HashSet;
use tracing::debug;
#[derive(Debug)]
pub enum IdMapper {
Dummy,
Mapper(Db),
}
impl IdMapper {
pub fn new(account_config: &AccountConfig, folder: &str) -> Result<Self> {
let digest = md5::compute(account_config.name.clone() + folder);
let db_path = data_dir()
.ok_or(eyre!("cannot get XDG data directory"))?
.join("himalaya")
.join(".id-mappers")
.join(format!("{digest:x}"));
let conn = Config::new()
.path(&db_path)
.idgen_persist_interval(1)
.open()
.with_context(|| format!("cannot open id mapper database at {db_path:?}"))?;
Ok(Self::Mapper(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(conn) => {
debug!("creating alias for id {id}…");
let alias = conn
.generate_id()
.with_context(|| format!("cannot create alias for id {id}"))?
.to_string();
debug!("created alias {alias} for id {id}");
conn.insert(&id, alias.as_bytes())
.with_context(|| format!("cannot insert 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(conn) => {
debug!("getting alias for id {id}…");
let alias = conn
.get(id)
.with_context(|| format!("cannot get alias for id {id}"))?;
let alias = match alias {
Some(alias) => {
let alias = String::from_utf8_lossy(alias.as_ref());
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: ToString,
{
let alias = alias.to_string();
match self {
Self::Dummy => Ok(alias.to_string()),
Self::Mapper(conn) => {
debug!("getting id from alias {alias}…");
let id = conn
.iter()
.flat_map(|entry| entry)
.find_map(|(entry_id, entry_alias)| {
if entry_alias.as_ref() == alias.as_bytes() {
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
Some(entry_id.to_string())
} else {
None
}
})
.ok_or_else(|| eyre!("cannot get id from alias {alias}"))?;
debug!("found id {id} from alias {alias}");
Ok(id)
}
}
}
pub fn get_ids(&self, aliases: impl IntoIterator<Item = impl ToString>) -> Result<Vec<String>> {
let aliases: Vec<String> = aliases.into_iter().map(|alias| alias.to_string()).collect();
match self {
Self::Dummy => Ok(aliases),
Self::Mapper(conn) => {
let aliases: HashSet<&str> = aliases.iter().map(|alias| alias.as_str()).collect();
let ids: Vec<String> = conn
.iter()
.flat_map(|entry| entry)
.filter_map(|(entry_id, entry_alias)| {
let alias = String::from_utf8_lossy(entry_alias.as_ref());
if aliases.contains(alias.as_ref()) {
let entry_id = String::from_utf8_lossy(entry_id.as_ref());
Some(entry_id.to_string())
} else {
None
}
})
.collect();
Ok(ids)
}
}
}
}

View file

@ -1,11 +1,22 @@
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use color_eyre::Result;
use std::path::PathBuf;
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::{self, TomlConfig},
config::TomlConfig,
envelope::command::EnvelopeSubcommand,
flag::command::FlagSubcommand,
folder::command::FolderSubcommand,
@ -14,12 +25,12 @@ use crate::{
attachment::command::AttachmentSubcommand, command::MessageSubcommand,
template::command::TemplateSubcommand,
},
output::{ColorFmt, OutputFmt},
printer::Printer,
};
#[derive(Parser, Debug)]
#[command(name = "himalaya", author, version, about)]
#[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)]
@ -33,8 +44,8 @@ pub struct Cli {
/// 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)]
#[arg(value_name = "PATH", value_parser = config::path_parser)]
#[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.
@ -52,30 +63,6 @@ pub struct Cli {
#[arg(value_name = "FORMAT", value_enum, default_value_t = Default::default())]
pub output: OutputFmt,
/// Control when to use colors
///
/// The default setting is 'auto', which means himalaya will try
/// to guess when to use colors. For example, if himalaya is
/// printing to a terminal, then it will use colors, but if it is
/// redirected to a file or a pipe, then it will suppress color
/// output. himalaya will suppress color output in some other
/// circumstances as well. For example, if the TERM environment
/// variable is not set or set to 'dumb', then himalaya will not
/// use colors.
///
/// The possible values are:
///
/// - never: colors will never be used
///
/// - always: colors will always be used regardless of where output is sent
///
/// - ansi: like 'always', but emits ANSI escapes (even in a Windows console)
///
/// - auto: himalaya tries to be smart
#[arg(long, short = 'C', global = true)]
#[arg(value_name = "MODE", value_enum, default_value_t = Default::default())]
pub color: ColorFmt,
/// Enable logs with spantrace.
///
/// This is the same as running the command with `RUST_LOG=debug`
@ -136,7 +123,7 @@ impl HimalayaCommand {
match self {
Self::Account(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;
cmd.execute(printer, &config).await
cmd.execute(printer, config, config_paths.first()).await
}
Self::Folder(cmd) => {
let config = TomlConfig::from_paths_or_default(config_paths).await?;

View file

@ -1,12 +1,13 @@
use std::io;
use clap::{value_parser, CommandFactory, Parser};
use clap_complete::Shell;
use color_eyre::Result;
use std::io;
use tracing::info;
use crate::cli::Cli;
/// Print completion script for a shell to stdout.
/// 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

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,291 +0,0 @@
pub mod wizard;
use color_eyre::{
eyre::{bail, eyre, Context},
Result,
};
use dirs::{config_dir, home_dir};
use email::{
account::config::AccountConfig, config::Config, envelope::config::EnvelopeConfig,
flag::config::FlagConfig, folder::config::FolderConfig, message::config::MessageConfig,
};
use serde::{Deserialize, Serialize};
use serde_toml_merge::merge;
use shellexpand_utils::{canonicalize, expand};
use std::{collections::HashMap, fs, path::PathBuf, sync::Arc};
use toml::{self, Value};
use tracing::debug;
#[cfg(feature = "account-sync")]
use crate::backend::BackendKind;
use crate::{account::config::TomlAccountConfig, wizard_prompt, wizard_warn};
/// Represents the user config file.
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct TomlConfig {
#[serde(alias = "name")]
pub display_name: Option<String>,
pub signature: Option<String>,
pub signature_delim: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub accounts: HashMap<String, TomlAccountConfig>,
}
impl TomlConfig {
/// Read and parse the TOML configuration at the given paths.
///
/// Returns an error if a configuration file cannot be read or if
/// a content cannot be parsed.
fn from_paths(paths: &[PathBuf]) -> Result<Self> {
match paths.len() {
0 => {
// should never happen
bail!("cannot read config file from empty paths");
}
1 => {
let path = &paths[0];
let ref content = fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?;
toml::from_str(content).context(format!("cannot parse config file at {path:?}"))
}
_ => {
let path = &paths[0];
let mut merged_content = fs::read_to_string(path)
.context(format!("cannot read config file at {path:?}"))?
.parse::<Value>()?;
for path in &paths[1..] {
match fs::read_to_string(path) {
Ok(content) => {
merged_content = merge(merged_content, content.parse()?).unwrap();
}
Err(err) => {
debug!("skipping subconfig file at {path:?}: {err}");
continue;
}
}
}
merged_content
.try_into()
.context(format!("cannot parse merged config file at {path:?}"))
}
}
}
/// Create and save a TOML configuration using the wizard.
///
/// If the user accepts the confirmation, the wizard starts and
/// help him to create his configuration file. Otherwise the
/// program stops.
///
/// NOTE: the wizard can only be used with interactive shells.
async fn from_wizard(path: &PathBuf) -> Result<Self> {
use dialoguer::Confirm;
use std::process;
wizard_warn!("Cannot find existing configuration at {path:?}.");
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to create one with the wizard?"
))
.default(true)
.interact_opt()?
.unwrap_or_default();
if !confirm {
process::exit(0);
}
wizard::configure(path).await
}
/// Read and parse the TOML configuration from default paths.
pub async fn from_default_paths() -> Result<Self> {
match Self::first_valid_default_path() {
Some(path) => Self::from_paths(&[path]),
None => Self::from_wizard(&Self::default_path()?).await,
}
}
/// Read and parse the TOML configuration at the optional given
/// path.
///
/// If the given path exists, then read and parse the TOML
/// configuration from it.
///
/// If the given path does not exist, then create it using the
/// wizard.
///
/// If no path is given, then either read and parse the TOML
/// configuration at the first valid default path, otherwise
/// create it using the wizard. wizard.
pub async fn from_paths_or_default(paths: &[PathBuf]) -> Result<Self> {
match paths.len() {
0 => Self::from_default_paths().await,
_ if paths[0].exists() => Self::from_paths(paths),
_ => Self::from_wizard(&paths[0]).await,
}
}
/// Get the default configuration path.
///
/// Returns an error if the XDG configuration directory cannot be
/// found.
pub fn default_path() -> Result<PathBuf> {
Ok(config_dir()
.ok_or(eyre!("cannot get XDG config directory"))?
.join("himalaya")
.join("config.toml"))
}
/// Get the first default configuration path that points to a
/// valid file.
///
/// 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`
pub fn first_valid_default_path() -> Option<PathBuf> {
Self::default_path()
.ok()
.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 into_toml_account_config(
&self,
account_name: Option<&str>,
) -> Result<(String, TomlAccountConfig)> {
#[allow(unused_mut)]
let (account_name, mut toml_account_config) = match account_name {
Some("default") | Some("") | None => self
.accounts
.iter()
.find_map(|(name, account)| {
account
.default
.filter(|default| *default)
.map(|_| (name.to_owned(), account.clone()))
})
.ok_or_else(|| eyre!("cannot find default account")),
Some(name) => self
.accounts
.get(name)
.map(|account| (name.to_owned(), account.clone()))
.ok_or_else(|| eyre!("cannot find account {name}")),
}?;
#[cfg(feature = "imap")]
if let Some(imap_config) = toml_account_config.imap.as_mut() {
imap_config
.auth
.replace_undefined_keyring_entries(&account_name)?;
}
#[cfg(feature = "smtp")]
if let Some(smtp_config) = toml_account_config.smtp.as_mut() {
smtp_config
.auth
.replace_undefined_keyring_entries(&account_name)?;
}
Ok((account_name, toml_account_config))
}
/// Build account configurations from a given account name.
pub fn into_account_configs(
self,
account_name: Option<&str>,
#[cfg(feature = "account-sync")] disable_cache: bool,
) -> Result<(Arc<TomlAccountConfig>, Arc<AccountConfig>)> {
#[cfg_attr(not(feature = "account-sync"), allow(unused_mut))]
let (account_name, mut toml_account_config) =
self.into_toml_account_config(account_name)?;
#[cfg(feature = "account-sync")]
if let Some(true) = toml_account_config.sync.as_ref().and_then(|c| c.enable) {
if !disable_cache {
toml_account_config.backend = match toml_account_config.backend {
Some(BackendKind::Imap) => Some(BackendKind::ImapCache),
backend => backend,
}
}
}
let config = Config {
display_name: self.display_name,
signature: self.signature,
signature_delim: self.signature_delim,
downloads_dir: self.downloads_dir,
accounts: HashMap::from_iter(self.accounts.clone().into_iter().map(
|(name, config)| {
(
name.clone(),
AccountConfig {
name,
email: config.email,
display_name: config.display_name,
signature: config.signature,
signature_delim: config.signature_delim,
downloads_dir: config.downloads_dir,
folder: config.folder.map(|c| FolderConfig {
aliases: c.alias,
list: c.list.map(|c| c.remote),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
envelope: config.envelope.map(|c| EnvelopeConfig {
list: c.list.map(|c| c.remote),
watch: c.watch.map(|c| c.remote),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
flag: config.flag.map(|c| FlagConfig {
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
message: config.message.map(|c| MessageConfig {
read: c.read.map(|c| c.remote),
write: c.write.map(|c| c.remote),
send: c.send.map(|c| c.remote),
delete: c.delete.map(Into::into),
#[cfg(feature = "account-sync")]
sync: c.sync,
}),
template: config.template,
#[cfg(feature = "account-sync")]
sync: config.sync.map(Into::into),
#[cfg(feature = "pgp")]
pgp: config.pgp,
},
)
},
)),
};
let account_config = config.account(account_name)?;
Ok((Arc::new(toml_account_config), Arc::new(account_config)))
}
}
/// Parse a configuration file path as [`PathBuf`].
///
/// The path is shell-expanded then canonicalized (if applicable).
pub fn path_parser(path: &str) -> Result<PathBuf, String> {
expand::try_path(path)
.map(canonicalize::path)
.map_err(|err| err.to_string())
}

View file

@ -1,537 +0,0 @@
use color_eyre::Result;
use dialoguer::{Confirm, Input, Select};
use shellexpand_utils::expand;
use std::{fs, path::PathBuf, process};
use toml_edit::{DocumentMut, Item};
use crate::{account, ui::THEME};
use super::TomlConfig;
#[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) async fn configure(path: &PathBuf) -> Result<TomlConfig> {
wizard_log!("Configuring your first account:");
let mut config = TomlConfig::default();
while let Some((name, account_config)) = account::wizard::configure().await? {
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 => {
wizard_warn!("No account configured, exiting.");
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(path.to_string_lossy().to_string())
.interact()?;
let path = expand::path(path);
println!("Writing the configuration to {path:?}");
let toml = pretty_serialize(&config)?;
fs::create_dir_all(path.parent().unwrap_or(&path))?;
fs::write(path, toml)?;
println!("Exiting the wizard…");
Ok(config)
}
fn pretty_serialize(config: &TomlConfig) -> Result<String> {
let mut doc: DocumentMut = toml::to_string(&config)?.parse()?;
doc.iter_mut().for_each(|(_, item)| {
if let Some(item) = item.as_table_mut() {
item.iter_mut().for_each(|(_, item)| {
set_table_dotted(item, "folder");
if let Some(item) = get_table_mut(item, "folder") {
let keys = ["alias", "add", "list", "expunge", "purge", "delete", "sync"];
set_tables_dotted(item, keys);
if let Some(item) = get_table_mut(item, "sync") {
set_tables_dotted(item, ["filter", "permissions"]);
}
}
set_table_dotted(item, "envelope");
if let Some(item) = get_table_mut(item, "envelope") {
set_tables_dotted(item, ["list", "get"]);
}
set_table_dotted(item, "flag");
if let Some(item) = get_table_mut(item, "flag") {
set_tables_dotted(item, ["add", "set", "remove"]);
}
set_table_dotted(item, "message");
if let Some(item) = get_table_mut(item, "message") {
let keys = ["add", "send", "peek", "get", "copy", "move", "delete"];
set_tables_dotted(item, keys);
}
#[cfg(feature = "maildir")]
set_table_dotted(item, "maildir");
#[cfg(feature = "imap")]
{
set_table_dotted(item, "imap");
if let Some(item) = get_table_mut(item, "imap") {
set_tables_dotted(item, ["passwd", "oauth2"]);
}
}
#[cfg(feature = "notmuch")]
set_table_dotted(item, "notmuch");
#[cfg(feature = "smtp")]
{
set_table_dotted(item, "smtp");
if let Some(item) = get_table_mut(item, "smtp") {
set_tables_dotted(item, ["passwd", "oauth2"]);
}
}
#[cfg(feature = "sendmail")]
set_table_dotted(item, "sendmail");
#[cfg(feature = "account-sync")]
set_table_dotted(item, "sync");
#[cfg(feature = "pgp")]
set_table_dotted(item, "pgp");
})
}
});
Ok(doc.to_string())
}
fn get_table_mut<'a>(item: &'a mut Item, key: &'a str) -> Option<&'a mut Item> {
item.get_mut(key).filter(|item| item.is_table())
}
fn set_table_dotted(item: &mut Item, key: &str) {
if let Some(table) = get_table_mut(item, key).and_then(|item| item.as_table_mut()) {
table.set_dotted(true)
}
}
fn set_tables_dotted<'a>(item: &'a mut Item, keys: impl IntoIterator<Item = &'a str>) {
for key in keys {
set_table_dotted(item, key)
}
}
// #[cfg(test)]
// mod test {
// use std::collections::HashMap;
// use crate::{account::config::TomlAccountConfig, config::TomlConfig};
// use super::pretty_serialize;
// fn assert_eq(config: TomlAccountConfig, expected_toml: &str) {
// let config = TomlConfig {
// accounts: HashMap::from_iter([("test".into(), config)]),
// ..Default::default()
// };
// let toml = pretty_serialize(&config).expect("serialize error");
// assert_eq!(toml, expected_toml);
// let expected_config = toml::from_str(&toml).expect("deserialize error");
// assert_eq!(config, expected_config);
// }
// #[test]
// fn pretty_serialize_default() {
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// "#,
// )
// }
// #[cfg(feature = "account-sync")]
// #[test]
// fn pretty_serialize_sync_all() {
// use email::account::sync::config::SyncConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// sync: Some(SyncConfig {
// enable: Some(false),
// dir: Some("/tmp/test".into()),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// sync.enable = false
// sync.dir = "/tmp/test"
// "#,
// );
// }
// #[cfg(feature = "account-sync")]
// #[test]
// fn pretty_serialize_sync_include() {
// use email::{
// account::sync::config::SyncConfig,
// folder::sync::config::{FolderSyncConfig, FolderSyncStrategy},
// };
// use std::collections::BTreeSet;
// use crate::folder::config::FolderConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// sync: Some(SyncConfig {
// enable: Some(true),
// dir: Some("/tmp/test".into()),
// ..Default::default()
// }),
// folder: Some(FolderConfig {
// sync: Some(FolderSyncConfig {
// filter: FolderSyncStrategy::Include(BTreeSet::from_iter(["test".into()])),
// ..Default::default()
// }),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// sync.enable = true
// sync.dir = "/tmp/test"
// folder.sync.filter.include = ["test"]
// folder.sync.permissions.create = true
// folder.sync.permissions.delete = true
// "#,
// );
// }
// #[cfg(feature = "imap")]
// #[test]
// fn pretty_serialize_imap_passwd_cmd() {
// use email::{
// account::config::passwd::PasswdConfig,
// imap::config::{ImapAuthConfig, ImapConfig},
// };
// use secret::Secret;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// imap: Some(ImapConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_command(
// "pass show test",
// ))),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// imap.host = "localhost"
// imap.port = 143
// imap.login = "test@localhost"
// imap.passwd.command = "pass show test"
// "#,
// );
// }
// #[cfg(feature = "imap")]
// #[test]
// fn pretty_serialize_imap_passwd_cmds() {
// use email::{
// account::config::passwd::PasswdConfig,
// imap::config::{ImapAuthConfig, ImapConfig},
// };
// use secret::Secret;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// imap: Some(ImapConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: ImapAuthConfig::Passwd(PasswdConfig(Secret::new_command(vec![
// "pass show test",
// "tr -d '[:blank:]'",
// ]))),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// imap.host = "localhost"
// imap.port = 143
// imap.login = "test@localhost"
// imap.passwd.command = ["pass show test", "tr -d '[:blank:]'"]
// "#,
// );
// }
// #[cfg(feature = "imap")]
// #[test]
// fn pretty_serialize_imap_oauth2() {
// use email::{
// account::config::oauth2::OAuth2Config,
// imap::config::{ImapAuthConfig, ImapConfig},
// };
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// imap: Some(ImapConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: ImapAuthConfig::OAuth2(OAuth2Config {
// client_id: "client-id".into(),
// auth_url: "auth-url".into(),
// token_url: "token-url".into(),
// ..Default::default()
// }),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// imap.host = "localhost"
// imap.port = 143
// imap.login = "test@localhost"
// imap.oauth2.method = "xoauth2"
// imap.oauth2.client-id = "client-id"
// imap.oauth2.auth-url = "auth-url"
// imap.oauth2.token-url = "token-url"
// imap.oauth2.pkce = false
// imap.oauth2.scopes = []
// "#,
// );
// }
// #[cfg(feature = "maildir")]
// #[test]
// fn pretty_serialize_maildir() {
// use email::maildir::config::MaildirConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// maildir: Some(MaildirConfig {
// root_dir: "/tmp/test".into(),
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// maildir.root-dir = "/tmp/test"
// "#,
// );
// }
// #[cfg(feature = "smtp")]
// #[test]
// fn pretty_serialize_smtp_passwd_cmd() {
// use email::{
// account::config::passwd::PasswdConfig,
// smtp::config::{SmtpAuthConfig, SmtpConfig},
// };
// use secret::Secret;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// smtp: Some(SmtpConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_command(
// "pass show test",
// ))),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// smtp.host = "localhost"
// smtp.port = 143
// smtp.login = "test@localhost"
// smtp.passwd.command = "pass show test"
// "#,
// );
// }
// #[cfg(feature = "smtp")]
// #[test]
// fn pretty_serialize_smtp_passwd_cmds() {
// use email::{
// account::config::passwd::PasswdConfig,
// smtp::config::{SmtpAuthConfig, SmtpConfig},
// };
// use secret::Secret;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// smtp: Some(SmtpConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: SmtpAuthConfig::Passwd(PasswdConfig(Secret::new_command(vec![
// "pass show test",
// "tr -d '[:blank:]'",
// ]))),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// smtp.host = "localhost"
// smtp.port = 143
// smtp.login = "test@localhost"
// smtp.passwd.command = ["pass show test", "tr -d '[:blank:]'"]
// "#,
// );
// }
// #[cfg(feature = "smtp")]
// #[test]
// fn pretty_serialize_smtp_oauth2() {
// use email::{
// account::config::oauth2::OAuth2Config,
// smtp::config::{SmtpAuthConfig, SmtpConfig},
// };
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// smtp: Some(SmtpConfig {
// host: "localhost".into(),
// port: 143,
// login: "test@localhost".into(),
// auth: SmtpAuthConfig::OAuth2(OAuth2Config {
// client_id: "client-id".into(),
// auth_url: "auth-url".into(),
// token_url: "token-url".into(),
// ..Default::default()
// }),
// ..Default::default()
// }),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// smtp.host = "localhost"
// smtp.port = 143
// smtp.login = "test@localhost"
// smtp.oauth2.method = "xoauth2"
// smtp.oauth2.client-id = "client-id"
// smtp.oauth2.auth-url = "auth-url"
// smtp.oauth2.token-url = "token-url"
// smtp.oauth2.pkce = false
// smtp.oauth2.scopes = []
// "#,
// );
// }
// #[cfg(feature = "pgp-cmds")]
// #[test]
// fn pretty_serialize_pgp_cmds() {
// use email::account::config::pgp::PgpConfig;
// assert_eq(
// TomlAccountConfig {
// email: "test@localhost".into(),
// pgp: Some(PgpConfig::Cmds(Default::default())),
// ..Default::default()
// },
// r#"[accounts.test]
// email = "test@localhost"
// pgp.backend = "cmds"
// "#,
// );
// }
// }

View file

@ -1,30 +1,29 @@
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, email::search_query,
backend::feature::BackendFeatureSource, config::Config, email::search_query,
envelope::list::ListEnvelopesOptions, search_query::SearchEmailsQuery,
};
use std::process::exit;
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, config::EnvelopesTable},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
};
/// List all envelopes.
/// Search and sort envelopes as a list.
///
/// This command allows you to list all envelopes included in the
/// given folder.
/// This command allows you to list envelopes included in the given
/// folder, matching the given query.
#[derive(Debug, Parser)]
pub struct ListEnvelopesCommand {
pub struct EnvelopeListCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
@ -41,16 +40,18 @@ pub struct ListEnvelopesCommand {
#[arg(long, short = 's', value_name = "NUMBER")]
pub page_size: Option<usize>,
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[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
@ -122,30 +123,30 @@ pub struct ListEnvelopesCommand {
pub query: Option<Vec<String>>,
}
impl Default for ListEnvelopesCommand {
impl Default for EnvelopeListCommand {
fn default() -> Self {
Self {
folder: Default::default(),
page: 1,
page_size: Default::default(),
table: Default::default(),
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
query: Default::default(),
table_max_width: Default::default(),
}
}
}
impl ListEnvelopesCommand {
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(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
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;
@ -153,14 +154,17 @@ impl ListEnvelopesCommand {
.page_size
.unwrap_or_else(|| account_config.get_envelope_list_page_size());
let list_envelopes_kind = toml_account_config.list_envelopes_kind();
let backend = Backend::new(
let backend = BackendBuilder::new(
toml_account_config.clone(),
account_config.clone(),
list_envelopes_kind,
|builder| builder.set_list_envelopes(BackendFeatureSource::Context),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let query = self
@ -196,15 +200,19 @@ impl ListEnvelopesCommand {
};
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.print_table(
Box::new(envelopes),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
},
)?;
Ok(())
printer.out(table)
}
}

View file

@ -1,14 +1,15 @@
pub mod list;
pub mod watch;
pub mod thread;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
use self::{list::EnvelopeListCommand, thread::EnvelopeThreadCommand};
/// Manage envelopes.
/// 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
@ -17,10 +18,10 @@ use self::{list::ListEnvelopesCommand, watch::WatchEnvelopesCommand};
#[derive(Debug, Subcommand)]
pub enum EnvelopeSubcommand {
#[command(alias = "lst")]
List(ListEnvelopesCommand),
List(EnvelopeListCommand),
#[command()]
Watch(WatchEnvelopesCommand),
Thread(EnvelopeThreadCommand),
}
impl EnvelopeSubcommand {
@ -28,7 +29,7 @@ impl EnvelopeSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::List(cmd) => cmd.execute(printer, config).await,
Self::Watch(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

@ -1,57 +0,0 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, printer::Printer,
};
/// Watch envelopes for changes.
///
/// This command allows you to watch a folder and execute hooks when
/// changes occur on envelopes.
#[derive(Debug, Parser)]
pub struct WatchEnvelopesCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl WatchEnvelopesCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing watch envelopes command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let watch_envelopes_kind = toml_account_config.watch_envelopes_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
watch_envelopes_kind,
|builder| builder.set_watch_envelopes(BackendFeatureSource::Context),
)
.await?;
printer.print_log(format!(
"Start watching folder {folder} for envelopes changes…"
))?;
backend.watch_envelopes(folder).await
}
}

View file

@ -1,92 +0,0 @@
#[cfg(feature = "account-sync")]
use email::envelope::sync::config::EnvelopeSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct EnvelopeConfig {
pub list: Option<ListEnvelopesConfig>,
pub watch: Option<WatchEnvelopesConfig>,
pub get: Option<GetEnvelopeConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<EnvelopeSyncConfig>,
}
impl EnvelopeConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(list) = &self.list {
kinds.extend(list.get_used_backends());
}
if let Some(watch) = &self.watch {
kinds.extend(watch.get_used_backends());
}
if let Some(get) = &self.get {
kinds.extend(get.get_used_backends());
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct ListEnvelopesConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::envelope::list::config::EnvelopeListConfig,
}
impl ListEnvelopesConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct WatchEnvelopesConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::envelope::watch::config::WatchEnvelopeConfig,
}
impl WatchEnvelopesConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct GetEnvelopeConfig {
pub backend: Option<BackendKind>,
}
impl GetEnvelopeConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,20 +1,22 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Add flag(s) to an envelope.
/// Add flag(s) to the given envelope.
///
/// This command allows you to attach the given flag(s) to the given
/// envelope(s).
@ -26,10 +28,6 @@ pub struct FlagAddCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,24 +38,27 @@ impl FlagAddCommand {
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(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_flags_kind = toml_account_config.add_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
add_flags_kind,
|builder| builder.set_add_flags(BackendFeatureSource::Context),
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.print(format!("Flag(s) {flags} successfully added!"))
printer.out(format!("Flag(s) {flags} successfully added!\n"))
}
}

View file

@ -2,19 +2,19 @@ mod add;
mod remove;
mod set;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
/// Manage flags.
/// Add, change and remove your envelopes flags.
///
/// A flag is a tag associated to an envelope. Existing flags are
/// seen, answered, flagged, deleted, draft. Other flags are
/// considered custom, which are not always supported (the
/// synchronization does not take care of them yet).
/// considered custom, which are not always supported.
#[derive(Debug, Subcommand)]
pub enum FlagSubcommand {
#[command(arg_required_else_help = true)]

View file

@ -1,20 +1,22 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Remove flag(s) from an envelope.
/// Remove flag(s) from a given envelope.
///
/// This command allows you to remove the given flag(s) from the given
/// envelope(s).
@ -26,10 +28,6 @@ pub struct FlagRemoveCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,24 +38,27 @@ impl FlagRemoveCommand {
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(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let remove_flags_kind = toml_account_config.remove_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
remove_flags_kind,
|builder| builder.set_remove_flags(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_remove_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.remove_flags(folder, &ids, &flags).await?;
printer.print(format!("Flag(s) {flags} successfully removed!"))
printer.out(format!("Flag(s) {flags} successfully removed!\n"))
}
}

View file

@ -1,20 +1,22 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
flag::arg::ids_and_flags::{into_tuple, IdsAndFlagsArgs},
folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
};
/// Replace flag(s) of an envelope.
/// Replace flag(s) of a given envelope.
///
/// This command allows you to replace existing flags of the given
/// envelope(s) with the given flag(s).
@ -26,10 +28,6 @@ pub struct FlagSetCommand {
#[command(flatten)]
pub args: IdsAndFlagsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,24 +38,27 @@ impl FlagSetCommand {
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(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let set_flags_kind = toml_account_config.set_flags_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
set_flags_kind,
|builder| builder.set_set_flags(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_set_flags(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.set_flags(folder, &ids, &flags).await?;
printer.print(format!("Flag(s) {flags} successfully replaced!"))
printer.out(format!("Flag(s) {flags} successfully replaced!\n"))
}
}

View file

@ -1,86 +0,0 @@
#[cfg(feature = "account-sync")]
use email::flag::sync::config::FlagSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FlagConfig {
pub add: Option<FlagAddConfig>,
pub set: Option<FlagSetConfig>,
pub remove: Option<FlagRemoveConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<FlagSyncConfig>,
}
impl FlagConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(add) = &self.add {
kinds.extend(add.get_used_backends());
}
if let Some(set) = &self.set {
kinds.extend(set.get_used_backends());
}
if let Some(remove) = &self.remove {
kinds.extend(remove.get_used_backends());
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FlagAddConfig {
pub backend: Option<BackendKind>,
}
impl FlagAddConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FlagSetConfig {
pub backend: Option<BackendKind>,
}
impl FlagSetConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FlagRemoveConfig {
pub backend: Option<BackendKind>,
}
impl FlagRemoveConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,48 +1,2 @@
pub mod arg;
pub mod command;
pub mod config;
use serde::Serialize;
use std::{collections::HashSet, ops};
/// Represents the flag variants.
#[derive(Clone, Debug, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize)]
pub enum Flag {
Seen,
Answered,
Flagged,
Deleted,
Draft,
Custom(String),
}
impl From<&email::flag::Flag> for Flag {
fn from(flag: &email::flag::Flag) -> Self {
use email::flag::Flag::*;
match flag {
Seen => Flag::Seen,
Answered => Flag::Answered,
Flagged => Flag::Flagged,
Deleted => Flag::Deleted,
Draft => Flag::Draft,
Custom(flag) => Flag::Custom(flag.clone()),
}
}
}
#[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<email::flag::Flags> for Flags {
fn from(flags: email::flag::Flags) -> Self {
Flags(flags.iter().map(Flag::from).collect())
}
}

View file

@ -1,128 +1,3 @@
pub mod arg;
pub mod command;
pub mod config;
pub mod flag;
use color_eyre::Result;
use email::account::config::AccountConfig;
use serde::Serialize;
use std::ops;
use crate::{
cache::IdMapper,
flag::{Flag, Flags},
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
#[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 to: 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())
}
}
/// 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: email::envelope::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(),
},
to: Mailbox {
name: envelope.to.name.clone(),
addr: envelope.to.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(())
}
}

View file

@ -1,19 +1,20 @@
use clap::Parser;
use color_eyre::{eyre::Context, Result};
use email::backend::feature::BackendFeatureSource;
use std::{fs, path::PathBuf};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{fs, path::PathBuf, sync::Arc};
use tracing::info;
use uuid::Uuid;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Download all attachments for the given message.
/// Download all attachments found in the given message.
///
/// This command allows you to download all attachments found for the
/// given message to your downloads directory.
@ -25,10 +26,6 @@ pub struct AttachmentDownloadCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,20 +37,25 @@ impl AttachmentDownloadCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let get_messages_kind = toml_account_config.get_messages_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let emails = backend.get_messages(folder, ids).await?;
@ -67,14 +69,14 @@ impl AttachmentDownloadCommand {
let attachments = email.attachments()?;
if attachments.is_empty() {
printer.print_log(format!("No attachment found for message {id}!"))?;
printer.log(format!("No attachment found for message {id}!\n"))?;
continue;
} else {
emails_count += 1;
}
printer.print_log(format!(
"{} attachment(s) found for message {id}!",
printer.log(format!(
"{} attachment(s) found for message {id}!\n",
attachments.len()
))?;
@ -84,7 +86,7 @@ impl AttachmentDownloadCommand {
.unwrap_or_else(|| Uuid::new_v4().to_string())
.into();
let filepath = account_config.get_download_file_path(&filename)?;
printer.print_log(format!("Downloading {:?}", filepath))?;
printer.log(format!("Downloading {:?}\n", filepath))?;
fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
@ -92,10 +94,10 @@ impl AttachmentDownloadCommand {
}
match attachments_count {
0 => printer.print("No attachment found!"),
1 => printer.print("Downloaded 1 attachment!"),
n => printer.print(format!(
"Downloaded {} attachment(s) from {} messages(s)!",
0 => printer.out("No attachment found!\n"),
1 => printer.out("Downloaded 1 attachment!\n"),
n => printer.out(format!(
"Downloaded {} attachment(s) from {} messages(s)!\n",
n, emails_count,
)),
}

View file

@ -1,20 +1,21 @@
mod download;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::download::AttachmentDownloadCommand;
/// Manage attachments.
/// Download your message attachments.
///
/// A message body can be composed of multiple MIME parts. An
/// attachment is the representation of a binary part of a message
/// body.
#[derive(Debug, Subcommand)]
pub enum AttachmentSubcommand {
#[command(arg_required_else_help = true)]
#[command(arg_required_else_help = true, alias = "dl")]
Download(AttachmentDownloadCommand),
}

View file

@ -1,20 +1,23 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
};
/// Copy a message from a source folder to a target folder.
/// Copy the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageCopyCommand {
#[command(flatten)]
@ -26,10 +29,6 @@ pub struct MessageCopyCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -42,26 +41,29 @@ impl MessageCopyCommand {
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let copy_messages_kind = toml_account_config.copy_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
copy_messages_kind,
|builder| builder.set_copy_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_copy_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.copy_messages(source, target, ids).await?;
printer.print(format!(
"Message(s) successfully copied from {source} to {target}!"
printer.out(format!(
"Message(s) successfully copied from {source} to {target}!\n"
))
}
}

View file

@ -1,17 +1,20 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Mark as deleted a message from a folder.
/// Mark as deleted the message associated to the given envelope id(s).
///
/// This command does not really delete the message: if the given
/// folder points to the trash folder, it adds the "deleted" flag to
@ -25,10 +28,6 @@ pub struct MessageDeleteCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,24 +39,27 @@ impl MessageDeleteCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let delete_messages_kind = toml_account_config.delete_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
delete_messages_kind,
|builder| builder.set_delete_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.delete_messages(folder, ids).await?;
printer.print(format!("Message(s) successfully removed from {folder}!"))
printer.out(format!("Message(s) successfully removed from {folder}!\n"))
}
}

View file

@ -0,0 +1,103 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Edit the message associated to the given envelope id.
///
/// This command allows you to edit the given message using the
/// editor defined in your environment variable $EDITOR. When the
/// edition process finishes, you can choose between saving or sending
/// the final message.
#[derive(Debug, Parser)]
pub struct MessageEditCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// List of headers that should be visible at the top of the
/// message.
///
/// If a given header is not found in the message, it will not be
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
pub headers: Vec<String>,
/// Edit the message on place.
///
/// If set, the original message being edited will be removed at
/// the end of the command. Useful when you need, for example, to
/// edit a draft, send it then remove it from the Drafts folder.
#[arg(long, short = 'p')]
pub on_place: bool,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageEditCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing edit message command");
let folder = &self.folder.name;
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 backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
.with_delete_messages(BackendFeatureSource::Context)
},
)
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message"))?
.to_read_tpl(&account_config, |mut tpl| {
if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
tpl
})
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
if self.on_place {
backend.delete_messages(folder, &[id]).await?;
}
Ok(())
}
}

View file

@ -0,0 +1,155 @@
use std::{
env::temp_dir,
fs,
io::{stdout, Write},
path::PathBuf,
sync::Arc,
};
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{himalaya::backend::BackendBuilder, terminal::config::TomlConfig as _};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Export the message associated to the given envelope id.
///
/// This command allows you to export a message. A message can be
/// fully exported in one single file, or exported in multiple files
/// (one per MIME part found in the message). This is useful, for
/// example, to read a HTML message.
#[derive(Debug, Parser)]
pub struct MessageExportCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Export the full raw message as one unique .eml file.
///
/// The raw message represents the headers and the body as it is
/// on the backend, unedited: not decoded nor decrypted. This is
/// useful for debugging faulty messages, but also for
/// saving/sending/transfering messages.
#[arg(long, short = 'F')]
pub full: bool,
/// Try to open the exported message, when applicable.
///
/// This argument only works with full message export, or when
/// HTML or plain text is present in the export.
#[arg(long, short = 'O')]
pub open: bool,
/// Where the message should be exported to.
///
/// The destination should point to a valid directory. If `--full`
/// is given, it can also point to a .eml file.
#[arg(long, short, alias = "dest")]
pub destination: Option<PathBuf>,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageExportCommand {
pub async fn execute(self, config: &TomlConfig) -> Result<()> {
info!("executing export message command");
let folder = &self.folder.name;
let id = &self.envelope.id;
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 backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let msgs = backend.get_messages(folder, &[*id]).await?;
let msg = msgs.first().ok_or(eyre!("cannot find message {id}"))?;
if self.full {
let bytes = msg.raw()?;
match self.destination {
Some(mut dest) if dest.is_dir() => {
dest.push(format!("{id}.eml"));
fs::write(&dest, bytes)?;
let dest = dest.display();
println!("Message {id} successfully exported at {dest}!");
}
Some(dest) => {
fs::write(&dest, bytes)?;
let dest = dest.display();
println!("Message {id} successfully exported at {dest}!");
}
None => {
stdout().write_all(bytes)?;
}
};
} else {
let dest = match self.destination {
Some(dest) if dest.is_dir() => {
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
Some(dest) if dest.is_file() => {
let dest = dest.parent().unwrap_or(&dest);
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
Some(dest) => {
return Err(eyre!("Destination {} does not exist!", dest.display()));
}
None => {
let dest = temp_dir();
let dest = msg.download_parts(&dest)?;
let d = dest.display();
println!("Message {id} successfully exported in {d}!");
dest
}
};
if self.open {
let index_html = dest.join("index.html");
if index_html.exists() {
return Ok(open::that(index_html)?);
}
let plain_txt = dest.join("plain.txt");
if plain_txt.exists() {
return Ok(open::that(plain_txt)?);
}
println!("--open was passed but nothing to open, ignoring");
}
}
Ok(())
}
}

View file

@ -1,22 +1,23 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Forward a message.
/// Forward the message associated to the given envelope id.
///
/// This command allows you to forward the given message using the
/// editor defined in your environment variable $EDITOR. When the
@ -36,10 +37,6 @@ pub struct MessageForwardCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -50,24 +47,25 @@ impl MessageForwardCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let id = self.envelope.id;

View file

@ -1,18 +1,18 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use mail_builder::MessageBuilder;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use url::Url;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig, printer::Printer,
ui::editor,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// Parse and edit a message from a mailto URL string.
/// Parse and edit a message from the given mailto URL string.
///
/// This command allows you to edit a message from the mailto format
/// using the editor defined in your environment variable
@ -24,10 +24,6 @@ pub struct MessageMailtoCommand {
#[arg()]
pub url: Url,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -36,8 +32,6 @@ impl MessageMailtoCommand {
pub fn new(url: &str) -> Result<Self> {
Ok(Self {
url: Url::parse(url)?,
#[cfg(feature = "account-sync")]
cache: Default::default(),
account: Default::default(),
})
}
@ -45,60 +39,57 @@ impl MessageMailtoCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing mailto message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let mut builder = MessageBuilder::new().to(self.url.path());
let mut body = String::new();
let mut msg = Vec::<u8>::new();
let mut body = Vec::<u8>::new();
msg.extend(b"Content-Type: text/plain; charset=utf-8\r\n");
for (key, val) in self.url.query_pairs() {
match key {
key if key.eq_ignore_ascii_case("in-reply-to") => {
builder = builder.in_reply_to(val.to_string());
}
key if key.eq_ignore_ascii_case("cc") => {
builder = builder.cc(val.to_string());
}
key if key.eq_ignore_ascii_case("bcc") => {
builder = builder.bcc(val.to_string());
}
key if key.eq_ignore_ascii_case("subject") => {
builder = builder.subject(val.to_string());
}
key if key.eq_ignore_ascii_case("body") => {
body += &val;
}
_ => (),
if key.eq_ignore_ascii_case("body") {
body.extend(val.as_bytes());
} else {
msg.extend(key.as_bytes());
msg.extend(b": ");
msg.extend(val.as_bytes());
msg.extend(b"\r\n");
}
}
match account_config.find_full_signature() {
Some(ref sig) => builder = builder.text_body(body + "\n\n" + sig),
None => builder = builder.text_body(body),
msg.extend(b"\r\n");
msg.extend(body);
if let Some(sig) = account_config.find_full_signature() {
msg.extend(b"\r\n");
msg.extend(sig.as_bytes());
}
let tpl = account_config
.generate_tpl_interpreter()
.with_show_only_headers(account_config.get_message_write_headers())
.build()
.from_msg_builder(builder)
.from_bytes(msg)
.await?
.into();

View file

@ -1,5 +1,7 @@
pub mod copy;
pub mod delete;
pub mod edit;
pub mod export;
pub mod forward;
pub mod mailto;
pub mod r#move;
@ -7,21 +9,24 @@ pub mod read;
pub mod reply;
pub mod save;
pub mod send;
pub mod thread;
pub mod write;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::{
copy::MessageCopyCommand, delete::MessageDeleteCommand, forward::MessageForwardCommand,
mailto::MessageMailtoCommand, r#move::MessageMoveCommand, read::MessageReadCommand,
reply::MessageReplyCommand, save::MessageSaveCommand, send::MessageSendCommand,
copy::MessageCopyCommand, delete::MessageDeleteCommand, edit::MessageEditCommand,
export::MessageExportCommand, forward::MessageForwardCommand, mailto::MessageMailtoCommand,
r#move::MessageMoveCommand, read::MessageReadCommand, reply::MessageReplyCommand,
save::MessageSaveCommand, send::MessageSendCommand, thread::MessageThreadCommand,
write::MessageWriteCommand,
};
/// Manage messages.
/// Read, write, send, copy, move and delete your messages.
///
/// A message is the content of an email. It is composed of headers
/// (located at the top of the message) and a body (located at the
@ -32,16 +37,22 @@ pub enum MessageSubcommand {
#[command(arg_required_else_help = true)]
Read(MessageReadCommand),
#[command(arg_required_else_help = true)]
Export(MessageExportCommand),
#[command(arg_required_else_help = true)]
Thread(MessageThreadCommand),
#[command(aliases = ["add", "create", "new", "compose"])]
Write(MessageWriteCommand),
#[command()]
Reply(MessageReplyCommand),
#[command(aliases = ["fwd", "fd"])]
Forward(MessageForwardCommand),
#[command()]
Edit(MessageEditCommand),
Mailto(MessageMailtoCommand),
Save(MessageSaveCommand),
@ -66,9 +77,12 @@ impl MessageSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Read(cmd) => cmd.execute(printer, config).await,
Self::Export(cmd) => cmd.execute(config).await,
Self::Thread(cmd) => cmd.execute(printer, config).await,
Self::Write(cmd) => cmd.execute(printer, config).await,
Self::Reply(cmd) => cmd.execute(printer, config).await,
Self::Forward(cmd) => cmd.execute(printer, config).await,
Self::Edit(cmd) => cmd.execute(printer, config).await,
Self::Mailto(cmd) => cmd.execute(printer, config).await,
Self::Save(cmd) => cmd.execute(printer, config).await,
Self::Send(cmd) => cmd.execute(printer, config).await,

View file

@ -1,21 +1,24 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
printer::Printer,
};
/// Move a message from a source folder to a target folder.
/// Move the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageMoveCommand {
#[command(flatten)]
@ -27,10 +30,6 @@ pub struct MessageMoveCommand {
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -43,26 +42,29 @@ impl MessageMoveCommand {
let target = &self.target_folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let move_messages_kind = toml_account_config.move_messages_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
move_messages_kind,
|builder| builder.set_move_messages(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_move_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.move_messages(source, target, ids).await?;
printer.print(format!(
"Message(s) successfully moved from {source} to {target}!"
printer.out(format!(
"Message(s) successfully moved from {source} to {target}!\n"
))
}
}

View file

@ -1,23 +1,26 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use mml::message::FilterParts;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Read a message.
/// Read a human-friendly version of the message associated to the
/// given envelope id(s).
///
/// This command allows you to read a message. When reading a message,
/// the "seen" flag is automatically applied to the corresponding
/// envelope. To prevent this behaviour, use the --preview flag.
/// envelope. To prevent this behaviour, use the "--preview" flag.
#[derive(Debug, Parser)]
pub struct MessageReadCommand {
#[command(flatten)]
@ -31,31 +34,10 @@ pub struct MessageReadCommand {
#[arg(long, short)]
pub preview: bool,
/// Read the raw version of the given message.
///
/// The raw message represents the headers and the body as it is
/// on the backend, unedited: not decoded nor decrypted. This is
/// useful for debugging faulty messages, but also for
/// saving/sending/transfering messages.
#[arg(long, short)]
#[arg(conflicts_with = "no_headers")]
#[arg(conflicts_with = "headers")]
pub raw: bool,
/// Read only body of text/html parts.
///
/// This argument is useful when you need to read the HTML version
/// of a message. Combined with --no-headers, you can write it to
/// a .html file and open it with your favourite browser.
#[arg(long)]
#[arg(conflicts_with = "raw")]
pub html: bool,
/// Read only the body of the message.
///
/// All headers will be removed from the message.
#[arg(long)]
#[arg(conflicts_with = "raw")]
#[arg(conflicts_with = "headers")]
pub no_headers: bool,
@ -66,14 +48,9 @@ pub struct MessageReadCommand {
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
#[arg(conflicts_with = "raw")]
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -85,20 +62,26 @@ impl MessageReadCommand {
let folder = &self.folder.name;
let ids = &self.envelopes.ids;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let get_messages_kind = toml_account_config.get_messages_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
.with_peek_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let emails = if self.preview {
@ -113,32 +96,22 @@ impl MessageReadCommand {
for email in emails.to_vec() {
bodies.push_str(glue);
if self.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()?));
} else {
let tpl = email
.to_read_tpl(&account_config, |mut tpl| {
if self.no_headers {
tpl = tpl.with_hide_all_headers();
} else if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
let tpl = email
.to_read_tpl(&account_config, |mut tpl| {
if self.no_headers {
tpl = tpl.with_hide_all_headers();
} else if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
if self.html {
tpl = tpl.with_filter_parts(FilterParts::Only("text/html".into()));
}
tpl
})
.await?;
bodies.push_str(&tpl);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
printer.print(bodies)
printer.out(bodies)
}
}

View file

@ -1,22 +1,23 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config, flag::Flag};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
printer::Printer,
ui::editor,
};
/// Reply to a message.
/// Reply to the message associated to the given envelope id.
///
/// This command allows you to reply to the given message using the
/// editor defined in your environment variable $EDITOR. When the
@ -39,10 +40,6 @@ pub struct MessageReplyCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -52,24 +49,25 @@ impl MessageReplyCommand {
info!("executing reply message command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let id = self.envelope.id;
@ -84,6 +82,11 @@ impl MessageReplyCommand {
.with_reply_all(self.reply.all)
.build()
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await?;
backend.add_flag(folder, &[id], Flag::Answered).await?;
Ok(())
}
}

View file

@ -1,18 +1,22 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig,
folder::arg::name::FolderNameOptionalFlag, message::arg::MessageRawArg,
};
/// Save a message to a folder.
/// Save the given raw message to the given folder.
///
/// This command allows you to add a raw message to the given folder.
#[derive(Debug, Parser)]
@ -23,10 +27,6 @@ pub struct MessageSaveCommand {
#[command(flatten)]
pub message: MessageRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -37,20 +37,23 @@ impl MessageSaveCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
add_message_kind,
|builder| builder.set_add_message(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
@ -68,6 +71,6 @@ impl MessageSaveCommand {
backend.add_message(folder, msg.as_bytes()).await?;
printer.print(format!("Message successfully saved to {folder}!"))
printer.out(format!("Message successfully saved to {folder}!\n"))
}
}

View file

@ -1,17 +1,19 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use std::io::{self, BufRead, IsTerminal};
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
message::arg::MessageRawArg, printer::Printer,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig, message::arg::MessageRawArg};
/// Send a message.
/// Send the given raw message.
///
/// This command allows you to send a raw message and to save a copy
/// to your send folder.
@ -20,10 +22,6 @@ pub struct MessageSendCommand {
#[command(flatten)]
pub message: MessageRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -32,27 +30,23 @@ impl MessageSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config
.add_message_kind()
.filter(|_| account_config.should_save_copy_sent_message()),
);
let backend = Backend::new(
toml_account_config.clone(),
account_config,
send_message_kind,
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder.set_send_message(BackendFeatureSource::Context);
builder.set_add_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let msg = if io::stdin().is_terminal() {
@ -68,6 +62,6 @@ impl MessageSendCommand {
backend.send_message_then_save_copy(msg.as_bytes()).await?;
printer.print("Message successfully sent!")
printer.out("Message successfully sent!")
}
}

View file

@ -0,0 +1,130 @@
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::envelope::arg::ids::EnvelopeIdArg;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// Read human-friendly version of messages associated to the
/// given envelope id's thread.
///
/// This command allows you to thread a message. When threading a message,
/// the "seen" flag is automatically applied to the corresponding
/// envelope. To prevent this behaviour, use the --preview flag.
#[derive(Debug, Parser)]
pub struct MessageThreadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
/// Thread the message without applying the "seen" flag to its
/// corresponding envelope.
#[arg(long, short)]
pub preview: bool,
/// Thread only the body of the message.
///
/// All headers will be removed from the message.
#[arg(long)]
#[arg(conflicts_with = "headers")]
pub no_headers: bool,
/// List of headers that should be visible at the top of the
/// message.
///
/// If a given header is not found in the message, it will not be
/// visible. If no header is given, defaults to the one set up in
/// your TOML configuration file.
#[arg(long = "header", short = 'H', value_name = "NAME")]
#[arg(conflicts_with = "no_headers")]
pub headers: Vec<String>,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageThreadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing thread message(s) command");
let folder = &self.folder.name;
let id = &self.envelope.id;
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 backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
.with_peek_messages(BackendFeatureSource::Context)
.with_thread_envelopes(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let envelopes = backend
.thread_envelope(folder, *id, Default::default())
.await?;
let ids: Vec<_> = envelopes
.graph()
.nodes()
.map(|e| e.id.parse::<usize>().unwrap())
.collect();
let emails = if self.preview {
backend.peek_messages(folder, &ids).await
} else {
backend.get_messages(folder, &ids).await
}?;
let mut glue = "";
let mut bodies = String::default();
for (i, email) in emails.to_vec().iter().enumerate() {
bodies.push_str(glue);
bodies.push_str(&format!("-------- Message {} --------\n\n", ids[i + 1]));
let tpl = email
.to_read_tpl(&account_config, |mut tpl| {
if self.no_headers {
tpl = tpl.with_hide_all_headers();
} else if !self.headers.is_empty() {
tpl = tpl.with_show_only_headers(&self.headers);
}
tpl
})
.await?;
bodies.push_str(&tpl);
glue = "\n\n";
}
printer.out(bodies)
}
}

View file

@ -1,20 +1,24 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, message::Message};
use email::{
config::Config,
{backend::feature::BackendFeatureSource, message::Message},
};
use pimalaya_tui::{
himalaya::{backend::BackendBuilder, editor},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
ui::editor,
};
/// Write a new message.
/// Compose a new message, from scratch.
///
/// This command allows you to write a new message using the editor
/// defined in your environment variable $EDITOR. When the edition
@ -28,10 +32,6 @@ pub struct MessageWriteCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -40,24 +40,25 @@ impl MessageWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write message command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let send_message_kind = toml_account_config.send_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind.into_iter().chain(send_message_kind),
|builder| {
builder.set_add_message(BackendFeatureSource::Context);
builder.set_send_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let tpl = Message::new_tpl_builder(account_config.clone())

View file

@ -1,189 +0,0 @@
use email::message::delete::config::DeleteMessageStyle;
#[cfg(feature = "account-sync")]
use email::message::sync::config::MessageSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageConfig {
pub write: Option<MessageAddConfig>,
pub send: Option<MessageSendConfig>,
pub peek: Option<MessagePeekConfig>,
pub read: Option<MessageGetConfig>,
pub copy: Option<MessageCopyConfig>,
pub r#move: Option<MessageMoveConfig>,
pub delete: Option<DeleteMessageConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<MessageSyncConfig>,
}
impl MessageConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(add) = &self.write {
kinds.extend(add.get_used_backends());
}
if let Some(send) = &self.send {
kinds.extend(send.get_used_backends());
}
if let Some(peek) = &self.peek {
kinds.extend(peek.get_used_backends());
}
if let Some(get) = &self.read {
kinds.extend(get.get_used_backends());
}
if let Some(copy) = &self.copy {
kinds.extend(copy.get_used_backends());
}
if let Some(move_) = &self.r#move {
kinds.extend(move_.get_used_backends());
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageAddConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::message::add::config::MessageWriteConfig,
}
impl MessageAddConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageSendConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::message::send::config::MessageSendConfig,
}
impl MessageSendConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessagePeekConfig {
pub backend: Option<BackendKind>,
}
impl MessagePeekConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageGetConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::message::get::config::MessageReadConfig,
}
impl MessageGetConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageCopyConfig {
pub backend: Option<BackendKind>,
}
impl MessageCopyConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct MessageMoveConfig {
pub backend: Option<BackendKind>,
}
impl MessageMoveConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct DeleteMessageConfig {
pub backend: Option<BackendKind>,
pub style: Option<DeleteMessageStyle>,
}
impl From<DeleteMessageConfig> for email::message::delete::config::DeleteMessageConfig {
fn from(config: DeleteMessageConfig) -> Self {
Self {
style: config.style,
}
}
}
impl DeleteMessageConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,5 +1,4 @@
pub mod arg;
pub mod attachment;
pub mod command;
pub mod config;
pub mod template;

View file

@ -1,18 +1,20 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
printer::Printer,
};
/// Generate a template for forwarding a message.
@ -34,10 +36,6 @@ pub struct TemplateForwardCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -48,20 +46,25 @@ impl TemplateForwardCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let get_messages_kind = toml_account_config.get_messages_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let id = self.envelope.id;
@ -76,6 +79,6 @@ impl TemplateForwardCommand {
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -4,25 +4,25 @@ mod save;
mod send;
mod write;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::{
forward::TemplateForwardCommand, reply::TemplateReplyCommand, save::TemplateSaveCommand,
send::TemplateSendCommand, write::TemplateWriteCommand,
};
/// Manage templates.
/// Generate, save and send message templates.
///
/// A template is an editable version of a message (headers +
/// body). It uses a specific language called MML that allows you to
/// attach file or encrypt content. This subcommand allows you manage
/// them.
///
/// You can learn more about MML at
/// <https://crates.io/crates/mml-lib>.
/// Learn more about MML at: <https://crates.io/crates/mml-lib>.
#[derive(Debug, Subcommand)]
pub enum TemplateSubcommand {
#[command(aliases = ["add", "create", "new", "compose"])]

View file

@ -1,18 +1,20 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::{eyre::eyre, Result};
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdArg,
folder::arg::name::FolderNameOptionalFlag,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs, reply::MessageReplyAllArg},
printer::Printer,
};
/// Generate a template for replying to a message.
@ -38,10 +40,6 @@ pub struct TemplateReplyCommand {
#[command(flatten)]
pub body: MessageRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -53,20 +51,25 @@ impl TemplateReplyCommand {
let folder = &self.folder.name;
let id = self.envelope.id;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let get_messages_kind = toml_account_config.get_messages_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
get_messages_kind,
|builder| builder.set_get_messages(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_get_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let tpl = backend
@ -81,6 +84,6 @@ impl TemplateReplyCommand {
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -1,16 +1,20 @@
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
email::template::arg::TemplateRawArg, folder::arg::name::FolderNameOptionalFlag,
printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
folder::arg::name::FolderNameOptionalFlag,
};
/// Save a template to a folder.
@ -27,10 +31,6 @@ pub struct TemplateSaveCommand {
#[command(flatten)]
pub template: TemplateRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -41,20 +41,25 @@ impl TemplateSaveCommand {
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let add_message_kind = toml_account_config.add_message_kind();
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
add_message_kind,
|builder| builder.set_add_message(BackendFeatureSource::Context),
|builder| {
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let is_tty = io::stdin().is_terminal();
@ -73,13 +78,13 @@ impl TemplateSaveCommand {
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(feature = "pgp")]
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
compiler.set_some_pgp(account_config.pgp.clone());
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
backend.add_message(folder, &msg).await?;
printer.print(format!("Template successfully saved to {folder}!"))
printer.out(format!("Template successfully saved to {folder}!\n"))
}
}

View file

@ -1,15 +1,20 @@
use std::{
io::{self, BufRead, IsTerminal},
sync::Arc,
};
use clap::Parser;
use color_eyre::Result;
use email::backend::feature::BackendFeatureSource;
use email::{backend::feature::BackendFeatureSource, config::Config};
use mml::MmlCompilerBuilder;
use std::io::{self, BufRead, IsTerminal};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
email::template::arg::TemplateRawArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, email::template::arg::TemplateRawArg,
};
/// Send a template.
@ -23,10 +28,6 @@ pub struct TemplateSendCommand {
#[command(flatten)]
pub template: TemplateRawArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -35,27 +36,25 @@ impl TemplateSendCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing send template command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let send_message_kind = toml_account_config.send_message_kind().into_iter().chain(
toml_account_config
.add_message_kind()
.filter(|_| account_config.should_save_copy_sent_message()),
);
let account_config = Arc::new(account_config);
let backend = Backend::new(
toml_account_config.clone(),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
account_config.clone(),
send_message_kind,
|builder| {
builder.set_send_message(BackendFeatureSource::Context);
builder.set_add_message(BackendFeatureSource::Context);
builder
.without_features()
.with_add_message(BackendFeatureSource::Context)
.with_send_message(BackendFeatureSource::Context)
},
)
.build()
.await?;
let tpl = if io::stdin().is_terminal() {
@ -72,13 +71,13 @@ impl TemplateSendCommand {
#[allow(unused_mut)]
let mut compiler = MmlCompilerBuilder::new();
#[cfg(feature = "pgp")]
#[cfg(any(feature = "pgp-gpg", feature = "pgp-commands", feature = "pgp-native"))]
compiler.set_some_pgp(account_config.pgp.clone());
let msg = compiler.build(tpl.as_str())?.compile().await?.into_vec()?;
backend.send_message_then_save_copy(&msg).await?;
printer.print("Message successfully sent!")
printer.out("Message successfully sent!")
}
}

View file

@ -1,14 +1,14 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::message::Message;
use email::{config::Config, message::Message};
use pimalaya_tui::terminal::{cli::printer::Printer, config::TomlConfig as _};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig,
email::template::arg::body::TemplateRawBodyArg, message::arg::header::HeaderRawArgs,
printer::Printer,
};
/// Generate a template for writing a new message from scratch.
@ -23,10 +23,6 @@ pub struct TemplateWriteCommand {
#[command(flatten)]
pub body: TemplateRawBodyArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -35,18 +31,18 @@ impl TemplateWriteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing write template command");
let (_, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (_, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let tpl = Message::new_tpl_builder(account_config)
let tpl = Message::new_tpl_builder(Arc::new(account_config))
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
printer.print(tpl)
printer.out(tpl)
}
}

View file

@ -1,14 +1,2 @@
pub mod arg;
pub mod command;
use color_eyre::Result;
use email::template::Template;
use crate::printer::{Print, WriteColor};
impl Print for Template {
fn print(&self, writer: &mut dyn WriteColor) -> Result<()> {
self.as_str().print(writer)?;
Ok(writer.reset()?)
}
}

View file

@ -1,55 +1,61 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::add::AddFolder};
use email::{
config::Config,
{backend::feature::BackendFeatureSource, folder::add::AddFolder},
};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Create a new folder.
/// Create the given folder.
///
/// This command allows you to create a new folder using the given
/// name.
#[derive(Debug, Parser)]
pub struct AddFolderCommand {
pub struct FolderAddCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl AddFolderCommand {
impl FolderAddCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing create folder command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let add_folder_kind = toml_account_config.add_folder_kind();
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 = Backend::new(
toml_account_config.clone(),
account_config,
add_folder_kind,
|builder| builder.set_add_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_add_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.add_folder(folder).await?;
printer.print(format!("Folder {folder} successfully created!"))
printer.out(format!("Folder {folder} successfully created!\n"))
}
}

View file

@ -1,18 +1,22 @@
use std::{process, sync::Arc};
use clap::Parser;
use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder};
use std::process;
use email::{
config::Config,
{backend::feature::BackendFeatureSource, folder::delete::DeleteFolder},
};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Delete a folder.
/// Delete the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// folder is also deleted after execution of the command.
@ -21,12 +25,11 @@ pub struct FolderDeleteCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
#[arg(long, short)]
pub yes: bool,
}
impl FolderDeleteCommand {
@ -35,34 +38,36 @@ impl FolderDeleteCommand {
let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to delete the folder {folder}? All emails will be definitely deleted.");
let confirm = Confirm::new()
.with_prompt(confirm_msg)
.default(false)
.report(false)
.interact_opt()?;
if let Some(false) | None = confirm {
process::exit(0);
};
if !self.yes {
let confirm = format!("Do you really want to delete the folder {folder}");
let confirm = format!("{confirm}? All emails will be definitely deleted.");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
if !prompt::bool(confirm, false)? {
process::exit(0);
};
}
let delete_folder_kind = toml_account_config.delete_folder_kind();
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 = Backend::new(
toml_account_config.clone(),
account_config,
delete_folder_kind,
|builder| builder.set_delete_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.delete_folder(folder).await?;
printer.print(format!("Folder {folder} successfully deleted!"))
printer.out(format!("Folder {folder} successfully deleted!\n"))
}
}

View file

@ -1,16 +1,21 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::expunge::ExpungeFolder};
use email::{
backend::feature::BackendFeatureSource, config::Config, folder::expunge::ExpungeFolder,
};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Expunge a folder.
/// Expunge the given folder.
///
/// The concept of expunging is similar to the IMAP one: it definitely
/// deletes emails from the given folder that contain the "deleted"
@ -20,10 +25,6 @@ pub struct FolderExpungeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
}
@ -33,24 +34,27 @@ impl FolderExpungeCommand {
info!("executing expunge folder command");
let folder = &self.folder.name;
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let expunge_folder_kind = toml_account_config.expunge_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
expunge_folder_kind,
|builder| builder.set_expunge_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_expunge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.expunge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully expunged!"))
printer.out(format!("Folder {folder} successfully expunged!\n"))
}
}

View file

@ -1,63 +1,73 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, folder::list::ListFolders};
use email::{
config::Config,
{backend::feature::BackendFeatureSource, folder::list::ListFolders},
};
use pimalaya_tui::{
himalaya::{
backend::BackendBuilder,
config::{Folders, FoldersTable},
},
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag,
backend::Backend,
config::TomlConfig,
folder::Folders,
printer::{PrintTableOpts, Printer},
ui::arg::max_width::TableMaxWidthFlag,
};
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// List all folders.
///
/// This command allows you to list all exsting folders.
#[derive(Debug, Parser)]
pub struct FolderListCommand {
#[command(flatten)]
pub table: TableMaxWidthFlag,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[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>,
}
impl FolderListCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing list folders command");
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let list_folders_kind = toml_account_config.list_folders_kind();
let toml_account_config = Arc::new(toml_account_config);
let backend = Backend::new(
let backend = BackendBuilder::new(
toml_account_config.clone(),
account_config.clone(),
list_folders_kind,
|builder| builder.set_list_folders(BackendFeatureSource::Context),
)
.await?;
let folders: Folders = backend.list_folders().await?.into();
printer.print_table(
Box::new(folders),
PrintTableOpts {
format: &account_config.get_message_read_format(),
max_width: self.table.max_width,
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_list_folders(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
let folders = Folders::from(backend.list_folders().await?);
let table = FoldersTable::from(folders)
.with_some_width(self.table_max_width)
.with_some_preset(toml_account_config.folder_list_table_preset())
.with_some_name_color(toml_account_config.folder_list_table_name_color())
.with_some_desc_color(toml_account_config.folder_list_table_desc_color());
printer.out(table)?;
Ok(())
}
}

View file

@ -4,24 +4,25 @@ mod expunge;
mod list;
mod purge;
use color_eyre::Result;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::{config::TomlConfig, printer::Printer};
use crate::config::TomlConfig;
use self::{
add::AddFolderCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
add::FolderAddCommand, delete::FolderDeleteCommand, expunge::FolderExpungeCommand,
list::FolderListCommand, purge::FolderPurgeCommand,
};
/// Manage folders.
/// Create, list and purge your folders (as known as mailboxes).
///
/// A folder (as known as mailbox, or directory) contains one or more
/// emails. This subcommand allows you to manage them.
/// A folder (as known as mailbox, or directory) is a messages
/// container. This subcommand allows you to manage them.
#[derive(Debug, Subcommand)]
pub enum FolderSubcommand {
#[command(visible_alias = "create", alias = "new")]
Add(AddFolderCommand),
Add(FolderAddCommand),
#[command(alias = "lst")]
List(FolderListCommand),

View file

@ -1,18 +1,19 @@
use std::{process, sync::Arc};
use clap::Parser;
use color_eyre::Result;
use dialoguer::Confirm;
use email::{backend::feature::BackendFeatureSource, folder::purge::PurgeFolder};
use std::process;
use email::{backend::feature::BackendFeatureSource, config::Config, folder::purge::PurgeFolder};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _, prompt},
};
use tracing::info;
#[cfg(feature = "account-sync")]
use crate::cache::arg::disable::CacheDisableFlag;
use crate::{
account::arg::name::AccountNameFlag, backend::Backend, config::TomlConfig,
folder::arg::name::FolderNameArg, printer::Printer,
account::arg::name::AccountNameFlag, config::TomlConfig, folder::arg::name::FolderNameArg,
};
/// Purge a folder.
/// Purge the given folder.
///
/// All emails from the given folder are definitely deleted. The
/// purged folder will remain empty after execution of the command.
@ -21,12 +22,11 @@ pub struct FolderPurgeCommand {
#[command(flatten)]
pub folder: FolderNameArg,
#[cfg(feature = "account-sync")]
#[command(flatten)]
pub cache: CacheDisableFlag,
#[command(flatten)]
pub account: AccountNameFlag,
#[arg(long, short)]
pub yes: bool,
}
impl FolderPurgeCommand {
@ -35,34 +35,36 @@ impl FolderPurgeCommand {
let folder = &self.folder.name;
let confirm_msg = format!("Do you really want to purge the folder {folder}? All emails will be definitely deleted.");
let confirm = Confirm::new()
.with_prompt(confirm_msg)
.default(false)
.report(false)
.interact_opt()?;
if let Some(false) | None = confirm {
process::exit(0);
if !self.yes {
let confirm = format!("Do you really want to purge the folder {folder}");
let confirm = format!("{confirm}? All emails will be definitely deleted.");
if !prompt::bool(confirm, false)? {
process::exit(0);
};
};
let (toml_account_config, account_config) = config.clone().into_account_configs(
self.account.name.as_deref(),
#[cfg(feature = "account-sync")]
self.cache.disable,
)?;
let (toml_account_config, account_config) = config
.clone()
.into_account_configs(self.account.name.as_deref(), |c: &Config, name| {
c.account(name).ok()
})?;
let purge_folder_kind = toml_account_config.purge_folder_kind();
let backend = Backend::new(
toml_account_config.clone(),
account_config,
purge_folder_kind,
|builder| builder.set_purge_folder(BackendFeatureSource::Context),
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_purge_folder(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.purge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully purged!"))
printer.out(format!("Folder {folder} successfully purged!\n"))
}
}

View file

@ -1,135 +0,0 @@
#[cfg(feature = "account-sync")]
use email::folder::sync::config::FolderSyncConfig;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use crate::backend::BackendKind;
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderConfig {
#[serde(alias = "aliases")]
pub alias: Option<HashMap<String, String>>,
pub add: Option<FolderAddConfig>,
pub list: Option<FolderListConfig>,
pub expunge: Option<FolderExpungeConfig>,
pub purge: Option<FolderPurgeConfig>,
pub delete: Option<FolderDeleteConfig>,
#[cfg(feature = "account-sync")]
pub sync: Option<FolderSyncConfig>,
}
impl FolderConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(add) = &self.add {
kinds.extend(add.get_used_backends());
}
if let Some(list) = &self.list {
kinds.extend(list.get_used_backends());
}
if let Some(expunge) = &self.expunge {
kinds.extend(expunge.get_used_backends());
}
if let Some(purge) = &self.purge {
kinds.extend(purge.get_used_backends());
}
if let Some(delete) = &self.delete {
kinds.extend(delete.get_used_backends());
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderAddConfig {
pub backend: Option<BackendKind>,
}
impl FolderAddConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderListConfig {
pub backend: Option<BackendKind>,
#[serde(flatten)]
pub remote: email::folder::list::config::FolderListConfig,
}
impl FolderListConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderExpungeConfig {
pub backend: Option<BackendKind>,
}
impl FolderExpungeConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderPurgeConfig {
pub backend: Option<BackendKind>,
}
impl FolderPurgeConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
pub struct FolderDeleteConfig {
pub backend: Option<BackendKind>,
}
impl FolderDeleteConfig {
pub fn get_used_backends(&self) -> HashSet<&BackendKind> {
let mut kinds = HashSet::default();
if let Some(kind) = &self.backend {
kinds.insert(kind);
}
kinds
}
}

View file

@ -1,67 +1,2 @@
pub mod arg;
pub mod command;
pub mod config;
use color_eyre::Result;
use serde::Serialize;
use std::ops;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::{Cell, Row, Table},
};
#[derive(Clone, Debug, Default, Serialize)]
pub struct Folder {
pub name: String,
pub desc: String,
}
impl From<&email::folder::Folder> for Folder {
fn from(folder: &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())
}
}
#[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<email::folder::Folders> for Folders {
fn from(folders: 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 +0,0 @@
pub(crate) mod wizard;

View file

@ -1,545 +0,0 @@
use color_eyre::Result;
use dialoguer::{Confirm, Input, Password, Select};
#[cfg(feature = "account-discovery")]
use email::account::discover::config::{AuthenticationType, AutoConfig, SecurityType, ServerType};
use email::{
account::config::{
oauth2::{OAuth2Config, OAuth2Method, OAuth2Scopes},
passwd::PasswdConfig,
},
imap::config::{ImapAuthConfig, ImapConfig, ImapEncryptionKind},
};
use oauth::v2_0::{AuthorizationCodeGrant, Client};
use secret::Secret;
use crate::{
backend::config::BackendConfig,
ui::{prompt, THEME},
wizard_log, wizard_prompt,
};
const ENCRYPTIONS: &[ImapEncryptionKind] = &[
ImapEncryptionKind::Tls,
ImapEncryptionKind::StartTls,
ImapEncryptionKind::None,
];
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";
#[cfg(feature = "account-discovery")]
pub(crate) async fn configure(
account_name: &str,
email: &str,
autoconfig: Option<&AutoConfig>,
) -> Result<BackendConfig> {
let autoconfig_oauth2 = autoconfig.and_then(|c| c.oauth2());
let autoconfig_server = autoconfig.and_then(|c| {
c.email_provider()
.incoming_servers()
.into_iter()
.find(|server| matches!(server.server_type(), ServerType::Imap))
});
let autoconfig_host = autoconfig_server
.and_then(|s| s.hostname())
.map(ToOwned::to_owned);
let default_host =
autoconfig_host.unwrap_or_else(|| format!("imap.{}", email.rsplit_once('@').unwrap().1));
let host = Input::with_theme(&*THEME)
.with_prompt("IMAP hostname")
.default(default_host)
.interact()?;
let autoconfig_encryption = autoconfig_server
.and_then(|imap| {
imap.security_type().map(|encryption| match encryption {
SecurityType::Plain => ImapEncryptionKind::None,
SecurityType::Starttls => ImapEncryptionKind::StartTls,
SecurityType::Tls => ImapEncryptionKind::Tls,
})
})
.unwrap_or_default();
let default_encryption_idx = match &autoconfig_encryption {
ImapEncryptionKind::Tls => 0,
ImapEncryptionKind::StartTls => 1,
ImapEncryptionKind::None => 2,
};
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP encryption")
.items(ENCRYPTIONS)
.default(default_encryption_idx)
.interact_opt()?;
let autoconfig_port = autoconfig_server
.and_then(|s| s.port())
.map(ToOwned::to_owned)
.unwrap_or_else(|| match &autoconfig_encryption {
ImapEncryptionKind::Tls => 993,
ImapEncryptionKind::StartTls => 143,
ImapEncryptionKind::None => 143,
});
let (encryption, default_port) = match encryption_idx {
Some(idx) if idx == default_encryption_idx => {
(Some(autoconfig_encryption), autoconfig_port)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
(Some(ImapEncryptionKind::Tls), 993)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
_ => (Some(ImapEncryptionKind::None), 143),
};
let 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())?;
let autoconfig_login = autoconfig_server.map(|imap| match imap.username() {
Some("%EMAILLOCALPART%") => email.rsplit_once('@').unwrap().0.to_owned(),
Some("%EMAILADDRESS%") => email.to_owned(),
_ => email.to_owned(),
});
let default_login = autoconfig_login.unwrap_or_else(|| email.to_owned());
let login = Input::with_theme(&*THEME)
.with_prompt("IMAP login")
.default(default_login)
.interact()?;
let default_oauth2_enabled = autoconfig_server
.and_then(|imap| {
imap.authentication_type()
.into_iter()
.find_map(|t| Option::from(matches!(t, AuthenticationType::OAuth2)))
})
.filter(|_| autoconfig_oauth2.is_some())
.unwrap_or_default();
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(default_oauth2_enabled)
.interact_opt()?
.unwrap_or_default();
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
config.method = match method_idx {
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()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
let default_auth_url = autoconfig_oauth2
.map(|o| o.auth_url().to_owned())
.unwrap_or_default();
config.auth_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 authorization URL")
.default(default_auth_url)
.interact()?;
let default_token_url = autoconfig_oauth2
.map(|o| o.token_url().to_owned())
.unwrap_or_default();
config.token_url = Input::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 token URL")
.default(default_token_url)
.interact()?;
let autoconfig_scopes = autoconfig_oauth2.map(|o| o.scope());
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(match &autoconfig_scopes {
Some(scopes) => Select::with_theme(&*THEME)
.with_prompt(prompt)
.items(scopes)
.default(0)
.interact_opt()?
.and_then(|idx| scopes.get(idx))
.map(|scope| scope.to_string()),
None => Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()),
})
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more IMAP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
}
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(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(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}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-imap-passwd"))
.interact()?,
),
_ => Default::default(),
};
ImapAuthConfig::Passwd(PasswdConfig(secret))
};
let config = ImapConfig {
host,
port,
encryption,
login,
auth,
watch: None,
};
Ok(BackendConfig::Imap(config))
}
#[cfg(not(feature = "account-discovery"))]
pub(crate) async fn configure(account_name: &str, email: &str) -> Result<BackendConfig> {
let default_host = format!("imap.{}", email.rsplit_once('@').unwrap().1);
let host = Input::with_theme(&*THEME)
.with_prompt("IMAP hostname")
.default(default_host)
.interact()?;
let encryption_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP encryption")
.items(ENCRYPTIONS)
.default(0)
.interact_opt()?;
let (encryption, default_port) = match encryption_idx {
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::Tls => {
(Some(ImapEncryptionKind::Tls), 993)
}
Some(idx) if ENCRYPTIONS[idx] == ImapEncryptionKind::StartTls => {
(Some(ImapEncryptionKind::StartTls), 143)
}
_ => (Some(ImapEncryptionKind::None), 143),
};
let 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())?;
let default_login = email.to_owned();
let login = Input::with_theme(&*THEME)
.with_prompt("IMAP login")
.default(default_login)
.interact()?;
let oauth2_enabled = Confirm::new()
.with_prompt(wizard_prompt!("Would you like to enable OAuth 2.0?"))
.default(false)
.interact_opt()?
.unwrap_or_default();
let auth = if oauth2_enabled {
let mut config = OAuth2Config::default();
let redirect_host = OAuth2Config::LOCALHOST.to_owned();
let redirect_port = OAuth2Config::get_first_available_port()?;
let method_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP OAuth 2.0 mechanism")
.items(OAUTH2_MECHANISMS)
.default(0)
.interact_opt()?;
config.method = match method_idx {
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()?;
config.client_secret =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-client-secret"))?;
config
.client_secret
.set_only_keyring(&client_secret)
.await?;
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()?;
let prompt_scope = |prompt: &str| -> Result<Option<String>> {
Ok(Some(
Input::with_theme(&*THEME)
.with_prompt(prompt)
.default(String::default())
.interact()?
.to_owned(),
)
.filter(|scope| !scope.is_empty()))
};
if let Some(scope) = prompt_scope("IMAP OAuth 2.0 main scope")? {
config.scopes = OAuth2Scopes::Scope(scope);
}
let confirm_additional_scope = || -> Result<bool> {
let confirm = Confirm::new()
.with_prompt(wizard_prompt!(
"Would you like to add more IMAP OAuth 2.0 scopes?"
))
.default(false)
.interact_opt()?
.unwrap_or_default();
Ok(confirm)
};
while confirm_additional_scope()? {
let mut scopes = match config.scopes {
OAuth2Scopes::Scope(scope) => vec![scope],
OAuth2Scopes::Scopes(scopes) => scopes,
};
if let Some(scope) = prompt_scope("Additional IMAP OAuth 2.0 scope")? {
scopes.push(scope)
}
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(redirect_host.to_owned())
.with_redirect_port(redirect_port)
.build()?;
let mut auth_code_grant = AuthorizationCodeGrant::new()
.with_redirect_host(redirect_host.to_owned())
.with_redirect_port(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}");
println!();
let (access_token, refresh_token) = auth_code_grant
.wait_for_redirection(&client, csrf_token)
.await?;
config.access_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-access-token"))?;
config.access_token.set_only_keyring(access_token).await?;
if let Some(refresh_token) = &refresh_token {
config.refresh_token =
Secret::try_new_keyring_entry(format!("{account_name}-imap-oauth2-refresh-token"))?;
config.refresh_token.set_only_keyring(refresh_token).await?;
}
ImapAuthConfig::OAuth2(config)
} else {
let secret_idx = Select::with_theme(&*THEME)
.with_prompt("IMAP authentication strategy")
.items(SECRETS)
.default(0)
.interact_opt()?;
let secret = match secret_idx {
Some(idx) if SECRETS[idx] == KEYRING => {
let secret = Secret::try_new_keyring_entry(format!("{account_name}-imap-passwd"))?;
secret
.set_only_keyring(prompt::passwd("IMAP password")?)
.await?;
secret
}
Some(idx) if SECRETS[idx] == RAW => Secret::new_raw(prompt::passwd("IMAP password")?),
Some(idx) if SECRETS[idx] == CMD => Secret::new_command(
Input::with_theme(&*THEME)
.with_prompt("Shell command")
.default(format!("pass show {account_name}-imap-passwd"))
.interact()?,
),
_ => Default::default(),
};
ImapAuthConfig::Passwd(PasswdConfig(secret))
};
let config = ImapConfig {
host,
port,
encryption,
login,
auth,
watch: None,
};
Ok(BackendConfig::Imap(config))
}

View file

@ -1,26 +1,10 @@
pub mod account;
pub mod backend;
pub mod cache;
pub mod cli;
pub mod completion;
pub mod config;
pub mod email;
pub mod folder;
#[cfg(feature = "imap")]
pub mod imap;
#[cfg(feature = "maildir")]
pub mod maildir;
pub mod manual;
#[cfg(feature = "notmuch")]
pub mod notmuch;
pub mod output;
pub mod printer;
#[cfg(feature = "sendmail")]
pub mod sendmail;
#[cfg(feature = "smtp")]
pub mod smtp;
pub mod tracing;
pub mod ui;
#[doc(inline)]
pub use crate::email::{envelope, flag, message};

View file

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

View file

@ -1,23 +0,0 @@
use color_eyre::Result;
use dialoguer::Input;
use dirs::home_dir;
use email::maildir::config::MaildirConfig;
use crate::{backend::config::BackendConfig, ui::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,27 +1,20 @@
use clap::Parser;
use color_eyre::{Result, Section};
use color_eyre::Result;
use himalaya::{
cli::Cli, config::TomlConfig, envelope::command::list::ListEnvelopesCommand,
message::command::mailto::MessageMailtoCommand, printer::StdoutPrinter,
cli::Cli, config::TomlConfig, envelope::command::list::EnvelopeListCommand,
message::command::mailto::MessageMailtoCommand,
};
use pimalaya_tui::terminal::{
cli::{printer::StdoutPrinter, tracing},
config::TomlConfig as _,
};
use std::env;
use tracing::level_filters::LevelFilter;
#[tokio::main]
async fn main() -> Result<()> {
if env::var("RUST_LOG").is_err() {
if std::env::args().any(|arg| arg == "--debug") {
env::set_var("RUST_LOG", "debug");
}
if std::env::args().any(|arg| arg == "--trace") {
env::set_var("RUST_LOG", "trace");
}
}
let cli = Cli::parse();
let tracing = tracing::install()?;
let filter = himalaya::tracing::install()?;
let mut printer = StdoutPrinter::new(cli.output, cli.color);
#[cfg(feature = "keyring")]
secret::keyring::set_global_service_name("himalaya-cli");
// if the first argument starts by "mailto:", execute straight the
// mailto message command
@ -38,23 +31,17 @@ async fn main() -> Result<()> {
.await;
}
let mut res = match cli.command {
let cli = Cli::parse();
let mut printer = StdoutPrinter::new(cli.output);
let res = match cli.command {
Some(cmd) => cmd.execute(&mut printer, cli.config_paths.as_ref()).await,
None => {
let config = TomlConfig::from_paths_or_default(cli.config_paths.as_ref()).await?;
ListEnvelopesCommand::default()
EnvelopeListCommand::default()
.execute(&mut printer, &config)
.await
}
};
if filter < LevelFilter::DEBUG {
res = res.note("Run with --debug to enable logs with spantrace.");
};
if filter < LevelFilter::TRACE {
res = res.note("Run with --trace to enable verbose logs with backtrace.")
};
res
tracing.with_debug_and_trace_notes(res)
}

View file

@ -1,13 +1,15 @@
use std::{fs, path::PathBuf};
use clap::{CommandFactory, Parser};
use clap_mangen::Man;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use shellexpand_utils::{canonicalize, expand};
use std::{fs, path::PathBuf};
use tracing::info;
use crate::{cli::Cli, printer::Printer};
use crate::cli::Cli;
/// Generate manual pages to a directory.
/// Generate manual pages to the given directory.
///
/// This command allows you to generate manual pages (following the
/// man page format) to the given directory. If the directory does not
@ -33,7 +35,7 @@ impl ManualGenerateCommand {
Man::new(cmd).render(&mut buffer)?;
fs::create_dir_all(&self.dir)?;
printer.print_log(format!("Generating man page for command {cmd_name}"))?;
printer.log(format!("Generating man page for command {cmd_name}\n"))?;
fs::write(self.dir.join(format!("{}.1", cmd_name)), buffer)?;
for subcmd in subcmds {
@ -42,16 +44,18 @@ impl ManualGenerateCommand {
let mut buffer = Vec::new();
Man::new(subcmd).render(&mut buffer)?;
printer.print_log(format!("Generating man page for subcommand {subcmd_name}"))?;
printer.log(format!(
"Generating man page for subcommand {subcmd_name}…\n"
))?;
fs::write(
self.dir.join(format!("{}-{}.1", cmd_name, subcmd_name)),
buffer,
)?;
}
printer.print(format!(
"{subcmds_len} man page(s) successfully generated in {:?}!",
self.dir
printer.log(format!(
"{subcmds_len} man page(s) successfully generated in {}!\n",
self.dir.display()
))?;
Ok(())

View file

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

View file

@ -1,24 +0,0 @@
use color_eyre::Result;
use dialoguer::Input;
use email::notmuch::config::NotmuchConfig;
use crate::{backend::config::BackendConfig, ui::THEME};
pub(crate) fn configure() -> Result<BackendConfig> {
let mut config = NotmuchConfig::default();
let default_database_path = NotmuchConfig::get_default_database_path()
.unwrap_or_default()
.to_string_lossy()
.to_string();
config.database_path = Some(
Input::with_theme(&*THEME)
.with_prompt("Notmuch database path")
.default(default_database_path)
.interact_text()?
.into(),
);
Ok(BackendConfig::Notmuch(config))
}

View file

@ -1,48 +0,0 @@
//! Module related to output CLI.
//!
//! This module provides arguments related to output.
use clap::Arg;
pub(crate) const ARG_COLOR: &str = "color";
pub(crate) const ARG_OUTPUT: &str = "output";
/// Output arguments.
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[
Arg::new(ARG_OUTPUT)
.help("Define the output format")
.long("output")
.short('o')
.global(true)
.value_name("format")
.value_parser(["plain", "json"])
.default_value("plain"),
Arg::new(ARG_COLOR)
.help("Control when to use colors")
.long_help(
"Control when to use colors.
The default setting is 'auto', which means himalaya will try to guess
when to use colors. For example, if himalaya is printing to a
terminal, then it will use colors, but if it is redirected to a file
or a pipe, then it will suppress color output. himalaya will suppress
color output in some other circumstances as well. For example, if the
TERM environment variable is not set or set to 'dumb', then himalaya
will not use colors.
The possible values for this flag are:
never Colors will never be used.
auto The default. himalaya tries to be smart.
always Colors will always be used regardless of where output is sent.
ansi Like 'always', but emits ANSI escapes (even in a Windows console).",
)
.long("color")
.short('C')
.global(true)
.value_parser(["never", "auto", "always", "ansi"])
.default_value("auto")
.value_name("mode"),
]
}

View file

@ -1,5 +0,0 @@
pub mod args;
#[allow(clippy::module_inception)]
pub mod output;
pub use output::*;

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