Compare commits

...

554 commits

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

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

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

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

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

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

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

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

    do not remove share folder

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

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

    clean

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

    replace buildInputs by NIX_LDFLAGS

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

    remove NIX_LDFLAGS

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

    use framework 11.0

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

    revert flake lock

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

    clean aarch64 nix conf

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

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

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

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

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

This commit will completely replace dialoguer with inquire.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

### Fixed

- Fixed broken links in README.md.

### Removed

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

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

update himalaya-lib, rename remaining mbox vars

add missing methods from lib

update changelog

* fixed missing folder aliases #430

* improve README links

* fix README repology link

* fix README repology table

* fix README repology table 2

* center README repology table

* fix README cosmetic issues

* fix README cosmetic issues 2

* fix README title

* fix README wiki links

* fix lock file

* prepare v0.6.2

* fix ci

* try some musl builds #356

* add musl build to artifact #356

* add musl build to deployment pipeline #356

* migrate clap v4, add man command #419

* add option to choose color manually #407

* update links and badges

* update matrix badge

* add github release version badge

* update badges links

* fix code bloc type

* fix tests

* fix cargo lock

* generate all man pages for all subcommands #419

* fix query and headers arg parsers

* fix invalid flags and options due to clap v4 migration

* fix tests

* remove -l|--log-level option

* refactor contributing guide

* update lib

* fix flags string printer

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

* fix ids arg parser

* fix flags subcommands conflicts between ids and flags

* flip back copy and move arguments

* add issue template (#439)

* update lib, prepare for sync feature

* update himalaya lib, fix senders and config

* update lock file himalaya lib

* fix sync enabling issues

* fix wrong imap backend init in main file

* fix notmuch backend post sync feature

* configuration wizard (#432)

* make DeserializedConfig::path more robust

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

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

* add wizard entrypoint and basic structure

* wip

* feat: impl Serialize for all DeserializedConfigs

* feat: select default account and write to file

* feat: add SMTP part of wizard

* build: update lockfile

* refactor: separate out multiple files for wizard

* style: friendlier and prettier messages

* feat: add maildir part of wizard

* feat: add notmuch part of wizard

* chore: clippy lints and reorder prompts

* fix: contrived solution to serializing None values

* fix: allow empty Option field when deserializing

* style: address PR review comments

* fix: utilize notmuch lib in finding database path

* fix notmuch wizard

---------

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

* add account sync progress bar

* improve sync spinner

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

* update himalaya lib, increase imap session pool size

* add disable cache flag

* add nlnet logo in readme

* update himalaya lib deps, make use of sync reports

* prepare v0.7.0

* bump rustc v1.67.0 and clap v4.1.4

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

---------

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

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

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

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

### Fixed

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

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

### Added

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

### Changed

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

### Fixed

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

Co-authored-by: Clément DOUIN <clement.douin@posteo.net>
2022-10-11 14:50:25 +02:00
143 changed files with 10877 additions and 5967 deletions

5
.github/FUNDING.yml vendored
View file

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

View file

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

View file

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

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

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

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

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

View file

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

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
# Cargo config directory
.cargo/
# Cargo build directory
target/
debug/

File diff suppressed because it is too large Load diff

View file

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

5138
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,62 +1,70 @@
[package]
name = "himalaya"
description = "Command-line interface for email management."
version = "0.6.0"
description = "CLI to manage emails"
version = "1.1.0"
authors = ["soywod <clement.douin@posteo.net>"]
edition = "2021"
license = "MIT"
categories = ["command-line-interface", "command-line-utilities", "email"]
keywords = ["cli", "mail", "email", "client", "imap"]
homepage = "https://github.com/soywod/himalaya"
documentation = "https://github.com/soywod/himalaya/wiki"
repository = "https://github.com/soywod/himalaya"
categories = ["command-line-utilities", "email"]
keywords = ["cli", "email", "imap", "maildir", "smtp"]
homepage = "https://pimalaya.org/"
documentation = "https://github.com/pimalaya/himalaya/"
repository = "https://github.com/pimalaya/himalaya/"
[package.metadata.deb]
priority = "optional"
section = "mail"
[package.metadata.docs.rs]
features = ["imap", "maildir", "smtp", "sendmail", "oauth2", "wizard", "pgp-commands", "pgp-native"]
rustdoc-args = ["--cfg", "docsrs"]
[features]
imap-backend = ["imap", "imap-proto"]
maildir-backend = ["maildir", "md5"]
notmuch-backend = ["himalaya-lib/notmuch-backend", "maildir-backend", "notmuch"]
default = ["imap-backend", "maildir-backend"]
default = ["imap", "maildir", "smtp", "sendmail", "wizard", "pgp-commands"]
imap = ["email-lib/imap", "pimalaya-tui/imap"]
maildir = ["email-lib/maildir", "pimalaya-tui/maildir"]
notmuch = ["email-lib/notmuch", "pimalaya-tui/notmuch"]
smtp = ["email-lib/smtp", "pimalaya-tui/smtp"]
sendmail = ["email-lib/sendmail", "pimalaya-tui/sendmail"]
keyring = ["email-lib/keyring", "pimalaya-tui/keyring", "secret-lib/keyring"]
oauth2 = ["email-lib/oauth2", "pimalaya-tui/oauth2", "keyring"]
wizard = ["email-lib/autoconfig", "pimalaya-tui/wizard"]
pgp-commands = ["email-lib/pgp-commands", "mml-lib/pgp-commands", "pimalaya-tui/pgp-commands"]
pgp-gpg = ["email-lib/pgp-gpg", "mml-lib/pgp-gpg", "pimalaya-tui/pgp-gpg"]
pgp-native = ["email-lib/pgp-native", "mml-lib/pgp-native", "pimalaya-tui/pgp-native"]
[build-dependencies]
pimalaya-tui = { version = "0.2", default-features = false, features = ["build-envs"] }
[dev-dependencies]
tempfile = "3.3.0"
himalaya = { path = ".", features = ["notmuch", "keyring", "oauth2", "pgp-gpg", "pgp-native"] }
[dependencies]
ammonia = "3.1.2"
anyhow = "1.0.44"
atty = "0.2.14"
chrono = "0.4.19"
clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] }
convert_case = "0.5.0"
env_logger = "0.8.3"
erased-serde = "0.3.18"
# himalaya-lib = { version = "=0.3.1", git = "https://git.sr.ht/~soywod/himalaya-lib", branch = "develop" }
himalaya-lib = "=0.3.1"
html-escape = "0.2.9"
lettre = { version = "=0.10.0-rc.7", features = ["serde"] }
log = "0.4.14"
mailparse = "0.13.6"
native-tls = "0.2.8"
regex = "1.5.4"
rfc2047-decoder = "0.1.2"
serde = { version = "1.0.118", features = ["derive"] }
serde_json = "1.0.61"
shellexpand = "2.1.0"
termcolor = "1.1"
terminal_size = "0.1.15"
toml = "0.5.8"
tree_magic = "0.2.3"
unicode-width = "0.1.7"
url = "2.2.2"
ariadne = "0.2"
clap = { version = "4.4", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.4"
clap_mangen = "0.2"
color-eyre = "0.6"
email-lib = { version = "0.26", default-features = false, features = ["tokio-rustls", "derive", "thread"] }
mml-lib = { version = "1", default-features = false, features = ["compiler", "interpreter", "derive"] }
once_cell = "1.16"
open = "5.3"
pimalaya-tui = { version = "0.2", default-features = false, features = ["rustls", "email", "path", "cli", "himalaya", "tracing", "sled"] }
secret-lib = { version = "1", default-features = false, features = ["tokio", "rustls", "command", "derive"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
shellexpand-utils = "=0.2.1"
tokio = { version = "1.23", default-features = false, features = ["macros", "rt-multi-thread"] }
toml = "0.8"
tracing = "0.1"
url = "2.2"
uuid = { version = "0.8", features = ["v4"] }
# Optional dependencies:
[patch.crates-io]
imap-codec.git = "https://github.com/duesee/imap-codec"
imap = { version = "=3.0.0-alpha.4", optional = true }
imap-proto = { version = "0.14.3", optional = true }
maildir = { version = "0.6.1", optional = true }
md5 = { version = "0.7.0", optional = true }
notmuch = { version = "0.7.1", optional = true }
email-lib.git = "https://github.com/pimalaya/core"
imap-client.git = "https://github.com/pimalaya/imap-client"
keyring-lib.git = "https://github.com/pimalaya/core"
mml-lib.git = "https://github.com/pimalaya/core"
oauth-lib.git = "https://github.com/pimalaya/core"
pgp-lib.git = "https://github.com/pimalaya/core"
pimalaya-tui.git = "https://github.com/pimalaya/tui"
process-lib.git = "https://github.com/pimalaya/core"
secret-lib.git = "https://github.com/pimalaya/core"

View file

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

764
README.md
View file

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

View file

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

7
build.rs Normal file
View file

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

651
config.sample.toml Normal file
View file

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

View file

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

143
flake.lock generated
View file

@ -1,138 +1,79 @@
{
"nodes": {
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1650374568,
"narHash": "sha256-Z+s0J8/r907g149rllvwhb4pKi8Wam5ij0st8PwAh+E=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "b4a34015c698c7793d592d66adbab377907a2be8",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1656928814,
"narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"fenix": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": [
"nixpkgs"
],
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1662220400,
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
"owner": "nix-community",
"repo": "naersk",
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
"lastModified": 1732405626,
"narHash": "sha256-uDbQrdOyqa2679kKPzoztMxesOV7DG2+FuX/TZdpxD0=",
"owner": "soywod",
"repo": "fenix",
"rev": "c7af381484169a78fb79a11652321ae80b0f92a6",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"owner": "soywod",
"repo": "fenix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1664356419,
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=",
"owner": "NixOS",
"lastModified": 1736437047,
"narHash": "sha256-JJBziecfU+56SUNxeJlDIgixJN5WYuADd+/TVd5sQos=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1",
"rev": "f17b95775191ea44bc426831235d87affb10faba",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
"owner": "nixos",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"pimalaya": {
"flake": false,
"locked": {
"lastModified": 1664356419,
"narHash": "sha256-PD0hM9YWp2lepAJk7edh8g1VtzJip5rals1fpoQUlY0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "46e8398474ac3b1b7bb198bf9097fc213bbf59b1",
"lastModified": 1737984647,
"narHash": "sha256-qcxytsdCS4HfyXGpqIIudvLKPCTZBBAPEAPxY1dinbU=",
"owner": "pimalaya",
"repo": "nix",
"rev": "712a481632f4929d24a34cb5762e0ffdc901bd99",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1659102345,
"narHash": "sha256-Vbzlz254EMZvn28BhpN8JOi5EuKqnHZ3ujFYgFcSGvk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "11b60e4f80d87794a2a4a8a256391b37c59a1ea7",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"owner": "pimalaya",
"repo": "nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"utils": "utils"
"fenix": "fenix",
"nixpkgs": "nixpkgs",
"pimalaya": "pimalaya"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3"
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1664334084,
"narHash": "sha256-cqP0TzDs3GDRprS6IgVQcWjQ0ynmjQFjYWvp+LE/s6I=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "70eab96a255ae9b4b82b38ea5ac5c8e5b57e0abd",
"lastModified": 1732050317,
"narHash": "sha256-G5LUEOC4kvB/Xbkglv0Noi04HnCfryur7dVjzlHkgpI=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "c0bbbb3e5d7d1d1d60308c8270bfd5b250032bb4",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "rust-lang",
"ref": "nightly",
"repo": "rust-analyzer",
"type": "github"
}
}

View file

@ -1,65 +1,26 @@
{
description = "Command-line interface for email management.";
description = "CLI to manage emails";
inputs = {
utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nix-community/naersk";
flake-compat = {
url = "github:edolstra/flake-compat";
# FIXME: when #358989 lands on nixos-unstable
# https://nixpk.gs/pr-tracker.html?pr=358989
nixpkgs.url = "github:nixos/nixpkgs/staging-next";
fenix = {
# TODO: https://github.com/nix-community/fenix/pull/145
# url = "github:nix-community/fenix";
url = "github:soywod/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
pimalaya = {
url = "github:pimalaya/nix";
flake = false;
};
};
outputs = { self, nixpkgs, utils, rust-overlay, naersk, ... }:
utils.lib.eachDefaultSystem
(system:
let
name = "himalaya";
overlays = [ (import rust-overlay) ];
pkgs = import nixpkgs { inherit system overlays; };
in
rec {
# nix build
defaultPackage = packages.${name};
packages = {
${name} = naersk.lib.${system}.buildPackage {
pname = name;
root = ./.;
nativeBuildInputs = with pkgs; [ openssl.dev pkg-config ];
overrideMain = _: {
postInstall = ''
mkdir -p $out/share/applications/
cp assets/himalaya.desktop $out/share/applications/
'';
};
};
};
# nix run
defaultApp = apps.${name};
apps.${name} = utils.lib.mkApp {
inherit name;
drv = packages.${name};
};
# nix develop
devShell = pkgs.mkShell {
inputsFrom = builtins.attrValues self.packages.${system};
nativeBuildInputs = with pkgs; [
# Nix LSP + formatter
rnix-lsp
nixpkgs-fmt
# Rust env
(rust-bin.fromRustupToolchainFile ./rust-toolchain.toml)
cargo-watch
rust-analyzer
# Notmuch
notmuch
];
};
}
);
outputs =
inputs:
(import inputs.pimalaya).mkFlakeOutputs inputs {
shell = ./shell.nix;
default = ./default.nix;
};
}

View file

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

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,2 +1,4 @@
[toolchain]
channel = "stable"
channel = "1.82.0"
profile = "default"
components = ["rust-src", "rust-analyzer"]

BIN
screenshot.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

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

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

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

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

156
src/cli.rs Normal file
View file

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

View file

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

View file

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

View file

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

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

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

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

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

3
src/config.rs Normal file
View file

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

View file

@ -1,20 +0,0 @@
//! This module provides arguments related to the user config.
use clap::{Arg, ArgMatches};
const ARG_CONFIG: &str = "config";
/// Represents the config file path argument. This argument allows the
/// user to customize the config file path.
pub fn arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_CONFIG)
.long("config")
.short("c")
.help("Forces a specific config file path")
.value_name("PATH")
}
/// Represents the config file path argument parser.
pub fn parse_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<&'a str> {
matches.value_of(ARG_CONFIG)
}

View file

@ -1,572 +0,0 @@
//! Deserialized config module.
//!
//! This module contains the raw deserialized representation of the
//! user configuration file.
use anyhow::{anyhow, Context, Result};
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
use log::{debug, trace};
use serde::Deserialize;
use std::{collections::HashMap, env, fs, path::PathBuf};
use toml;
use crate::{account::DeserializedAccountConfig, config::prelude::*};
/// Represents the user config file.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedConfig {
#[serde(alias = "name")]
pub display_name: Option<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(default, with = "email_text_plain_format")]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_reading_decrypt_cmd: Option<String>,
pub email_writing_encrypt_cmd: Option<String>,
#[serde(default, with = "email_hooks")]
pub email_hooks: Option<EmailHooks>,
#[serde(flatten)]
pub accounts: HashMap<String, DeserializedAccountConfig>,
}
impl DeserializedConfig {
/// Tries to create a config from an optional path.
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
trace!(">> parse config from path");
debug!("path: {:?}", path);
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
let content = fs::read_to_string(path).context("cannot read config file")?;
let config: Self = toml::from_str(&content).context("cannot parse config file")?;
if config.accounts.is_empty() {
return Err(anyhow!("config file must contain at least one account"));
}
trace!("config: {:?}", config);
trace!("<< parse config from path");
Ok(config)
}
/// Tries to get the XDG config file path from XDG_CONFIG_HOME
/// environment variable.
fn path_from_xdg() -> Result<PathBuf> {
let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?;
let path = PathBuf::from(path).join("himalaya").join("config.toml");
Ok(path)
}
/// Tries to get the XDG config file path from HOME environment
/// variable.
fn path_from_xdg_alt() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
let path = PathBuf::from(path)
.join(".config")
.join("himalaya")
.join("config.toml");
Ok(path)
}
/// Tries to get the .himalayarc config file path from HOME
/// environment variable.
fn path_from_home() -> Result<PathBuf> {
let home_var = if cfg!(target_family = "windows") {
"USERPROFILE"
} else {
"HOME"
};
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
let path = PathBuf::from(path).join(".himalayarc");
Ok(path)
}
/// Tries to get the config file path.
pub fn path() -> Result<PathBuf> {
Self::path_from_xdg()
.or_else(|_| Self::path_from_xdg_alt())
.or_else(|_| Self::path_from_home())
}
pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> {
let (account_config, backend_config) = match account_name {
Some("default") | Some("") | None => self
.accounts
.iter()
.find_map(|(_, account)| {
if account.is_default() {
Some(account)
} else {
None
}
})
.ok_or_else(|| anyhow!("cannot find default account")),
Some(name) => self
.accounts
.get(name)
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
}?
.to_configs(self);
Ok((account_config, backend_config))
}
}
#[cfg(test)]
mod tests {
use himalaya_lib::{EmailSender, SendmailConfig, SmtpConfig};
#[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig;
#[cfg(feature = "maildir-backend")]
use himalaya_lib::MaildirConfig;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig;
use std::io::Write;
use tempfile::NamedTempFile;
use crate::account::DeserializedBaseAccountConfig;
#[cfg(feature = "imap-backend")]
use crate::account::DeserializedImapAccountConfig;
#[cfg(feature = "maildir-backend")]
use crate::account::DeserializedMaildirAccountConfig;
#[cfg(feature = "notmuch-backend")]
use crate::account::DeserializedNotmuchAccountConfig;
use super::*;
fn make_config(config: &str) -> Result<DeserializedConfig> {
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", config).unwrap();
DeserializedConfig::from_opt_path(file.into_temp_path().to_str())
}
#[test]
fn empty_config() {
let config = make_config("");
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"config file must contain at least one account"
);
}
#[test]
fn account_missing_backend_field() {
let config = make_config("[account]");
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `backend` at line 1 column 1"
);
}
#[test]
fn account_invalid_backend_field() {
let config = make_config(
"[account]
backend = \"bad\"",
);
assert!(config
.unwrap_err()
.root_cause()
.to_string()
.starts_with("unknown variant `bad`"));
}
#[test]
fn account_missing_email_field() {
let config = make_config(
"[account]
backend = \"none\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `email` at line 1 column 1"
);
}
#[test]
fn imap_account_missing_host_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `imap-host` at line 1 column 1"
);
}
#[test]
fn account_backend_imap_missing_port_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `imap-port` at line 1 column 1"
);
}
#[test]
fn account_backend_imap_missing_login_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `imap-login` at line 1 column 1"
);
}
#[test]
fn account_backend_imap_missing_passwd_cmd_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993
imap-login = \"login\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `imap-passwd-cmd` at line 1 column 1"
);
}
#[test]
fn account_backend_maildir_missing_root_dir_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"maildir\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `maildir-root-dir` at line 1 column 1"
);
}
#[cfg(feature = "notmuch-backend")]
#[test]
fn account_backend_notmuch_missing_db_path_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"notmuch\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `notmuch-db-path` at line 1 column 1"
);
}
#[test]
fn account_missing_sender_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `sender` at line 1 column 1"
);
}
#[test]
fn account_invalid_sender_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"bad\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"unknown variant `bad`, expected one of `none`, `smtp`, `sendmail` at line 1 column 1",
);
}
#[test]
fn account_smtp_sender_missing_host_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `smtp-host` at line 1 column 1"
);
}
#[test]
fn account_smtp_sender_missing_port_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `smtp-port` at line 1 column 1"
);
}
#[test]
fn account_smtp_sender_missing_login_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `smtp-login` at line 1 column 1"
);
}
#[test]
fn account_smtp_sender_missing_passwd_cmd_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25
smtp-login = \"login\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `smtp-passwd-cmd` at line 1 column 1"
);
}
#[test]
fn account_sendmail_sender_missing_cmd_field() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"sendmail\"",
);
assert_eq!(
config.unwrap_err().root_cause().to_string(),
"missing field `sendmail-cmd` at line 1 column 1"
);
}
#[test]
fn account_smtp_sender_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"smtp\"
smtp-host = \"localhost\"
smtp-port = 25
smtp-login = \"login\"
smtp-passwd-cmd = \"echo password\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig::None(DeserializedBaseAccountConfig {
email: "test@localhost".into(),
email_sender: EmailSender::Smtp(SmtpConfig {
host: "localhost".into(),
port: 25,
login: "login".into(),
passwd_cmd: "echo password".into(),
..SmtpConfig::default()
}),
..DeserializedBaseAccountConfig::default()
})
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_sendmail_sender_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
backend = \"none\"
sender = \"sendmail\"
sendmail-cmd = \"echo send\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig::None(DeserializedBaseAccountConfig {
email: "test@localhost".into(),
email_sender: EmailSender::Sendmail(SendmailConfig {
cmd: "echo send".into(),
}),
..DeserializedBaseAccountConfig::default()
})
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_backend_imap_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"imap\"
imap-host = \"localhost\"
imap-port = 993
imap-login = \"login\"
imap-passwd-cmd = \"echo password\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
base: DeserializedBaseAccountConfig {
email: "test@localhost".into(),
..DeserializedBaseAccountConfig::default()
},
backend: ImapConfig {
host: "localhost".into(),
port: 993,
login: "login".into(),
passwd_cmd: "echo password".into(),
..ImapConfig::default()
}
})
)]),
..DeserializedConfig::default()
}
);
}
#[test]
fn account_backend_maildir_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"maildir\"
maildir-root-dir = \"/tmp/maildir\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig::Maildir(DeserializedMaildirAccountConfig {
base: DeserializedBaseAccountConfig {
email: "test@localhost".into(),
..DeserializedBaseAccountConfig::default()
},
backend: MaildirConfig {
root_dir: "/tmp/maildir".into(),
}
})
)]),
..DeserializedConfig::default()
}
);
}
#[cfg(feature = "notmuch-backend")]
#[test]
fn account_backend_notmuch_minimum_config() {
let config = make_config(
"[account]
email = \"test@localhost\"
sender = \"none\"
backend = \"notmuch\"
notmuch-db-path = \"/tmp/notmuch.db\"",
);
assert_eq!(
config.unwrap(),
DeserializedConfig {
accounts: HashMap::from_iter([(
"account".into(),
DeserializedAccountConfig::Notmuch(DeserializedNotmuchAccountConfig {
base: DeserializedBaseAccountConfig {
email: "test@localhost".into(),
..DeserializedBaseAccountConfig::default()
},
backend: NotmuchConfig {
db_path: "/tmp/notmuch.db".into(),
}
})
)]),
..DeserializedConfig::default()
}
);
}
}

View file

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

View file

@ -1,139 +0,0 @@
use himalaya_lib::{EmailHooks, EmailSender, EmailTextPlainFormat, SendmailConfig, SmtpConfig};
use serde::Deserialize;
use std::path::PathBuf;
#[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig;
#[cfg(feature = "maildir-backend")]
use himalaya_lib::MaildirConfig;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig;
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "SmtpConfig")]
struct SmtpConfigDef {
#[serde(rename = "smtp-host")]
pub host: String,
#[serde(rename = "smtp-port")]
pub port: u16,
#[serde(rename = "smtp-starttls")]
pub starttls: Option<bool>,
#[serde(rename = "smtp-insecure")]
pub insecure: Option<bool>,
#[serde(rename = "smtp-login")]
pub login: String,
#[serde(rename = "smtp-passwd-cmd")]
pub passwd_cmd: String,
}
#[cfg(feature = "imap-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "ImapConfig")]
pub struct ImapConfigDef {
#[serde(rename = "imap-host")]
pub host: String,
#[serde(rename = "imap-port")]
pub port: u16,
#[serde(rename = "imap-starttls")]
pub starttls: Option<bool>,
#[serde(rename = "imap-insecure")]
pub insecure: Option<bool>,
#[serde(rename = "imap-login")]
pub login: String,
#[serde(rename = "imap-passwd-cmd")]
pub passwd_cmd: String,
#[serde(rename = "imap-notify-cmd")]
pub notify_cmd: Option<String>,
#[serde(rename = "imap-notify-query")]
pub notify_query: Option<String>,
#[serde(rename = "imap-watch-cmds")]
pub watch_cmds: Option<Vec<String>>,
}
#[cfg(feature = "maildir-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "MaildirConfig")]
pub struct MaildirConfigDef {
#[serde(rename = "maildir-root-dir")]
pub root_dir: PathBuf,
}
#[cfg(feature = "notmuch-backend")]
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "NotmuchConfig")]
pub struct NotmuchConfigDef {
#[serde(rename = "notmuch-db-path")]
pub db_path: PathBuf,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
enum EmailTextPlainFormatDef {
Auto,
Flowed,
Fixed(usize),
}
pub mod email_text_plain_format {
use himalaya_lib::EmailTextPlainFormat;
use serde::{Deserialize, Deserializer};
use super::EmailTextPlainFormatDef;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailTextPlainFormat>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat);
let helper = Option::deserialize(deserializer)?;
Ok(helper.map(|Helper(external)| external))
}
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
pub enum EmailSenderDef {
None,
#[serde(with = "SmtpConfigDef")]
Smtp(SmtpConfig),
#[serde(with = "SendmailConfigDef")]
Sendmail(SendmailConfig),
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "SendmailConfig")]
pub struct SendmailConfigDef {
#[serde(rename = "sendmail-cmd")]
cmd: String,
}
/// Represents the email hooks. Useful for doing extra email
/// processing before or after sending it.
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
#[serde(remote = "EmailHooks")]
struct EmailHooksDef {
/// Represents the hook called just before sending an email.
pub pre_send: Option<String>,
}
pub mod email_hooks {
use himalaya_lib::EmailHooks;
use serde::{Deserialize, Deserializer};
use super::EmailHooksDef;
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailHooks>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks);
let helper = Option::deserialize(deserializer)?;
Ok(helper.map(|Helper(external)| external))
}
}

View file

@ -1,54 +0,0 @@
//! Account module.
//!
//! This module contains the definition of the printable account,
//! which is only used by the "accounts" command to list all available
//! accounts from the config file.
use serde::Serialize;
use std::fmt;
use crate::ui::table::{Cell, Row, Table};
/// Represents the printable account.
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
pub struct Account {
/// Represents the account name.
pub name: String,
/// Represents the backend name of the account.
pub backend: String,
/// Represents the default state of the account.
pub default: bool,
}
impl Account {
pub fn new(name: &str, backend: &str, default: bool) -> Self {
Self {
name: name.into(),
backend: backend.into(),
default,
}
}
}
impl fmt::Display for Account {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.name)
}
}
impl Table for Account {
fn head() -> Row {
Row::new()
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
.cell(Cell::new("BACKEND").bold().underline().white())
.cell(Cell::new("DEFAULT").bold().underline().white())
}
fn row(&self) -> Row {
let default = if self.default { "yes" } else { "" };
Row::new()
.cell(Cell::new(&self.name).shrinkable().green())
.cell(Cell::new(&self.backend).blue())
.cell(Cell::new(default).white())
}
}

View file

@ -1,61 +0,0 @@
//! Account module.
//!
//! This module contains the definition of the printable account,
//! which is only used by the "accounts" command to list all available
//! accounts from the config file.
use anyhow::Result;
use serde::Serialize;
use std::{collections::hash_map::Iter, ops::Deref};
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
use super::{Account, DeserializedAccountConfig};
/// Represents the list of printable accounts.
#[derive(Debug, Default, Serialize)]
pub struct Accounts(pub Vec<Account>);
impl Deref for Accounts {
type Target = Vec<Account>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl PrintTable for Accounts {
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
writeln!(writer)?;
Table::print(writer, self, opts)?;
writeln!(writer)?;
Ok(())
}
}
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
let mut accounts: Vec<_> = map
.map(|(name, account)| match account {
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => {
Account::new(name, "imap", config.base.default.unwrap_or_default())
}
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => {
Account::new(name, "maildir", config.base.default.unwrap_or_default())
}
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => {
Account::new(name, "notmuch", config.base.default.unwrap_or_default())
}
DeserializedAccountConfig::None(..) => Account::new(name, "none", false),
})
.collect();
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
Self(accounts)
}
}

View file

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

View file

@ -1,215 +0,0 @@
//! Deserialized account config module.
//!
//! This module contains the raw deserialized representation of an
//! account in the accounts section of the user configuration file.
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat};
#[cfg(feature = "imap-backend")]
use himalaya_lib::ImapConfig;
#[cfg(feature = "maildir-backend")]
use himalaya_lib::MaildirConfig;
#[cfg(feature = "notmuch-backend")]
use himalaya_lib::NotmuchConfig;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::config::{prelude::*, DeserializedConfig};
/// Represents all existing kind of account config.
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(tag = "backend", rename_all = "snake_case")]
pub enum DeserializedAccountConfig {
None(DeserializedBaseAccountConfig),
#[cfg(feature = "imap-backend")]
Imap(DeserializedImapAccountConfig),
#[cfg(feature = "maildir-backend")]
Maildir(DeserializedMaildirAccountConfig),
#[cfg(feature = "notmuch-backend")]
Notmuch(DeserializedNotmuchAccountConfig),
}
impl DeserializedAccountConfig {
pub fn to_configs(&self, global_config: &DeserializedConfig) -> (AccountConfig, BackendConfig) {
match self {
DeserializedAccountConfig::None(config) => {
(config.to_account_config(global_config), BackendConfig::None)
}
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => (
config.base.to_account_config(global_config),
BackendConfig::Imap(&config.backend),
),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => (
config.base.to_account_config(global_config),
BackendConfig::Maildir(&config.backend),
),
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => (
config.base.to_account_config(global_config),
BackendConfig::Notmuch(&config.backend),
),
}
}
pub fn is_default(&self) -> bool {
match self {
DeserializedAccountConfig::None(config) => config.default.unwrap_or_default(),
#[cfg(feature = "imap-backend")]
DeserializedAccountConfig::Imap(config) => config.base.default.unwrap_or_default(),
#[cfg(feature = "maildir-backend")]
DeserializedAccountConfig::Maildir(config) => config.base.default.unwrap_or_default(),
#[cfg(feature = "notmuch-backend")]
DeserializedAccountConfig::Notmuch(config) => config.base.default.unwrap_or_default(),
}
}
}
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct DeserializedBaseAccountConfig {
pub email: String,
pub default: Option<bool>,
pub display_name: Option<String>,
pub signature_delim: Option<String>,
pub signature: Option<String>,
pub downloads_dir: Option<PathBuf>,
pub folder_listing_page_size: Option<usize>,
pub folder_aliases: Option<HashMap<String, String>>,
pub email_listing_page_size: Option<usize>,
pub email_reading_headers: Option<Vec<String>>,
#[serde(default, with = "email_text_plain_format")]
pub email_reading_format: Option<EmailTextPlainFormat>,
pub email_reading_decrypt_cmd: Option<String>,
pub email_writing_encrypt_cmd: Option<String>,
#[serde(flatten, with = "EmailSenderDef")]
pub email_sender: EmailSender,
#[serde(default, with = "email_hooks")]
pub email_hooks: Option<EmailHooks>,
}
impl DeserializedBaseAccountConfig {
pub fn to_account_config(&self, config: &DeserializedConfig) -> AccountConfig {
let mut folder_aliases = config
.folder_aliases
.as_ref()
.map(ToOwned::to_owned)
.unwrap_or_default();
folder_aliases.extend(
self.folder_aliases
.as_ref()
.map(ToOwned::to_owned)
.unwrap_or_default(),
);
AccountConfig {
email: self.email.to_owned(),
display_name: self
.display_name
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)),
signature_delim: self
.signature_delim
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)),
signature: self
.signature
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.signature.as_ref().map(ToOwned::to_owned)),
downloads_dir: self
.downloads_dir
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)),
folder_listing_page_size: self
.folder_listing_page_size
.or_else(|| config.folder_listing_page_size),
folder_aliases,
email_listing_page_size: self
.email_listing_page_size
.or_else(|| config.email_listing_page_size),
email_reading_headers: self
.email_reading_headers
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)),
email_reading_format: self
.email_reading_format
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned))
.unwrap_or_default(),
email_reading_decrypt_cmd: self
.email_reading_decrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_reading_decrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_writing_encrypt_cmd: self
.email_writing_encrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
.or_else(|| {
config
.email_writing_encrypt_cmd
.as_ref()
.map(ToOwned::to_owned)
}),
email_sender: self.email_sender.to_owned(),
email_hooks: EmailHooks {
pre_send: self
.email_hooks
.as_ref()
.map(ToOwned::to_owned)
.map(|hook| hook.pre_send)
.or_else(|| {
config
.email_hooks
.as_ref()
.map(|hook| hook.pre_send.as_ref().map(ToOwned::to_owned))
})
.unwrap_or_default(),
},
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[cfg(feature = "imap-backend")]
pub struct DeserializedImapAccountConfig {
#[serde(flatten)]
pub base: DeserializedBaseAccountConfig,
#[serde(flatten, with = "ImapConfigDef")]
pub backend: ImapConfig,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[cfg(feature = "maildir-backend")]
pub struct DeserializedMaildirAccountConfig {
#[serde(flatten)]
pub base: DeserializedBaseAccountConfig,
#[serde(flatten, with = "MaildirConfigDef")]
pub backend: MaildirConfig,
}
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[cfg(feature = "notmuch-backend")]
pub struct DeserializedNotmuchAccountConfig {
#[serde(flatten)]
pub base: DeserializedBaseAccountConfig,
#[serde(flatten, with = "NotmuchConfigDef")]
pub backend: NotmuchConfig,
}

View file

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

View file

@ -1,9 +0,0 @@
pub mod account;
pub mod accounts;
pub mod args;
pub mod config;
pub mod handlers;
pub use account::*;
pub use accounts::*;
pub use config::*;

View file

@ -1,489 +0,0 @@
//! Module related to email CLI.
//!
//! This module provides subcommands, arguments and a command matcher related to email.
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use himalaya_lib::email::TplOverride;
use log::{debug, trace};
use crate::{email, flag, folder, tpl, ui::table};
const ARG_ATTACHMENTS: &str = "attachment";
const ARG_CRITERIA: &str = "criterion";
const ARG_ENCRYPT: &str = "encrypt";
const ARG_HEADERS: &str = "header";
const ARG_ID: &str = "id";
const ARG_IDS: &str = "ids";
const ARG_MIME_TYPE: &str = "mime-type";
const ARG_PAGE: &str = "page";
const ARG_PAGE_SIZE: &str = "page-size";
const ARG_QUERY: &str = "query";
const ARG_RAW: &str = "raw";
const ARG_REPLY_ALL: &str = "reply-all";
const CMD_ATTACHMENTS: &str = "attachments";
const CMD_COPY: &str = "copy";
const CMD_DEL: &str = "delete";
const CMD_FORWARD: &str = "forward";
const CMD_LIST: &str = "list";
const CMD_MOVE: &str = "move";
const CMD_READ: &str = "read";
const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save";
const CMD_SEARCH: &str = "search";
const CMD_SEND: &str = "send";
const CMD_SORT: &str = "sort";
const CMD_WRITE: &str = "write";
type Criteria = String;
type Encrypt = bool;
type Folder<'a> = &'a str;
type Page = usize;
type PageSize = usize;
type Query = String;
type Raw = bool;
type RawEmail<'a> = &'a str;
type TextMime<'a> = &'a str;
pub(crate) type All = bool;
pub(crate) type Attachments<'a> = Vec<&'a str>;
pub(crate) type Headers<'a> = Vec<&'a str>;
pub(crate) type Id<'a> = &'a str;
pub(crate) type Ids<'a> = &'a str;
/// Represents the email commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Attachments(Id<'a>),
Copy(Id<'a>, Folder<'a>),
Delete(Ids<'a>),
Forward(Id<'a>, Attachments<'a>, Encrypt),
List(table::args::MaxTableWidth, Option<PageSize>, Page),
Move(Id<'a>, Folder<'a>),
Read(Id<'a>, TextMime<'a>, Raw, Headers<'a>),
Reply(Id<'a>, All, Attachments<'a>, Encrypt),
Save(RawEmail<'a>),
Search(Query, table::args::MaxTableWidth, Option<PageSize>, Page),
Send(RawEmail<'a>),
Sort(
Criteria,
Query,
table::args::MaxTableWidth,
Option<PageSize>,
Page,
),
Write(TplOverride<'a>, Attachments<'a>, Encrypt),
Flag(Option<flag::args::Cmd<'a>>),
Tpl(Option<tpl::args::Cmd<'a>>),
}
/// Email command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
trace!("matches: {:?}", m);
let cmd = if let Some(m) = m.subcommand_matches(CMD_ATTACHMENTS) {
debug!("attachments command matched");
let id = parse_id_arg(m);
Cmd::Attachments(id)
} else if let Some(m) = m.subcommand_matches(CMD_COPY) {
debug!("copy command matched");
let id = parse_id_arg(m);
let folder = folder::args::parse_target_arg(m);
Cmd::Copy(id, folder)
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
debug!("delete command matched");
let ids = parse_ids_arg(m);
Cmd::Delete(ids)
} else if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
debug!("forward command matched");
let id = parse_id_arg(m);
let attachments = parse_attachments_arg(m);
let encrypt = parse_encrypt_flag(m);
Cmd::Forward(id, attachments, encrypt)
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
debug!("list command matched");
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
Cmd::List(max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_MOVE) {
debug!("move command matched");
let id = parse_id_arg(m);
let folder = folder::args::parse_target_arg(m);
Cmd::Move(id, folder)
} else if let Some(m) = m.subcommand_matches(CMD_READ) {
debug!("read command matched");
let id = parse_id_arg(m);
let mime = parse_mime_type_arg(m);
let raw = parse_raw_flag(m);
let headers = parse_headers_arg(m);
Cmd::Read(id, mime, raw, headers)
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
debug!("reply command matched");
let id = parse_id_arg(m);
let all = parse_reply_all_flag(m);
let attachments = parse_attachments_arg(m);
let encrypt = parse_encrypt_flag(m);
Cmd::Reply(id, all, attachments, encrypt)
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
debug!("save command matched");
let email = parse_raw_arg(m);
Cmd::Save(email)
} else if let Some(m) = m.subcommand_matches(CMD_SEARCH) {
debug!("search command matched");
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
let query = parse_query_arg(m);
Cmd::Search(query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SORT) {
debug!("sort command matched");
let max_table_width = table::args::parse_max_width(m);
let page_size = parse_page_size_arg(m);
let page = parse_page_arg(m);
let criteria = parse_criteria_arg(m);
let query = parse_query_arg(m);
Cmd::Sort(criteria, query, max_table_width, page_size, page)
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
debug!("send command matched");
let email = parse_raw_arg(m);
Cmd::Send(email)
} else if let Some(m) = m.subcommand_matches(CMD_WRITE) {
debug!("write command matched");
let attachments = parse_attachments_arg(m);
let encrypt = parse_encrypt_flag(m);
let tpl = tpl::args::parse_override_arg(m);
Cmd::Write(tpl, attachments, encrypt)
} else if let Some(m) = m.subcommand_matches(tpl::args::CMD_TPL) {
Cmd::Tpl(tpl::args::matches(m)?)
} else if let Some(m) = m.subcommand_matches(flag::args::CMD_FLAG) {
Cmd::Flag(flag::args::matches(m)?)
} else {
debug!("default list command matched");
Cmd::List(None, None, 0)
};
Ok(Some(cmd))
}
/// Represents the email subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![
flag::args::subcmds(),
tpl::args::subcmds(),
vec![
SubCommand::with_name(CMD_ATTACHMENTS)
.aliases(&["attachment", "attach", "att", "at", "a"])
.about("Downloads all attachments of the targeted email")
.arg(email::args::id_arg()),
SubCommand::with_name(CMD_LIST)
.aliases(&["lst", "l"])
.about("Lists all emails")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width()),
SubCommand::with_name(CMD_SEARCH)
.aliases(&["s", "query", "q"])
.about("Lists emails matching the given query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width())
.arg(query_arg()),
SubCommand::with_name(CMD_SORT)
.about("Sorts emails by the given criteria and matching the given query")
.arg(page_size_arg())
.arg(page_arg())
.arg(table::args::max_width())
.arg(criteria_arg())
.arg(query_arg()),
SubCommand::with_name(CMD_WRITE)
.about("Writes a new email")
.aliases(&["w", "new", "n"])
.args(&tpl::args::args())
.arg(attachments_arg())
.arg(encrypt_flag()),
SubCommand::with_name(CMD_SEND)
.about("Sends a raw email")
.arg(raw_arg()),
SubCommand::with_name(CMD_SAVE)
.about("Saves a raw email")
.arg(raw_arg()),
SubCommand::with_name(CMD_READ)
.about("Reads text bodies of a email")
.arg(id_arg())
.arg(mime_type_arg())
.arg(raw_flag())
.arg(headers_arg()),
SubCommand::with_name(CMD_REPLY)
.aliases(&["rep", "r"])
.about("Answers to an email")
.arg(id_arg())
.arg(reply_all_flag())
.arg(attachments_arg())
.arg(encrypt_flag()),
SubCommand::with_name(CMD_FORWARD)
.aliases(&["fwd", "f"])
.about("Forwards an email")
.arg(id_arg())
.arg(attachments_arg())
.arg(encrypt_flag()),
SubCommand::with_name(CMD_COPY)
.aliases(&["cp", "c"])
.about("Copies an email to the targeted folder")
.arg(id_arg())
.arg(folder::args::target_arg()),
SubCommand::with_name(CMD_MOVE)
.aliases(&["mv"])
.about("Moves an email to the targeted folder")
.arg(id_arg())
.arg(folder::args::target_arg()),
SubCommand::with_name(CMD_DEL)
.aliases(&["del", "d", "remove", "rm"])
.about("Deletes an email")
.arg(ids_arg()),
],
]
.concat()
}
/// Represents the email id argument.
pub fn id_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_ID)
.help("Specifies the target email")
.value_name("ID")
.required(true)
}
/// Represents the email id argument parser.
pub fn parse_id_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_ID).unwrap()
}
/// Represents the email sort criteria argument.
pub fn criteria_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_CRITERIA)
.long("criterion")
.short("c")
.help("Email sorting preferences")
.value_name("CRITERION:ORDER")
.takes_value(true)
.multiple(true)
.required(true)
.possible_values(&[
"arrival",
"arrival:asc",
"arrival:desc",
"cc",
"cc:asc",
"cc:desc",
"date",
"date:asc",
"date:desc",
"from",
"from:asc",
"from:desc",
"size",
"size:asc",
"size:desc",
"subject",
"subject:asc",
"subject:desc",
"to",
"to:asc",
"to:desc",
])
}
/// Represents the email sort criteria argument parser.
pub fn parse_criteria_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
matches
.values_of(ARG_CRITERIA)
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ")
}
/// Represents the email ids argument.
pub fn ids_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_IDS)
.help("Specifies the target email(s)")
.long_help("Specifies a range of emails. The range follows the RFC3501 format.")
.value_name("IDS")
.required(true)
}
/// Represents the email ids argument parser.
pub fn parse_ids_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(email::args::ARG_IDS).unwrap()
}
/// Represents the email reply all argument.
pub fn reply_all_flag<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_REPLY_ALL)
.help("Includes all recipients")
.short("A")
.long("all")
}
/// Represents the email reply all argument parser.
pub fn parse_reply_all_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
matches.is_present(ARG_REPLY_ALL)
}
/// Represents the page size argument.
fn page_size_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_PAGE_SIZE)
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
}
/// Represents the page size argument parser.
fn parse_page_size_arg<'a>(matches: &'a ArgMatches<'a>) -> Option<usize> {
matches.value_of(ARG_PAGE_SIZE).and_then(|s| s.parse().ok())
}
/// Represents the page argument.
fn page_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_PAGE)
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value("1")
}
/// Represents the page argument parser.
fn parse_page_arg<'a>(matches: &'a ArgMatches<'a>) -> usize {
matches
.value_of(ARG_PAGE)
.unwrap()
.parse()
.ok()
.map(|page| 1.max(page) - 1)
.unwrap_or_default()
}
/// Represents the email attachments argument.
pub fn attachments_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_ATTACHMENTS)
.help("Adds attachment to the email")
.short("a")
.long("attachment")
.value_name("PATH")
.multiple(true)
}
/// Represents the email attachments argument parser.
pub fn parse_attachments_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
matches
.values_of(ARG_ATTACHMENTS)
.unwrap_or_default()
.collect()
}
/// Represents the email headers argument.
pub fn headers_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_HEADERS)
.help("Shows additional headers with the email")
.short("h")
.long("header")
.value_name("STRING")
.multiple(true)
}
/// Represents the email headers argument parser.
pub fn parse_headers_arg<'a>(matches: &'a ArgMatches<'a>) -> Vec<&'a str> {
matches.values_of(ARG_HEADERS).unwrap_or_default().collect()
}
/// Represents the raw flag.
pub fn raw_flag<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_RAW)
.help("Reads a raw email")
.long("raw")
.short("r")
}
/// Represents the raw flag parser.
pub fn parse_raw_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
matches.is_present(ARG_RAW)
}
/// Represents the email raw argument.
pub fn raw_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_RAW).raw(true)
}
/// Represents the email raw argument parser.
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_RAW).unwrap_or_default()
}
/// Represents the email encrypt flag.
pub fn encrypt_flag<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_ENCRYPT)
.help("Encrypts the email")
.short("e")
.long("encrypt")
}
/// Represents the email encrypt flag parser.
pub fn parse_encrypt_flag<'a>(matches: &'a ArgMatches<'a>) -> bool {
matches.is_present(ARG_ENCRYPT)
}
/// Represents the email MIME type argument.
pub fn mime_type_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_MIME_TYPE)
.help("MIME type to use")
.short("t")
.long("mime-type")
.value_name("MIME")
.possible_values(&["plain", "html"])
.default_value("plain")
}
/// Represents the email MIME type argument parser.
pub fn parse_mime_type_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_MIME_TYPE).unwrap()
}
/// Represents the email query argument.
pub fn query_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_QUERY)
.long_help("The query system depends on the backend, see the wiki for more details")
.value_name("QUERY")
.multiple(true)
.required(true)
}
/// Represents the email query argument parser.
pub fn parse_query_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
matches
.values_of(ARG_QUERY)
.unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd, escape) {
// Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string());
(true, cmds)
}
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ")
}

View file

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

View file

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

View file

@ -1,30 +0,0 @@
use himalaya_lib::{Envelope, Flag};
use crate::ui::{Cell, Row, Table};
impl Table for Envelope {
fn head() -> Row {
Row::new()
.cell(Cell::new("ID").bold().underline().white())
.cell(Cell::new("FLAGS").bold().underline().white())
.cell(Cell::new("SUBJECT").shrinkable().bold().underline().white())
.cell(Cell::new("SENDER").bold().underline().white())
.cell(Cell::new("DATE").bold().underline().white())
}
fn row(&self) -> Row {
let id = self.id.to_string();
let flags = self.flags.to_symbols_string();
let unseen = !self.flags.contains(&Flag::Seen);
let subject = &self.subject;
let sender = &self.sender;
let date = self.date.as_deref().unwrap_or_default();
Row::new()
.cell(Cell::new(id).bold_if(unseen).red())
.cell(Cell::new(flags).bold_if(unseen).white())
.cell(Cell::new(subject).shrinkable().bold_if(unseen).green())
.cell(Cell::new(sender).bold_if(unseen).blue())
.cell(Cell::new(date).bold_if(unseen).yellow())
}
}

View file

@ -1,16 +0,0 @@
use anyhow::Result;
use himalaya_lib::Envelopes;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
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,5 +0,0 @@
pub mod envelope;
pub mod envelopes;
pub use envelope::*;
pub use envelopes::*;

View file

@ -1,99 +0,0 @@
//! Email flag CLI module.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to the email flag domain.
use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use log::{debug, info};
use crate::email;
const ARG_FLAGS: &str = "flag";
const CMD_ADD: &str = "add";
const CMD_DEL: &str = "remove";
const CMD_SET: &str = "set";
pub(crate) const CMD_FLAG: &str = "flag";
type Flags = String;
/// Represents the flag commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Add(email::args::Ids<'a>, Flags),
Set(email::args::Ids<'a>, Flags),
Del(email::args::Ids<'a>, Flags),
}
/// Represents the flag command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_ADD) {
debug!("add subcommand matched");
let ids = email::args::parse_ids_arg(m);
let flags: String = parse_flags_arg(m);
Some(Cmd::Add(ids, flags))
} else if let Some(m) = m.subcommand_matches(CMD_SET) {
debug!("set subcommand matched");
let ids = email::args::parse_ids_arg(m);
let flags: String = parse_flags_arg(m);
Some(Cmd::Set(ids, flags))
} else if let Some(m) = m.subcommand_matches(CMD_DEL) {
info!("remove subcommand matched");
let ids = email::args::parse_ids_arg(m);
let flags: String = parse_flags_arg(m);
Some(Cmd::Del(ids, flags))
} else {
None
};
Ok(cmd)
}
/// Represents the flag subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name(CMD_FLAG)
.aliases(&["flags", "flg"])
.about("Handles email flags")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name(CMD_ADD)
.aliases(&["a"])
.about("Adds email flags")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name(CMD_SET)
.aliases(&["s", "change", "c"])
.about("Sets email flags")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)
.subcommand(
SubCommand::with_name(CMD_DEL)
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
.about("Removes email flags")
.arg(email::args::ids_arg())
.arg(flags_arg()),
)]
}
/// Represents the flags argument.
pub fn flags_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_FLAGS)
.long_help("Flags are case-insensitive, and they do not need to be prefixed with `\\`.")
.value_name("FLAGS…")
.multiple(true)
.required(true)
}
/// Represents the flags argument parser.
pub fn parse_flags_arg<'a>(matches: &'a ArgMatches<'a>) -> String {
matches
.values_of(ARG_FLAGS)
.unwrap_or_default()
.collect::<Vec<_>>()
.join(" ")
}

View file

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

View file

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

View file

@ -1,153 +0,0 @@
//! Folder CLI module.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to the folder domain.
use anyhow::Result;
use clap::{self, App, Arg, ArgMatches, SubCommand};
use log::debug;
use crate::ui::table;
const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target";
const CMD_FOLDERS: &str = "folders";
/// Represents the folder commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
List(table::args::MaxTableWidth),
}
/// Represents the folder command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDERS) {
debug!("folders command matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else {
None
};
Ok(cmd)
}
/// Represents folder subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name(CMD_FOLDERS)
.aliases(&[
"folder",
"fold",
"fo",
"mailboxes",
"mailbox",
"mboxes",
"mbox",
"mb",
"m",
])
.about("Lists folders")
.arg(table::args::max_width())]
}
/// Represents the source folder argument.
pub fn source_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_SOURCE)
.short("f")
.long("folder")
.help("Specifies the source folder")
.value_name("SOURCE")
.default_value("inbox")
}
/// Represents the source folder argument parser.
pub fn parse_source_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_SOURCE).unwrap()
}
/// Represents the target folder argument.
pub fn target_arg<'a>() -> Arg<'a, 'a> {
Arg::with_name(ARG_TARGET)
.help("Specifies the target folder")
.value_name("TARGET")
.required(true)
}
/// Represents the target folder argument parser.
pub fn parse_target_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_TARGET).unwrap()
}
#[cfg(test)]
mod tests {
use clap::{App, ErrorKind};
use super::*;
#[test]
fn it_should_match_cmds() {
let arg = App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", "folders"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
let arg = App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", "folders", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
}
#[test]
fn it_should_match_aliases() {
macro_rules! get_matches_from {
($alias:expr) => {
App::new("himalaya")
.subcommands(subcmds())
.get_matches_from(&["himalaya", $alias])
.subcommand_name()
};
}
assert_eq!(Some("folders"), get_matches_from!["folders"]);
assert_eq!(Some("folders"), get_matches_from!["folder"]);
assert_eq!(Some("folders"), get_matches_from!["fold"]);
assert_eq!(Some("folders"), get_matches_from!["fo"]);
}
#[test]
fn it_should_match_source_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
App::new("himalaya")
.arg(source_arg())
.get_matches_from(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(Some("inbox"), app.value_of("source"));
let app = get_matches_from!["-f", "SOURCE"];
assert_eq!(Some("SOURCE"), app.value_of("source"));
let app = get_matches_from!["--folder", "SOURCE"];
assert_eq!(Some("SOURCE"), app.value_of("source"));
}
#[test]
fn it_should_match_target_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
App::new("himalaya")
.arg(target_arg())
.get_matches_from_safe(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind);
let app = get_matches_from!["TARGET"];
assert_eq!(Some("TARGET"), app.unwrap().value_of("target"));
}
}

View file

@ -1,19 +0,0 @@
use himalaya_lib::folder::Folder;
use crate::ui::{Cell, Row, Table};
impl Table for Folder {
fn head() -> Row {
Row::new()
.cell(Cell::new("DELIM").bold().underline().white())
.cell(Cell::new("NAME").bold().underline().white())
.cell(Cell::new("DESC").bold().underline().white())
}
fn row(&self) -> Row {
Row::new()
.cell(Cell::new(&self.delim).white())
.cell(Cell::new(&self.name).blue())
.cell(Cell::new(&self.desc).green())
}
}

View file

@ -1,16 +0,0 @@
use anyhow::Result;
use himalaya_lib::folder::Folders;
use crate::{
printer::{PrintTable, PrintTableOpts, WriteColor},
ui::Table,
};
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,185 +0,0 @@
//! Folder handling module.
//!
//! This module gathers all folder actions triggered by the CLI.
use anyhow::Result;
use himalaya_lib::{AccountConfig, Backend};
use log::trace;
use crate::printer::{PrintTableOpts, Printer};
/// Lists all folders.
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
max_width: Option<usize>,
config: &AccountConfig,
printer: &mut P,
backend: &mut B,
) -> Result<()> {
let folders = backend.folder_list()?;
trace!("folders: {:?}", folders);
printer.print_table(
// TODO: remove Box
Box::new(folders),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)
}
#[cfg(test)]
mod tests {
use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders};
use std::{fmt::Debug, io};
use termcolor::ColorSpec;
use crate::printer::{Print, PrintTable, WriteColor};
use super::*;
#[test]
fn it_should_list_mboxes() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> anyhow::Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_str<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
unimplemented!()
}
fn print_struct<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> anyhow::Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
impl<'a> Backend<'a> for TestBackend {
fn folder_add(&mut self, _: &str) -> backend::Result<()> {
unimplemented!();
}
fn folder_list(&mut self) -> backend::Result<Folders> {
Ok(Folders(vec![
Folder {
delim: "/".into(),
name: "INBOX".into(),
desc: "desc".into(),
},
Folder {
delim: "/".into(),
name: "Sent".into(),
desc: "desc".into(),
},
]))
}
fn folder_delete(&mut self, _: &str) -> backend::Result<()> {
unimplemented!();
}
fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
unimplemented!()
}
fn envelope_search(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> backend::Result<Envelopes> {
unimplemented!()
}
fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
unimplemented!()
}
fn email_get(&mut self, _: &str, _: &str) -> backend::Result<Email> {
unimplemented!()
}
fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
unimplemented!()
}
fn as_any(&self) -> &(dyn std::any::Any + 'a) {
self
}
}
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
assert!(list(None, &account_config, &mut printer, &mut backend).is_ok());
assert_eq!(
concat![
"\n",
"DELIM │NAME │DESC \n",
"/ │INBOX │desc \n",
"/ │Sent │desc \n",
"\n"
],
printer.writer.content
);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,17 +0,0 @@
pub mod account;
pub mod email;
pub mod envelope;
pub mod flag;
pub mod folder;
#[cfg(feature = "imap-backend")]
pub mod imap;
pub mod tpl;
pub use self::account::{args, handlers, Account, Accounts};
pub use self::email::*;
pub use self::envelope::*;
pub use self::flag::*;
pub use self::folder::*;
#[cfg(feature = "imap-backend")]
pub use self::imap::*;
pub use self::tpl::*;

View file

@ -1,185 +0,0 @@
//! Module related to email template CLI.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to email templating.
use anyhow::Result;
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
use himalaya_lib::email::TplOverride;
use log::debug;
use crate::email;
const ARG_BCC: &str = "bcc";
const ARG_BODY: &str = "body";
const ARG_CC: &str = "cc";
const ARG_FROM: &str = "from";
const ARG_HEADERS: &str = "header";
const ARG_SIGNATURE: &str = "signature";
const ARG_SUBJECT: &str = "subject";
const ARG_TO: &str = "to";
const ARG_TPL: &str = "template";
const CMD_FORWARD: &str = "forward";
const CMD_NEW: &str = "new";
const CMD_REPLY: &str = "reply";
const CMD_SAVE: &str = "save";
const CMD_SEND: &str = "send";
pub(crate) const CMD_TPL: &str = "template";
type Tpl<'a> = &'a str;
/// Represents the template commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd<'a> {
Forward(email::args::Id<'a>, TplOverride<'a>),
New(TplOverride<'a>),
Reply(email::args::Id<'a>, email::args::All, TplOverride<'a>),
Save(email::args::Attachments<'a>, Tpl<'a>),
Send(email::args::Attachments<'a>, Tpl<'a>),
}
/// Represents the template command matcher.
pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FORWARD) {
debug!("forward subcommand matched");
let id = email::args::parse_id_arg(m);
let tpl = parse_override_arg(m);
Some(Cmd::Forward(id, tpl))
} else if let Some(m) = m.subcommand_matches(CMD_NEW) {
debug!("new subcommand matched");
let tpl = parse_override_arg(m);
Some(Cmd::New(tpl))
} else if let Some(m) = m.subcommand_matches(CMD_REPLY) {
debug!("reply subcommand matched");
let id = email::args::parse_id_arg(m);
let all = email::args::parse_reply_all_flag(m);
let tpl = parse_override_arg(m);
Some(Cmd::Reply(id, all, tpl))
} else if let Some(m) = m.subcommand_matches(CMD_SAVE) {
debug!("save subcommand matched");
let attachments = email::args::parse_attachments_arg(m);
let tpl = parse_raw_arg(m);
Some(Cmd::Save(attachments, tpl))
} else if let Some(m) = m.subcommand_matches(CMD_SEND) {
debug!("send subcommand matched");
let attachments = email::args::parse_attachments_arg(m);
let tpl = parse_raw_arg(m);
Some(Cmd::Send(attachments, tpl))
} else {
None
};
Ok(cmd)
}
/// Represents the template subcommands.
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
vec![SubCommand::with_name(CMD_TPL)
.aliases(&["tpl"])
.about("Handles email templates")
.setting(AppSettings::SubcommandRequiredElseHelp)
.subcommand(
SubCommand::with_name(CMD_NEW)
.aliases(&["n"])
.about("Generates a template for a new email")
.args(&args()),
)
.subcommand(
SubCommand::with_name(CMD_REPLY)
.aliases(&["rep", "re", "r"])
.about("Generates a template for replying to an email")
.arg(email::args::id_arg())
.arg(email::args::reply_all_flag())
.args(&args()),
)
.subcommand(
SubCommand::with_name(CMD_FORWARD)
.aliases(&["fwd", "fw", "f"])
.about("Generates a template for forwarding an email")
.arg(email::args::id_arg())
.args(&args()),
)
.subcommand(
SubCommand::with_name(CMD_SAVE)
.about("Saves an email based on the given template")
.arg(&email::args::attachments_arg())
.arg(Arg::with_name(ARG_TPL).raw(true)),
)
.subcommand(
SubCommand::with_name(CMD_SEND)
.about("Sends an email based on the given template")
.arg(&email::args::attachments_arg())
.arg(Arg::with_name(ARG_TPL).raw(true)),
)]
}
/// Represents the template arguments.
pub fn args<'a>() -> Vec<Arg<'a, 'a>> {
vec![
Arg::with_name(ARG_SUBJECT)
.help("Overrides the Subject header")
.short("s")
.long("subject")
.value_name("STRING"),
Arg::with_name(ARG_FROM)
.help("Overrides the From header")
.short("f")
.long("from")
.value_name("ADDR")
.multiple(true),
Arg::with_name(ARG_TO)
.help("Overrides the To header")
.short("t")
.long("to")
.value_name("ADDR")
.multiple(true),
Arg::with_name(ARG_CC)
.help("Overrides the Cc header")
.short("c")
.long("cc")
.value_name("ADDR")
.multiple(true),
Arg::with_name(ARG_BCC)
.help("Overrides the Bcc header")
.short("b")
.long("bcc")
.value_name("ADDR")
.multiple(true),
Arg::with_name(ARG_HEADERS)
.help("Overrides a specific header")
.short("h")
.long("header")
.value_name("KEY:VAL")
.multiple(true),
Arg::with_name(ARG_BODY)
.help("Overrides the body")
.short("B")
.long("body")
.value_name("STRING"),
Arg::with_name(ARG_SIGNATURE)
.help("Overrides the signature")
.short("S")
.long("signature")
.value_name("STRING"),
]
}
/// Represents the template override argument parser.
pub fn parse_override_arg<'a>(matches: &'a ArgMatches<'a>) -> TplOverride {
TplOverride {
subject: matches.value_of(ARG_SUBJECT),
from: matches.values_of(ARG_FROM).map(Iterator::collect),
to: matches.values_of(ARG_TO).map(Iterator::collect),
cc: matches.values_of(ARG_CC).map(Iterator::collect),
bcc: matches.values_of(ARG_BCC).map(Iterator::collect),
headers: matches.values_of(ARG_HEADERS).map(Iterator::collect),
body: matches.value_of(ARG_BODY),
signature: matches.value_of(ARG_SIGNATURE),
}
}
/// Represents the raw template argument parser.
pub fn parse_raw_arg<'a>(matches: &'a ArgMatches<'a>) -> &'a str {
matches.value_of(ARG_TPL).unwrap_or_default()
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,42 @@
mod add;
mod remove;
mod set;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{add::FlagAddCommand, remove::FlagRemoveCommand, set::FlagSetCommand};
/// 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.
#[derive(Debug, Subcommand)]
pub enum FlagSubcommand {
#[command(arg_required_else_help = true)]
#[command(alias = "create")]
Add(FlagAddCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["update", "change", "replace"])]
Set(FlagSetCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["rm", "delete", "del"])]
Remove(FlagRemoveCommand),
}
impl FlagSubcommand {
#[allow(unused)]
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Add(cmd) => cmd.execute(printer, config).await,
Self::Set(cmd) => cmd.execute(printer, config).await,
Self::Remove(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,25 @@
use clap::Parser;
use std::ops::Deref;
/// The raw message body argument parser.
#[derive(Debug, Parser)]
pub struct MessageRawBodyArg {
/// Prefill the template with a custom body.
#[arg(trailing_var_arg = true)]
#[arg(name = "body_raw", value_name = "BODY")]
pub raw: Vec<String>,
}
impl MessageRawBodyArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
}
}
impl Deref for MessageRawBodyArg {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.raw
}
}

View file

@ -0,0 +1,20 @@
use clap::Parser;
/// The envelope id argument parser.
#[derive(Debug, Parser)]
pub struct HeaderRawArgs {
/// Prefill the template with custom headers.
///
/// A raw header should follow the pattern KEY:VAL.
#[arg(long = "header", short = 'H', required = false)]
#[arg(name = "header-raw", value_name = "KEY:VAL", value_parser = raw_header_parser)]
pub raw: Vec<(String, String)>,
}
pub fn raw_header_parser(raw_header: &str) -> Result<(String, String), String> {
if let Some((key, val)) = raw_header.split_once(':') {
Ok((key.trim().to_owned(), val.trim().to_owned()))
} else {
Err(format!("cannot parse raw header {raw_header:?}"))
}
}

View file

@ -0,0 +1,20 @@
use clap::Parser;
pub mod body;
pub mod header;
pub mod reply;
/// The raw message argument parser.
#[derive(Debug, Parser)]
pub struct MessageRawArg {
/// The raw message, including headers and body.
#[arg(trailing_var_arg = true)]
#[arg(name = "message_raw", value_name = "MESSAGE")]
pub raw: Vec<String>,
}
impl MessageRawArg {
pub fn raw(self) -> String {
self.raw.join(" ").replace('\r', "").replace('\n', "\r\n")
}
}

View file

@ -0,0 +1,12 @@
use clap::Parser;
/// The reply to all argument parser.
#[derive(Debug, Parser)]
pub struct MessageReplyAllArg {
/// Reply to all recipients.
///
/// This argument will add all recipients for the To and Cc
/// headers.
#[arg(long, short = 'A')]
pub all: bool,
}

View file

@ -0,0 +1,105 @@
use clap::Parser;
use color_eyre::{eyre::Context, Result};
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;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// 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.
#[derive(Debug, Parser)]
pub struct AttachmentDownloadCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl AttachmentDownloadCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing download attachment(s) command");
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(), |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 emails = backend.get_messages(folder, ids).await?;
let mut emails_count = 0;
let mut attachments_count = 0;
let mut ids = ids.iter();
for email in emails.to_vec() {
let id = ids.next().unwrap();
let attachments = email.attachments()?;
if attachments.is_empty() {
printer.log(format!("No attachment found for message {id}!\n"))?;
continue;
} else {
emails_count += 1;
}
printer.log(format!(
"{} attachment(s) found for message {id}!\n",
attachments.len()
))?;
for attachment in attachments {
let filename: PathBuf = attachment
.filename
.unwrap_or_else(|| Uuid::new_v4().to_string())
.into();
let filepath = account_config.get_download_file_path(&filename)?;
printer.log(format!("Downloading {:?}\n", filepath))?;
fs::write(&filepath, &attachment.body)
.with_context(|| format!("cannot save attachment at {filepath:?}"))?;
attachments_count += 1;
}
}
match attachments_count {
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

@ -0,0 +1,28 @@
mod download;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::download::AttachmentDownloadCommand;
/// 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, alias = "dl")]
Download(AttachmentDownloadCommand),
}
impl AttachmentSubcommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
match self {
Self::Download(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

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

View file

@ -0,0 +1,69 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Copy the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageCopyCommand {
#[command(flatten)]
pub source_folder: SourceFolderNameOptionalFlag,
#[command(flatten)]
pub target_folder: TargetFolderNameArg,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageCopyCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing copy message(s) command");
let source = &self.source_folder.name;
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(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_copy_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.copy_messages(source, target, ids).await?;
printer.out(format!(
"Message(s) successfully copied from {source} to {target}!\n"
))
}
}

View file

@ -0,0 +1,65 @@
use std::sync::Arc;
use clap::Parser;
use color_eyre::Result;
use email::{backend::feature::BackendFeatureSource, config::Config};
use pimalaya_tui::{
himalaya::backend::BackendBuilder,
terminal::{cli::printer::Printer, config::TomlConfig as _},
};
use tracing::info;
use crate::{
account::arg::name::AccountNameFlag, config::TomlConfig, envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::FolderNameOptionalFlag,
};
/// 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
/// its envelope, otherwise it moves it to the trash folder. Only the
/// expunge folder command truly deletes messages.
#[derive(Debug, Parser)]
pub struct MessageDeleteCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageDeleteCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing delete message(s) command");
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(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_delete_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.delete_messages(folder, ids).await?;
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

@ -0,0 +1,84 @@
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,
message::arg::{body::MessageRawBodyArg, header::HeaderRawArgs},
};
/// 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
/// edition process finishes, you can choose between saving or sending
/// the final message.
#[derive(Debug, Parser)]
pub struct MessageForwardCommand {
#[command(flatten)]
pub folder: FolderNameOptionalFlag,
#[command(flatten)]
pub envelope: EnvelopeIdArg,
#[command(flatten)]
pub headers: HeaderRawArgs,
#[command(flatten)]
pub body: MessageRawBodyArg,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageForwardCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing forward 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)
},
)
.build()
.await?;
let id = self.envelope.id;
let tpl = backend
.get_messages(folder, &[id])
.await?
.first()
.ok_or(eyre!("cannot find message"))?
.to_forward_tpl_builder(account_config.clone())
.with_headers(self.headers.raw)
.with_body(self.body.raw())
.build()
.await?;
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
}
}

View file

@ -0,0 +1,98 @@
use std::sync::Arc;
use clap::Parser;
use color_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 url::Url;
use crate::{account::arg::name::AccountNameFlag, config::TomlConfig};
/// 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
/// $EDITOR. When the edition process finishes, you can choose between
/// saving or sending the final message.
#[derive(Debug, Parser)]
pub struct MessageMailtoCommand {
/// The mailto url.
#[arg()]
pub url: Url,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageMailtoCommand {
pub fn new(url: &str) -> Result<Self> {
Ok(Self {
url: Url::parse(url)?,
account: Default::default(),
})
}
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(), |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)
},
)
.without_sending_backend()
.build()
.await?;
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() {
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");
}
}
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_bytes(msg)
.await?
.into();
editor::edit_tpl_with_editor(account_config, printer, &backend, tpl).await
}
}

View file

@ -0,0 +1,94 @@
pub mod copy;
pub mod delete;
pub mod edit;
pub mod export;
pub mod forward;
pub mod mailto;
pub mod r#move;
pub mod read;
pub mod reply;
pub mod save;
pub mod send;
pub mod thread;
pub mod write;
use clap::Subcommand;
use color_eyre::Result;
use pimalaya_tui::terminal::cli::printer::Printer;
use crate::config::TomlConfig;
use self::{
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,
};
/// 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
/// bottom of the message). Both are separated by two new lines. This
/// subcommand allows you to manage them.
#[derive(Debug, Subcommand)]
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),
Reply(MessageReplyCommand),
#[command(aliases = ["fwd", "fd"])]
Forward(MessageForwardCommand),
Edit(MessageEditCommand),
Mailto(MessageMailtoCommand),
Save(MessageSaveCommand),
Send(MessageSendCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["cpy", "cp"])]
Copy(MessageCopyCommand),
#[command(arg_required_else_help = true)]
#[command(alias = "mv")]
Move(MessageMoveCommand),
#[command(arg_required_else_help = true)]
#[command(aliases = ["remove", "rm"])]
Delete(MessageDeleteCommand),
}
impl MessageSubcommand {
#[allow(unused)]
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,
Self::Copy(cmd) => cmd.execute(printer, config).await,
Self::Move(cmd) => cmd.execute(printer, config).await,
Self::Delete(cmd) => cmd.execute(printer, config).await,
}
}
}

View file

@ -0,0 +1,70 @@
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;
#[allow(unused)]
use crate::{
account::arg::name::AccountNameFlag,
config::TomlConfig,
envelope::arg::ids::EnvelopeIdsArgs,
folder::arg::name::{SourceFolderNameOptionalFlag, TargetFolderNameArg},
};
/// Move the message associated to the given envelope id(s) to the
/// given target folder.
#[derive(Debug, Parser)]
pub struct MessageMoveCommand {
#[command(flatten)]
pub source_folder: SourceFolderNameOptionalFlag,
#[command(flatten)]
pub target_folder: TargetFolderNameArg,
#[command(flatten)]
pub envelopes: EnvelopeIdsArgs,
#[command(flatten)]
pub account: AccountNameFlag,
}
impl MessageMoveCommand {
pub async fn execute(self, printer: &mut impl Printer, config: &TomlConfig) -> Result<()> {
info!("executing move message(s) command");
let source = &self.source_folder.name;
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(), |c: &Config, name| {
c.account(name).ok()
})?;
let backend = BackendBuilder::new(
Arc::new(toml_account_config),
Arc::new(account_config),
|builder| {
builder
.without_features()
.with_move_messages(BackendFeatureSource::Context)
},
)
.without_sending_backend()
.build()
.await?;
backend.move_messages(source, target, ids).await?;
printer.out(format!(
"Message(s) successfully moved from {source} to {target}!\n"
))
}
}

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