From a3686c1c44e4a129f8214eb7e323247fa0c2d0fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Thu, 22 Sep 2022 16:38:38 +0200 Subject: [PATCH] clean config refactor --- CHANGELOG.md | 46 + COPYING | 661 ++++++++++++ Cargo.lock | 422 ++++---- Cargo.toml | 63 +- LICENSE | 32 - README.md | 5 - cli/.gitignore | 1 - cli/Cargo.toml | 59 -- cli/src/compl/mod.rs | 9 - cli/src/config/account.rs | 110 -- cli/src/imap/imap_envelopes.rs | 16 - cli/src/lib.rs | 57 - cli/src/main.rs | 351 ------- cli/src/output/mod.rs | 18 - cli/src/output/output_utils.rs | 41 - cli/src/smtp/mod.rs | 1 - cli/src/smtp/smtp_service.rs | 86 -- cli/src/ui/mod.rs | 9 - flake.nix | 15 +- lib/Cargo.toml | 35 - lib/src/account/account_config.rs | 536 ---------- .../account/deserialized_account_config.rs | 156 --- lib/src/account/deserialized_config.rs | 111 -- lib/src/account/mod.rs | 12 - lib/src/backend/backend.rs | 78 -- lib/src/backend/id_mapper.rs | 131 --- lib/src/backend/imap/error.rs | 86 -- lib/src/backend/imap/imap_backend.rs | 441 -------- lib/src/backend/imap/imap_envelope.rs | 78 -- lib/src/backend/imap/imap_envelopes.rs | 18 - lib/src/backend/imap/imap_flag.rs | 15 - lib/src/backend/imap/imap_flags.rs | 23 - lib/src/backend/imap/msg_sort_criterion.rs | 62 -- lib/src/backend/maildir/error.rs | 49 - lib/src/backend/maildir/maildir_backend.rs | 356 ------- lib/src/backend/maildir/maildir_envelope.rs | 72 -- lib/src/backend/maildir/maildir_envelopes.rs | 21 - lib/src/backend/maildir/maildir_flag.rs | 24 - lib/src/backend/maildir/maildir_flags.rs | 11 - lib/src/backend/mod.rs | 73 -- lib/src/backend/notmuch/error.rs | 49 - lib/src/backend/notmuch/notmuch_backend.rs | 366 ------- lib/src/backend/notmuch/notmuch_envelope.rs | 73 -- lib/src/backend/notmuch/notmuch_envelopes.rs | 16 - lib/src/lib.rs | 6 - lib/src/mbox/mbox.rs | 23 - lib/src/mbox/mboxes.rs | 29 - lib/src/mbox/mod.rs | 9 - lib/src/msg/addr.rs | 67 -- lib/src/msg/envelope.rs | 21 - lib/src/msg/envelopes.rs | 25 - lib/src/msg/error.rs | 56 - lib/src/msg/flag.rs | 27 - lib/src/msg/flags.rs | 88 -- lib/src/msg/mod.rs | 29 - lib/src/msg/msg.rs | 971 ------------------ lib/src/msg/msg_utils.rs | 24 - lib/src/msg/parts.rs | 150 --- lib/src/msg/tpl.rs | 15 - lib/src/process.rs | 34 - .../emails/alice-to-patrick-encrypted.eml | 19 - lib/tests/emails/alice-to-patrick.eml | 7 - lib/tests/keys/alice.asc | 81 -- lib/tests/keys/alice.pub.asc | 41 - lib/tests/keys/patrick.asc | 81 -- lib/tests/keys/patrick.pub.asc | 41 - lib/tests/test_imap_backend.rs | 77 -- lib/tests/test_maildir_backend.rs | 91 -- lib/tests/test_notmuch_backend.rs | 86 -- rustfmt.toml | 74 -- .../compl/compl_args.rs => src/compl/args.rs | 0 .../compl/handlers.rs | 0 src/compl/mod.rs | 8 + .../config_args.rs => src/config/args.rs | 0 src/config/config.rs | 576 +++++++++++ src/config/mod.rs | 5 + src/config/prelude.rs | 139 +++ src/domain/account/account.rs | 54 + src/domain/account/accounts.rs | 61 ++ .../domain/account/args.rs | 4 +- src/domain/account/config.rs | 224 ++++ .../domain/account/handlers.rs | 48 +- src/domain/account/mod.rs | 9 + .../msg_args.rs => src/domain/email/args.rs | 43 +- .../domain/email/handlers.rs | 194 ++-- src/domain/email/mod.rs | 2 + .../msg => src/domain/envelope}/envelope.rs | 2 +- .../msg => src/domain/envelope}/envelopes.rs | 4 +- src/domain/envelope/mod.rs | 5 + .../flag_args.rs => src/domain/flag/args.rs | 8 +- .../domain/flag/handlers.rs | 44 +- src/domain/flag/mod.rs | 2 + .../mbox_args.rs => src/domain/folder/args.rs | 30 +- .../mbox.rs => src/domain/folder/folder.rs | 4 +- .../mboxes.rs => src/domain/folder/folders.rs | 6 +- .../domain/folder/handlers.rs | 66 +- src/domain/folder/mod.rs | 8 + .../imap_args.rs => src/domain/imap/args.rs | 0 .../domain/imap/handlers.rs | 2 +- src/domain/imap/mod.rs | 2 + src/domain/mod.rs | 17 + .../msg/tpl_args.rs => src/domain/tpl/args.rs | 14 +- .../domain/tpl/handlers.rs | 62 +- src/domain/tpl/mod.rs | 2 + src/lib.rs | 8 + src/main.rs | 320 ++++++ .../output_args.rs => src/output/args.rs | 0 src/output/mod.rs | 4 + .../output_entity.rs => src/output/output.rs | 0 src/printer/mod.rs | 7 + {cli/src/output => src/printer}/print.rs | 2 +- .../src/output => src/printer}/print_table.rs | 4 +- .../printer/printer.rs | 72 +- {cli/src => src}/ui/choice.rs | 0 {cli/src => src}/ui/editor.rs | 58 +- src/ui/mod.rs | 5 + .../ui/table_arg.rs => src/ui/table/args.rs | 0 src/ui/table/mod.rs | 4 + {cli/src/ui => src/ui/table}/table.rs | 14 +- vim/README.md | 175 ---- vim/autoload/himalaya/account.vim | 11 - vim/autoload/himalaya/mbox.vim | 84 -- vim/autoload/himalaya/msg.vim | 415 -------- vim/autoload/himalaya/request.vim | 59 -- vim/autoload/himalaya/shared/bindings.vim | 11 - vim/autoload/himalaya/shared/cli.vim | 26 - vim/autoload/himalaya/shared/log.vim | 10 - vim/autoload/himalaya/shared/thread.vim | 3 - vim/autoload/himalaya/shared/utils.vim | 30 - vim/doc/.gitignore | 1 - vim/doc/himalaya.txt | 126 --- vim/ftplugin/himalaya-msg-list.vim | 20 - vim/ftplugin/himalaya-msg-read.vim | 17 - vim/ftplugin/himalaya-msg-write.vim | 18 - vim/lua/himalaya/mbox.lua | 58 -- vim/plugin/himalaya.vim | 19 - vim/syntax/himalaya-msg-list.vim | 26 - 137 files changed, 2772 insertions(+), 7546 deletions(-) create mode 100644 COPYING delete mode 100644 LICENSE delete mode 100644 cli/.gitignore delete mode 100644 cli/Cargo.toml delete mode 100644 cli/src/compl/mod.rs delete mode 100644 cli/src/config/account.rs delete mode 100644 cli/src/imap/imap_envelopes.rs delete mode 100644 cli/src/lib.rs delete mode 100644 cli/src/main.rs delete mode 100644 cli/src/output/mod.rs delete mode 100644 cli/src/output/output_utils.rs delete mode 100644 cli/src/smtp/mod.rs delete mode 100644 cli/src/smtp/smtp_service.rs delete mode 100644 cli/src/ui/mod.rs delete mode 100644 lib/Cargo.toml delete mode 100644 lib/src/account/account_config.rs delete mode 100644 lib/src/account/deserialized_account_config.rs delete mode 100644 lib/src/account/deserialized_config.rs delete mode 100644 lib/src/account/mod.rs delete mode 100644 lib/src/backend/backend.rs delete mode 100644 lib/src/backend/id_mapper.rs delete mode 100644 lib/src/backend/imap/error.rs delete mode 100644 lib/src/backend/imap/imap_backend.rs delete mode 100644 lib/src/backend/imap/imap_envelope.rs delete mode 100644 lib/src/backend/imap/imap_envelopes.rs delete mode 100644 lib/src/backend/imap/imap_flag.rs delete mode 100644 lib/src/backend/imap/imap_flags.rs delete mode 100644 lib/src/backend/imap/msg_sort_criterion.rs delete mode 100644 lib/src/backend/maildir/error.rs delete mode 100644 lib/src/backend/maildir/maildir_backend.rs delete mode 100644 lib/src/backend/maildir/maildir_envelope.rs delete mode 100644 lib/src/backend/maildir/maildir_envelopes.rs delete mode 100644 lib/src/backend/maildir/maildir_flag.rs delete mode 100644 lib/src/backend/maildir/maildir_flags.rs delete mode 100644 lib/src/backend/mod.rs delete mode 100644 lib/src/backend/notmuch/error.rs delete mode 100644 lib/src/backend/notmuch/notmuch_backend.rs delete mode 100644 lib/src/backend/notmuch/notmuch_envelope.rs delete mode 100644 lib/src/backend/notmuch/notmuch_envelopes.rs delete mode 100644 lib/src/lib.rs delete mode 100644 lib/src/mbox/mbox.rs delete mode 100644 lib/src/mbox/mboxes.rs delete mode 100644 lib/src/mbox/mod.rs delete mode 100644 lib/src/msg/addr.rs delete mode 100644 lib/src/msg/envelope.rs delete mode 100644 lib/src/msg/envelopes.rs delete mode 100644 lib/src/msg/error.rs delete mode 100644 lib/src/msg/flag.rs delete mode 100644 lib/src/msg/flags.rs delete mode 100644 lib/src/msg/mod.rs delete mode 100644 lib/src/msg/msg.rs delete mode 100644 lib/src/msg/msg_utils.rs delete mode 100644 lib/src/msg/parts.rs delete mode 100644 lib/src/msg/tpl.rs delete mode 100644 lib/src/process.rs delete mode 100644 lib/tests/emails/alice-to-patrick-encrypted.eml delete mode 100644 lib/tests/emails/alice-to-patrick.eml delete mode 100644 lib/tests/keys/alice.asc delete mode 100644 lib/tests/keys/alice.pub.asc delete mode 100644 lib/tests/keys/patrick.asc delete mode 100644 lib/tests/keys/patrick.pub.asc delete mode 100644 lib/tests/test_imap_backend.rs delete mode 100644 lib/tests/test_maildir_backend.rs delete mode 100644 lib/tests/test_notmuch_backend.rs delete mode 100644 rustfmt.toml rename cli/src/compl/compl_args.rs => src/compl/args.rs (100%) rename cli/src/compl/compl_handlers.rs => src/compl/handlers.rs (100%) create mode 100644 src/compl/mod.rs rename cli/src/config/config_args.rs => src/config/args.rs (100%) create mode 100644 src/config/config.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/prelude.rs create mode 100644 src/domain/account/account.rs create mode 100644 src/domain/account/accounts.rs rename cli/src/config/account_args.rs => src/domain/account/args.rs (95%) create mode 100644 src/domain/account/config.rs rename cli/src/config/account_handlers.rs => src/domain/account/handlers.rs (72%) create mode 100644 src/domain/account/mod.rs rename cli/src/msg/msg_args.rs => src/domain/email/args.rs (94%) rename cli/src/msg/msg_handlers.rs => src/domain/email/handlers.rs (63%) create mode 100644 src/domain/email/mod.rs rename {cli/src/msg => src/domain/envelope}/envelope.rs (96%) rename {cli/src/msg => src/domain/envelope}/envelopes.rs (77%) create mode 100644 src/domain/envelope/mod.rs rename cli/src/msg/flag_args.rs => src/domain/flag/args.rs (95%) rename cli/src/msg/flag_handlers.rs => src/domain/flag/handlers.rs (59%) create mode 100644 src/domain/flag/mod.rs rename cli/src/mbox/mbox_args.rs => src/domain/folder/args.rs (83%) rename cli/src/mbox/mbox.rs => src/domain/folder/folder.rs (89%) rename cli/src/mbox/mboxes.rs => src/domain/folder/folders.rs (68%) rename cli/src/mbox/mbox_handlers.rs => src/domain/folder/handlers.rs (69%) create mode 100644 src/domain/folder/mod.rs rename cli/src/imap/imap_args.rs => src/domain/imap/args.rs (100%) rename cli/src/imap/imap_handlers.rs => src/domain/imap/handlers.rs (91%) create mode 100644 src/domain/imap/mod.rs create mode 100644 src/domain/mod.rs rename cli/src/msg/tpl_args.rs => src/domain/tpl/args.rs (95%) rename cli/src/msg/tpl_handlers.rs => src/domain/tpl/handlers.rs (59%) create mode 100644 src/domain/tpl/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs rename cli/src/output/output_args.rs => src/output/args.rs (100%) create mode 100644 src/output/mod.rs rename cli/src/output/output_entity.rs => src/output/output.rs (100%) create mode 100644 src/printer/mod.rs rename {cli/src/output => src/printer}/print.rs (93%) rename {cli/src/output => src/printer}/print_table.rs (81%) rename cli/src/output/printer_service.rs => src/printer/printer.rs (82%) rename {cli/src => src}/ui/choice.rs (100%) rename {cli/src => src}/ui/editor.rs (66%) create mode 100644 src/ui/mod.rs rename cli/src/ui/table_arg.rs => src/ui/table/args.rs (100%) create mode 100644 src/ui/table/mod.rs rename {cli/src/ui => src/ui/table}/table.rs (96%) delete mode 100644 vim/README.md delete mode 100644 vim/autoload/himalaya/account.vim delete mode 100644 vim/autoload/himalaya/mbox.vim delete mode 100644 vim/autoload/himalaya/msg.vim delete mode 100644 vim/autoload/himalaya/request.vim delete mode 100644 vim/autoload/himalaya/shared/bindings.vim delete mode 100644 vim/autoload/himalaya/shared/cli.vim delete mode 100644 vim/autoload/himalaya/shared/log.vim delete mode 100644 vim/autoload/himalaya/shared/thread.vim delete mode 100644 vim/autoload/himalaya/shared/utils.vim delete mode 100644 vim/doc/.gitignore delete mode 100644 vim/doc/himalaya.txt delete mode 100644 vim/ftplugin/himalaya-msg-list.vim delete mode 100644 vim/ftplugin/himalaya-msg-read.vim delete mode 100644 vim/ftplugin/himalaya-msg-write.vim delete mode 100644 vim/lua/himalaya/mbox.lua delete mode 100644 vim/plugin/himalaya.vim delete mode 100644 vim/syntax/himalaya-msg-list.vim diff --git a/CHANGELOG.md b/CHANGELOG.md index f08f0a7..6650912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.0] - 2022-09-19 + +- Separate the CLI from the lib module [#340] + +The source code has been splitted into subrepositories: + +1. The email logic has been extracted from the CLI and placed in a lib + on [sourcehut](https://git.sr.ht/~soywod/himalaya-lib) +2. The vim plugin is now in a dedicated repository on + [sourcehut](https://git.sr.ht/~soywod/himalaya-vim) as well +3. This repository only contains the CLI source code (it was not + possible to move it to sourcehut because of cross platform builds) + +- [**BREAKING**] Refactor config system [#344] + +The configuration has been rethought in order to be more intuitive and +structured. Here are the breaking changes for the global config: + +- `name` becomes `display-name` and is not mandatory anymore +- `signature-delimiter` becomes `signature-delim` +- `default-page-size` has been moved to `folder-listing-page-size` and + `email-listing-page-size` +- `notify-cmd`, `notify-query` and `watch-cmds` have been removed from + the global config (available in account config only) +- `folder-aliases` has been added to the global config (previously + known as `mailboxes` from the account config) +- `email-reading-headers`, `email-reading-format`, + `email-reading-decrypt-cmd`, `email-writing-encrypt-cmd` and + `email-hooks` have been added + +The account config inherits the same breaking changes from the global +config plus: + +- `imap-*` requires `backend = "imap"` +- `maildir-*` requires `backend = "maildir"` +- `notmuch-*` requires `backend = "notmuch"` +- `smtp-*` requires `sender = "internal"` +- `pgp-encrypt-cmd` becomes `email-writing-encrypt-cmd` +- `pgp-decrypt-cmd` becomes `email-reading-decrypt-cmd` +- `mailboxes` becomes `folder-aliases` +- `hooks` becomes `email-hooks` +- `maildir-dir` becomes `maildir-root-dir` +- `notmuch-database-dir` becomes `notmuch-db-path` + ## [0.5.10] - 2022-03-20 ### Fixed @@ -517,4 +561,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#334]: https://github.com/soywod/himalaya/issues/334 [#335]: https://github.com/soywod/himalaya/issues/335 [#338]: https://github.com/soywod/himalaya/issues/338 +[#340]: https://github.com/soywod/himalaya/issues/340 +[#344]: https://github.com/soywod/himalaya/issues/344 [#346]: https://github.com/soywod/himalaya/issues/346 diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..bae94e1 --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index adda362..8f77630 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,19 +8,18 @@ version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ - "memchr 2.4.1", + "memchr 2.5.0", ] [[package]] name = "ammonia" -version = "3.1.4" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9f21d23d82bae9d33c21080572af1fa749788e68234b5d8fa5e39d3e0783ed" +checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74" dependencies = [ "html5ever", - "lazy_static", "maplit", - "markup5ever_rcdom", + "once_cell", "tendril", "url", ] @@ -36,9 +35,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.56" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "atty" @@ -237,12 +236,12 @@ dependencies = [ [[package]] name = "email-encoding" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b91dddc343e7eaa27f9764e5bffe57370d957017fdd75244f5045e829a8441" +checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98" dependencies = [ "base64", - "memchr 2.4.1", + "memchr 2.5.0", ] [[package]] @@ -253,9 +252,9 @@ checksum = "8684b7c9cb4857dfa1e5b9629ef584ba618c9b93bae60f58cb23f4f271d0468e" [[package]] name = "encoding_rs" -version = "0.8.30" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ "cfg-if 1.0.0", ] @@ -275,9 +274,9 @@ dependencies = [ [[package]] name = "erased-serde" -version = "0.3.18" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56047058e1ab118075ca22f9ecd737bcc961aa3566a3019cb71388afa280bd8a" +checksum = "81d013529d5574a60caeda29e179e695125448e5de52e3874f7b4c1d7360e18e" dependencies = [ "serde", ] @@ -392,7 +391,7 @@ dependencies = [ "futures-core", "futures-io", "futures-task", - "memchr 2.4.1", + "memchr 2.5.0", "pin-project-lite", "pin-utils", "slab", @@ -410,31 +409,20 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.16" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.9.0+wasi-snapshot-preview1", -] - -[[package]] -name = "getrandom" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" -dependencies = [ - "cfg-if 1.0.0", - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" [[package]] name = "hermit-abi" @@ -447,7 +435,7 @@ dependencies = [ [[package]] name = "himalaya" -version = "0.5.10" +version = "0.6.0" dependencies = [ "ammonia", "anyhow", @@ -473,6 +461,7 @@ dependencies = [ "serde", "serde_json", "shellexpand", + "tempfile", "termcolor", "terminal_size", "toml", @@ -522,18 +511,18 @@ dependencies = [ [[package]] name = "html-escape" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "816ea801a95538fc5f53c836697b3f8b64a9d664c4f0b91efe1fe7c92e4dbcb7" +checksum = "b8e7479fa1ef38eb49fb6a42c426be515df2d063f06cb8efd3e50af073dbc26c" dependencies = [ "utf8-width", ] [[package]] name = "html5ever" -version = "0.25.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aafcf38a1a36118242d29b92e1b08ef84e67e4a5ed06e0a80be20e6a32bfed6b" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", @@ -599,9 +588,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -618,9 +607,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "lazy_static" @@ -653,9 +642,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.120" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad5c14e80759d0939d013e6ca49930e59fc53dd8e5009132f76240c179380c09" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "lock_api" @@ -668,18 +657,19 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", ] @@ -719,9 +709,9 @@ checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "markup5ever" -version = "0.10.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24f40fb03852d1cdd84330cddcaf98e9ec08a7b7768e952fad3b4cf048ec8fd" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", "phf", @@ -731,18 +721,6 @@ dependencies = [ "tendril", ] -[[package]] -name = "markup5ever_rcdom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f015da43bcd8d4f144559a3423f4591d69b8ce0652c905374da7205df336ae2b" -dependencies = [ - "html5ever", - "markup5ever", - "tendril", - "xml5ever", -] - [[package]] name = "match_cfg" version = "0.1.0" @@ -772,9 +750,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" @@ -790,9 +768,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "native-tls" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" dependencies = [ "lazy_static", "libc", @@ -829,7 +807,7 @@ checksum = "e7413f999671bd4745a7b624bd370a569fb6bc574b23c83a3c5ed2e453f3d5e2" dependencies = [ "bitvec", "funty", - "memchr 2.4.1", + "memchr 2.5.0", "version_check", ] @@ -839,7 +817,7 @@ version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "memchr 2.4.1", + "memchr 2.5.0", "minimal-lexical", ] @@ -855,9 +833,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -865,33 +843,45 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "openssl" -version = "0.10.38" +version = "0.10.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" dependencies = [ "bitflags", "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", + "openssl-macros", "openssl-sys", ] +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -900,9 +890,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.72" +version = "0.9.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" dependencies = [ "autocfg", "cc", @@ -923,13 +913,12 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.11.2" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "instant", - "lock_api 0.4.6", - "parking_lot_core 0.8.5", + "lock_api 0.4.7", + "parking_lot_core 0.9.3", ] [[package]] @@ -948,16 +937,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "cfg-if 1.0.0", - "instant", "libc", - "redox_syscall 0.2.11", + "redox_syscall 0.2.13", "smallvec", - "winapi", + "windows-sys", ] [[package]] @@ -978,42 +966,33 @@ dependencies = [ [[package]] name = "phf" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" dependencies = [ - "phf_shared 0.8.0", + "phf_shared", ] [[package]] name = "phf_codegen" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" dependencies = [ "phf_generator", - "phf_shared 0.8.0", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.8.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" dependencies = [ - "phf_shared 0.8.0", + "phf_shared", "rand", ] -[[package]] -name = "phf_shared" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - [[package]] name = "phf_shared" version = "0.10.0" @@ -1025,9 +1004,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1037,9 +1016,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "ppv-lite86" @@ -1055,18 +1034,18 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "proc-macro2" -version = "1.0.36" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.15" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -1085,23 +1064,20 @@ checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8" [[package]] name = "rand" -version = "0.7.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "getrandom 0.1.16", "libc", "rand_chacha", "rand_core", - "rand_hc", - "rand_pcg", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1109,29 +1085,11 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.1.16", -] - -[[package]] -name = "rand_hc" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" -dependencies = [ - "rand_core", -] - -[[package]] -name = "rand_pcg" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" -dependencies = [ - "rand_core", + "getrandom", ] [[package]] @@ -1142,39 +1100,40 @@ checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" [[package]] name = "redox_syscall" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ - "getrandom 0.2.5", - "redox_syscall 0.2.11", + "getrandom", + "redox_syscall 0.2.13", + "thiserror", ] [[package]] name = "regex" -version = "1.5.5" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", - "memchr 2.4.1", + "memchr 2.5.0", "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -1198,18 +1157,18 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "winapi", + "windows-sys", ] [[package]] @@ -1243,18 +1202,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.136" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.136" +version = "1.0.138" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" dependencies = [ "proc-macro2", "quote", @@ -1263,9 +1222,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "itoa", "ryu", @@ -1289,15 +1248,15 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" @@ -1311,26 +1270,26 @@ dependencies = [ [[package]] name = "string_cache" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" dependencies = [ - "lazy_static", "new_debug_unreachable", - "parking_lot 0.11.2", - "phf_shared 0.10.0", + "once_cell", + "parking_lot 0.12.1", + "phf_shared", "precomputed-hash", "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f24c8e5e19d22a726626f1a5e16fe15b132dcf21d10177fa5a45ce7962996b97" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ "phf_generator", - "phf_shared 0.8.0", + "phf_shared", "proc-macro2", "quote", ] @@ -1349,13 +1308,13 @@ checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" [[package]] name = "syn" -version = "1.0.88" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd69e719f31e88618baa1eaa6ee2de5c9a1c004f1e9ecdb58e8352a13f20a01" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -1373,16 +1332,16 @@ dependencies = [ "cfg-if 1.0.0", "fastrand", "libc", - "redox_syscall 0.2.11", + "redox_syscall 0.2.13", "remove_dir_all", "winapi", ] [[package]] name = "tendril" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" dependencies = [ "futf", "mac", @@ -1450,9 +1409,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -1465,9 +1424,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "toml" -version = "0.5.8" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] @@ -1487,15 +1446,21 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -1506,12 +1471,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - [[package]] name = "url" version = "2.2.2" @@ -1532,9 +1491,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cf7d77f457ef8dfa11e4cd5933c5ddb5dc52a94664071951219a97710f0a32b" +checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1" [[package]] name = "uuid" @@ -1542,7 +1501,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.5", + "getrandom", ] [[package]] @@ -1557,18 +1516,18 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" -[[package]] -name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" - [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "winapi" version = "0.3.9" @@ -1600,20 +1559,51 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "wyz" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - -[[package]] -name = "xml5ever" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9234163818fd8e2418fcde330655e757900d4236acd8cc70fef345ef91f6d865" -dependencies = [ - "log", - "mac", - "markup5ever", - "time", -] diff --git a/Cargo.toml b/Cargo.toml index de6b2bf..98bb1ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,61 @@ -[workspace] -members = ["lib", "cli"] +[package] +name = "himalaya" +description = "Command-line interface for email management." +version = "0.6.0" +authors = ["soywod "] +edition = "2021" +license-file = "COPYING" +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" + +[package.metadata.deb] +priority = "optional" +section = "mail" + +[features] +imap-backend = ["imap", "imap-proto"] +maildir-backend = ["maildir", "md5"] +notmuch-backend = ["notmuch", "maildir-backend"] +default = ["imap-backend", "maildir-backend", "notmuch-backend"] + +[dev-dependencies] +tempfile = "3.3.0" + +[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.1.0", features = ["imap-backend"], path = "../../sourcehut/himalaya-lib" } +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" +uuid = { version = "0.8", features = ["v4"] } + +# Optional dependencies: + +imap = { version = "=3.0.0-alpha.4", optional = true } +imap-proto = { version = "0.14.3", optional = true } +maildir = { version = "0.6.1", optional = true } +md5 = { version = "0.7.0", optional = true } +notmuch = { version = "0.7.1", optional = true } diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e32e403..0000000 --- a/LICENSE +++ /dev/null @@ -1,32 +0,0 @@ -Copyright (c) 2020-2021, soywod (Clément DOUIN) - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. All advertising materials mentioning features or use of this software must - display the following acknowledgement: - This product includes software developed by Clément DOUIN. - -4. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, -PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR -OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF -ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 41ec1c3..b7b71dc 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,3 @@ -**Himalaya receives financial support from the -[NLnet](https://nlnet.nl/project/Himalaya/) foundation! 🤯✨🌈** - -*See the [discussion](https://github.com/soywod/himalaya/discussions/399) for more information.* - # 📫 Himalaya Command-line interface for email management diff --git a/cli/.gitignore b/cli/.gitignore deleted file mode 100644 index 03314f7..0000000 --- a/cli/.gitignore +++ /dev/null @@ -1 +0,0 @@ -Cargo.lock diff --git a/cli/Cargo.toml b/cli/Cargo.toml deleted file mode 100644 index 25b638c..0000000 --- a/cli/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "himalaya" -description = "Command-line interface for email management" -version = "0.5.10" -authors = ["soywod "] -edition = "2018" -license-file = "../LICENSE" -readme = "../README.md" -categories = ["command-line-interface", "command-line-utilities", "email"] -keywords = ["cli", "mail", "email", "client", "imap"] -homepage = "https://github.com/soywod/himalaya/wiki" -documentation = "https://github.com/soywod/himalaya/wiki" -repository = "https://github.com/soywod/himalaya" - -[package.metadata.deb] -priority = "optional" -section = "mail" - -[features] -imap-backend = ["imap", "imap-proto"] -maildir-backend = ["maildir", "md5"] -notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend"] - -[dependencies] -ammonia = "3.1.2" -anyhow = "1.0.44" -atty = "0.2.14" -chrono = "0.4.19" -clap = { version = "2.33.3", default-features = false, features = ["suggestions", "color"] } -convert_case = "0.5.0" -env_logger = "0.8.3" -erased-serde = "0.3.18" -himalaya-lib = { path = "../lib" } -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" -uuid = { version = "0.8", features = ["v4"] } - -# Optional dependencies: - -imap = { version = "=3.0.0-alpha.4", optional = true } -imap-proto = { version = "0.14.3", optional = true } -maildir = { version = "0.6.1", optional = true } -md5 = { version = "0.7.0", optional = true } -notmuch = { version = "0.7.1", optional = true } diff --git a/cli/src/compl/mod.rs b/cli/src/compl/mod.rs deleted file mode 100644 index 2f0876d..0000000 --- a/cli/src/compl/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Module related to shell completion. -//! -//! This module allows users to generate autocompletion scripts for their shells. You can see the -//! list of available shells directly on the [clap's docs.rs website]. -//! -//! [clap's docs.rs website]: https://docs.rs/clap/2.33.3/clap/enum.Shell.html - -pub mod compl_args; -pub mod compl_handlers; diff --git a/cli/src/config/account.rs b/cli/src/config/account.rs deleted file mode 100644 index 3a11deb..0000000 --- a/cli/src/config/account.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Account module. -//! -//! This module contains the definition of the printable account, -//! which is only used by the "accounts" command to list all available -//! accounts from the config file. - -use anyhow::Result; -use serde::Serialize; -use std::{ - collections::hash_map::Iter, - fmt::{self, Display}, - ops::Deref, -}; - -use himalaya_lib::account::DeserializedAccountConfig; - -use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, - ui::{Cell, Row, Table}, -}; - -/// Represents the list of printable accounts. -#[derive(Debug, Default, Serialize)] -pub struct Accounts(pub Vec); - -impl Deref for Accounts { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl PrintTable for Accounts { - fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { - writeln!(writer)?; - Table::print(writer, self, opts)?; - writeln!(writer)?; - Ok(()) - } -} - -/// Represents the printable account. -#[derive(Debug, Default, PartialEq, Eq, Serialize)] -pub struct Account { - /// Represents the account name. - pub name: String, - - /// Represents the backend name of the account. - pub backend: String, - - /// Represents the default state of the account. - pub default: bool, -} - -impl Account { - pub fn new(name: &str, backend: &str, default: bool) -> Self { - Self { - name: name.into(), - backend: backend.into(), - default, - } - } -} - -impl Display for Account { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -impl Table for Account { - fn head() -> Row { - Row::new() - .cell(Cell::new("NAME").shrinkable().bold().underline().white()) - .cell(Cell::new("BACKEND").bold().underline().white()) - .cell(Cell::new("DEFAULT").bold().underline().white()) - } - - fn row(&self) -> Row { - let default = if self.default { "yes" } else { "" }; - Row::new() - .cell(Cell::new(&self.name).shrinkable().green()) - .cell(Cell::new(&self.backend).blue()) - .cell(Cell::new(default).white()) - } -} - -impl From> for Accounts { - fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self { - let mut accounts: Vec<_> = map - .map(|(name, config)| match config { - #[cfg(feature = "imap-backend")] - DeserializedAccountConfig::Imap(config) => { - Account::new(name, "imap", config.default.unwrap_or_default()) - } - #[cfg(feature = "maildir-backend")] - DeserializedAccountConfig::Maildir(config) => { - Account::new(name, "maildir", config.default.unwrap_or_default()) - } - #[cfg(feature = "notmuch-backend")] - DeserializedAccountConfig::Notmuch(config) => { - Account::new(name, "notmuch", config.default.unwrap_or_default()) - } - }) - .collect(); - accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); - Self(accounts) - } -} diff --git a/cli/src/imap/imap_envelopes.rs b/cli/src/imap/imap_envelopes.rs deleted file mode 100644 index 8095b9b..0000000 --- a/cli/src/imap/imap_envelopes.rs +++ /dev/null @@ -1,16 +0,0 @@ -use anyhow::{Context, Result}; -use himalaya_lib::{ - backend::{from_imap_fetch, ImapFetch}, - msg::Envelopes, -}; - -/// Represents the list of raw envelopes returned by the `imap` crate. -pub type ImapFetches = imap::types::ZeroCopy>; - -pub fn from_imap_fetches(fetches: ImapFetches) -> Result { - let mut envelopes = Envelopes::default(); - for fetch in fetches.iter().rev() { - envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?); - } - Ok(envelopes) -} diff --git a/cli/src/lib.rs b/cli/src/lib.rs deleted file mode 100644 index 9fefb19..0000000 --- a/cli/src/lib.rs +++ /dev/null @@ -1,57 +0,0 @@ -pub mod mbox { - pub mod mbox; - pub use mbox::*; - - pub mod mboxes; - pub use mboxes::*; - - pub mod mbox_args; - pub mod mbox_handlers; -} - -#[cfg(feature = "imap-backend")] -pub mod imap { - pub mod imap_args; - pub mod imap_handlers; - - pub mod imap_envelopes; - pub use imap_envelopes::*; -} - -pub mod msg { - pub mod envelope; - pub use envelope::*; - - pub mod envelopes; - pub use envelopes::*; - - pub mod msg_args; - - pub mod msg_handlers; - - pub mod flag_args; - pub mod flag_handlers; - - pub mod tpl_args; - - pub mod tpl_handlers; -} - -pub mod smtp { - pub mod smtp_service; - pub use smtp_service::*; -} - -pub mod config { - pub mod config_args; - - pub mod account_args; - pub mod account_handlers; - - pub mod account; - pub use account::*; -} - -pub mod compl; -pub mod output; -pub mod ui; diff --git a/cli/src/main.rs b/cli/src/main.rs deleted file mode 100644 index e81924f..0000000 --- a/cli/src/main.rs +++ /dev/null @@ -1,351 +0,0 @@ -use anyhow::{Context, Result}; -use himalaya_lib::{ - account::{Account, BackendConfig, DeserializedConfig, DEFAULT_INBOX_FOLDER}, - backend::Backend, -}; -use std::{convert::TryFrom, env}; -use url::Url; - -use himalaya::{ - compl::{compl_args, compl_handlers}, - config::{account_args, account_handlers, config_args}, - mbox::{mbox_args, mbox_handlers}, - msg::{flag_args, flag_handlers, msg_args, msg_handlers, tpl_args, tpl_handlers}, - output::{output_args, OutputFmt, StdoutPrinter}, - smtp::LettreService, -}; - -#[cfg(feature = "imap-backend")] -use himalaya::imap::{imap_args, imap_handlers}; - -#[cfg(feature = "imap-backend")] -use himalaya_lib::backend::ImapBackend; - -#[cfg(feature = "maildir-backend")] -use himalaya_lib::backend::MaildirBackend; - -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::{account::MaildirBackendConfig, backend::NotmuchBackend}; - -fn create_app<'a>() -> clap::App<'a, 'a> { - let app = clap::App::new(env!("CARGO_PKG_NAME")) - .version(env!("CARGO_PKG_VERSION")) - .about(env!("CARGO_PKG_DESCRIPTION")) - .author(env!("CARGO_PKG_AUTHORS")) - .global_setting(clap::AppSettings::GlobalVersion) - .arg(&config_args::path_arg()) - .arg(&account_args::name_arg()) - .args(&output_args::args()) - .arg(mbox_args::source_arg()) - .subcommands(compl_args::subcmds()) - .subcommands(account_args::subcmds()) - .subcommands(mbox_args::subcmds()) - .subcommands(msg_args::subcmds()); - - #[cfg(feature = "imap-backend")] - let app = app.subcommands(imap_args::subcmds()); - - app -} - -#[allow(clippy::single_match)] -fn main() -> Result<()> { - let default_env_filter = env_logger::DEFAULT_FILTER_ENV; - env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); - - // Check mailto command BEFORE app initialization. - let raw_args: Vec = env::args().collect(); - if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { - let config = DeserializedConfig::from_opt_path(None)?; - let (account_config, backend_config) = - Account::from_config_and_opt_account_name(&config, None)?; - let mut printer = StdoutPrinter::from(OutputFmt::Plain); - let url = Url::parse(&raw_args[1])?; - let mut smtp = LettreService::from(&account_config); - - #[cfg(feature = "imap-backend")] - let mut imap; - - #[cfg(feature = "maildir-backend")] - let mut maildir; - - #[cfg(feature = "notmuch-backend")] - let maildir_config: MaildirBackendConfig; - #[cfg(feature = "notmuch-backend")] - let mut notmuch; - - let backend: Box<&mut dyn Backend> = match backend_config { - #[cfg(feature = "imap-backend")] - BackendConfig::Imap(ref imap_config) => { - imap = ImapBackend::new(&account_config, imap_config); - Box::new(&mut imap) - } - #[cfg(feature = "maildir-backend")] - BackendConfig::Maildir(ref maildir_config) => { - maildir = MaildirBackend::new(&account_config, maildir_config); - Box::new(&mut maildir) - } - #[cfg(feature = "notmuch-backend")] - BackendConfig::Notmuch(ref notmuch_config) => { - maildir_config = MaildirBackendConfig { - maildir_dir: notmuch_config.notmuch_database_dir.clone(), - }; - maildir = MaildirBackend::new(&account_config, &maildir_config); - notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?; - Box::new(&mut notmuch) - } - }; - - return msg_handlers::mailto(&url, &account_config, &mut printer, backend, &mut smtp); - } - - let app = create_app(); - let m = app.get_matches(); - - // Check completion command BEFORE entities and services initialization. - // Related issue: https://github.com/soywod/himalaya/issues/115. - match compl_args::matches(&m)? { - Some(compl_args::Command::Generate(shell)) => { - return compl_handlers::generate(create_app(), shell); - } - _ => (), - } - - // Init entities and services. - let config = DeserializedConfig::from_opt_path(m.value_of("config"))?; - let (account_config, backend_config) = - Account::from_config_and_opt_account_name(&config, m.value_of("account"))?; - let mbox = m - .value_of("mbox-source") - .or_else(|| account_config.mailboxes.get("inbox").map(|s| s.as_str())) - .unwrap_or(DEFAULT_INBOX_FOLDER); - let mut printer = StdoutPrinter::try_from(m.value_of("output"))?; - #[cfg(feature = "imap-backend")] - let mut imap; - - #[cfg(feature = "maildir-backend")] - let mut maildir; - - #[cfg(feature = "notmuch-backend")] - let maildir_config: MaildirBackendConfig; - #[cfg(feature = "notmuch-backend")] - let mut notmuch; - - let backend: Box<&mut dyn Backend> = match backend_config { - #[cfg(feature = "imap-backend")] - BackendConfig::Imap(ref imap_config) => { - imap = ImapBackend::new(&account_config, imap_config); - Box::new(&mut imap) - } - #[cfg(feature = "maildir-backend")] - BackendConfig::Maildir(ref maildir_config) => { - maildir = MaildirBackend::new(&account_config, maildir_config); - Box::new(&mut maildir) - } - #[cfg(feature = "notmuch-backend")] - BackendConfig::Notmuch(ref notmuch_config) => { - maildir_config = MaildirBackendConfig { - maildir_dir: notmuch_config.notmuch_database_dir.clone(), - }; - maildir = MaildirBackend::new(&account_config, &maildir_config); - notmuch = NotmuchBackend::new(&account_config, notmuch_config, &mut maildir)?; - Box::new(&mut notmuch) - } - }; - - let mut smtp = LettreService::from(&account_config); - - // Check IMAP commands. - #[allow(irrefutable_let_patterns)] - #[cfg(feature = "imap-backend")] - if let BackendConfig::Imap(ref imap_config) = backend_config { - let mut imap = ImapBackend::new(&account_config, imap_config); - match imap_args::matches(&m)? { - Some(imap_args::Command::Notify(keepalive)) => { - return imap_handlers::notify(keepalive, mbox, &mut imap); - } - Some(imap_args::Command::Watch(keepalive)) => { - return imap_handlers::watch(keepalive, mbox, &mut imap); - } - _ => (), - } - } - - // Check account commands. - match account_args::matches(&m)? { - Some(account_args::Cmd::List(max_width)) => { - return account_handlers::list(max_width, &config, &account_config, &mut printer); - } - _ => (), - } - - // Check mailbox commands. - match mbox_args::matches(&m)? { - Some(mbox_args::Cmd::List(max_width)) => { - return mbox_handlers::list(max_width, &account_config, &mut printer, backend); - } - _ => (), - } - - // Check message commands. - match msg_args::matches(&m)? { - Some(msg_args::Cmd::Attachments(seq)) => { - return msg_handlers::attachments(seq, mbox, &account_config, &mut printer, backend); - } - Some(msg_args::Cmd::Copy(seq, mbox_dst)) => { - return msg_handlers::copy(seq, mbox, mbox_dst, &mut printer, backend); - } - Some(msg_args::Cmd::Delete(seq)) => { - return msg_handlers::delete(seq, mbox, &mut printer, backend); - } - Some(msg_args::Cmd::Forward(seq, attachment_paths, encrypt)) => { - return msg_handlers::forward( - seq, - attachment_paths, - encrypt, - mbox, - &account_config, - &mut printer, - backend, - &mut smtp, - ); - } - Some(msg_args::Cmd::List(max_width, page_size, page)) => { - return msg_handlers::list( - max_width, - page_size, - page, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(msg_args::Cmd::Move(seq, mbox_dst)) => { - return msg_handlers::move_(seq, mbox, mbox_dst, &mut printer, backend); - } - Some(msg_args::Cmd::Read(seq, text_mime, raw, headers)) => { - return msg_handlers::read( - seq, - text_mime, - raw, - headers, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(msg_args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { - return msg_handlers::reply( - seq, - all, - attachment_paths, - encrypt, - mbox, - &account_config, - &mut printer, - backend, - &mut smtp, - ); - } - Some(msg_args::Cmd::Save(raw_msg)) => { - return msg_handlers::save(mbox, raw_msg, &mut printer, backend); - } - Some(msg_args::Cmd::Search(query, max_width, page_size, page)) => { - return msg_handlers::search( - query, - max_width, - page_size, - page, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(msg_args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { - return msg_handlers::sort( - criteria, - query, - max_width, - page_size, - page, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(msg_args::Cmd::Send(raw_msg)) => { - return msg_handlers::send(raw_msg, &account_config, &mut printer, backend, &mut smtp); - } - Some(msg_args::Cmd::Write(tpl, atts, encrypt)) => { - return msg_handlers::write( - tpl, - atts, - encrypt, - &account_config, - &mut printer, - backend, - &mut smtp, - ); - } - Some(msg_args::Cmd::Flag(m)) => match m { - Some(flag_args::Cmd::Set(seq_range, ref flags)) => { - return flag_handlers::set(seq_range, flags, mbox, &mut printer, backend); - } - Some(flag_args::Cmd::Add(seq_range, ref flags)) => { - return flag_handlers::add(seq_range, flags, mbox, &mut printer, backend); - } - Some(flag_args::Cmd::Remove(seq_range, ref flags)) => { - return flag_handlers::remove(seq_range, flags, mbox, &mut printer, backend); - } - _ => (), - }, - Some(msg_args::Cmd::Tpl(m)) => match m { - Some(tpl_args::Cmd::New(tpl)) => { - return tpl_handlers::new(tpl, &account_config, &mut printer); - } - Some(tpl_args::Cmd::Reply(seq, all, tpl)) => { - return tpl_handlers::reply( - seq, - all, - tpl, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(tpl_args::Cmd::Forward(seq, tpl)) => { - return tpl_handlers::forward( - seq, - tpl, - mbox, - &account_config, - &mut printer, - backend, - ); - } - Some(tpl_args::Cmd::Save(atts, tpl)) => { - return tpl_handlers::save(mbox, &account_config, atts, tpl, &mut printer, backend); - } - Some(tpl_args::Cmd::Send(atts, tpl)) => { - return tpl_handlers::send( - mbox, - &account_config, - atts, - tpl, - &mut printer, - backend, - &mut smtp, - ); - } - _ => (), - }, - _ => (), - } - - backend.disconnect().context("cannot disconnect") -} diff --git a/cli/src/output/mod.rs b/cli/src/output/mod.rs deleted file mode 100644 index d458a0b..0000000 --- a/cli/src/output/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! Module related to output formatting and printing. - -pub mod output_args; - -pub mod output_utils; -pub use output_utils::*; - -pub mod output_entity; -pub use output_entity::*; - -pub mod print; -pub use print::*; - -pub mod print_table; -pub use print_table::*; - -pub mod printer_service; -pub use printer_service::*; diff --git a/cli/src/output/output_utils.rs b/cli/src/output/output_utils.rs deleted file mode 100644 index 2053479..0000000 --- a/cli/src/output/output_utils.rs +++ /dev/null @@ -1,41 +0,0 @@ -use anyhow::{anyhow, Context, Result}; -use log::debug; -use std::{ - io::prelude::*, - process::{Command, Stdio}, -}; - -/// TODO: move this in a more approriate place. -pub fn run_cmd(cmd: &str) -> Result { - debug!("running command: {}", cmd); - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output() - } else { - Command::new("sh").arg("-c").arg(cmd).output() - }?; - - Ok(String::from_utf8(output.stdout)?) -} - -pub fn pipe_cmd(cmd: &str, data: &[u8]) -> Result> { - let mut res = Vec::new(); - - let process = Command::new(cmd) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn() - .with_context(|| format!("cannot spawn process from command {:?}", cmd))?; - process - .stdin - .ok_or_else(|| anyhow!("cannot get stdin"))? - .write_all(data) - .with_context(|| "cannot write data to stdin")?; - process - .stdout - .ok_or_else(|| anyhow!("cannot get stdout"))? - .read_to_end(&mut res) - .with_context(|| "cannot read data from stdout")?; - - Ok(res) -} diff --git a/cli/src/smtp/mod.rs b/cli/src/smtp/mod.rs deleted file mode 100644 index dc0a9f6..0000000 --- a/cli/src/smtp/mod.rs +++ /dev/null @@ -1 +0,0 @@ -//! Module related to SMTP. diff --git a/cli/src/smtp/smtp_service.rs b/cli/src/smtp/smtp_service.rs deleted file mode 100644 index 7a9db3b..0000000 --- a/cli/src/smtp/smtp_service.rs +++ /dev/null @@ -1,86 +0,0 @@ -use anyhow::{Context, Result}; -use himalaya_lib::{account::Account, msg::Msg}; -use lettre::{ - self, - transport::smtp::{ - client::{Tls, TlsParameters}, - SmtpTransport, - }, - Transport, -}; -use std::convert::TryInto; - -use crate::output::pipe_cmd; - -pub trait SmtpService { - fn send(&mut self, account: &Account, msg: &Msg) -> Result>; -} - -pub struct LettreService<'a> { - account: &'a Account, - transport: Option, -} - -impl LettreService<'_> { - fn transport(&mut self) -> Result<&SmtpTransport> { - if let Some(ref transport) = self.transport { - Ok(transport) - } else { - let builder = if self.account.smtp_starttls { - SmtpTransport::starttls_relay(&self.account.smtp_host) - } else { - SmtpTransport::relay(&self.account.smtp_host) - }?; - - let tls = TlsParameters::builder(self.account.smtp_host.to_owned()) - .dangerous_accept_invalid_hostnames(self.account.smtp_insecure) - .dangerous_accept_invalid_certs(self.account.smtp_insecure) - .build()?; - let tls = if self.account.smtp_starttls { - Tls::Required(tls) - } else { - Tls::Wrapper(tls) - }; - - self.transport = Some( - builder - .tls(tls) - .port(self.account.smtp_port) - .credentials(self.account.smtp_creds()?) - .build(), - ); - - Ok(self.transport.as_ref().unwrap()) - } - } -} - -impl SmtpService for LettreService<'_> { - fn send(&mut self, account: &Account, msg: &Msg) -> Result> { - let mut raw_msg = msg.into_sendable_msg(account)?.formatted(); - - let envelope: lettre::address::Envelope = - if let Some(cmd) = account.hooks.pre_send.as_deref() { - for cmd in cmd.split('|') { - raw_msg = pipe_cmd(cmd.trim(), &raw_msg) - .with_context(|| format!("cannot execute pre-send hook {:?}", cmd))?; - } - let parsed_mail = mailparse::parse_mail(&raw_msg)?; - Msg::from_parsed_mail(parsed_mail, account)?.try_into() - } else { - msg.try_into() - }?; - - self.transport()?.send_raw(&envelope, &raw_msg)?; - Ok(raw_msg) - } -} - -impl<'a> From<&'a Account> for LettreService<'a> { - fn from(account: &'a Account) -> Self { - Self { - account, - transport: None, - } - } -} diff --git a/cli/src/ui/mod.rs b/cli/src/ui/mod.rs deleted file mode 100644 index 4210d06..0000000 --- a/cli/src/ui/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Module related to User Interface. - -pub mod table_arg; - -pub mod table; -pub use table::*; - -pub mod choice; -pub mod editor; diff --git a/flake.nix b/flake.nix index c3c7af2..25fc597 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "Command-line interface for email management"; + description = "Command-line interface for email management."; inputs = { utils.url = "github:numtide/flake-utils"; @@ -34,18 +34,6 @@ ''; }; }; - "${name}-vim" = pkgs.vimUtils.buildVimPluginFrom2Nix { - inherit (packages.${name}) version; - name = "${name}-vim"; - src = self; - buildInputs = [ packages.${name} ]; - dontConfigure = false; - configurePhase = "cd vim/"; - postInstall = '' - mkdir -p $out/bin - ln -s ${packages.${name}}/bin/himalaya $out/bin/himalaya - ''; - }; }; # nix run @@ -57,7 +45,6 @@ # nix develop devShell = pkgs.mkShell { - RUSTUP_TOOLCHAIN = "stable"; inputsFrom = builtins.attrValues self.packages.${system}; nativeBuildInputs = with pkgs; [ # Nix LSP + formatter diff --git a/lib/Cargo.toml b/lib/Cargo.toml deleted file mode 100644 index 2549a13..0000000 --- a/lib/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "himalaya-lib" -version = "0.1.0" -edition = "2021" - -[features] -imap-backend = ["imap", "imap-proto"] -maildir-backend = ["maildir", "md5"] -notmuch-backend = ["notmuch", "maildir-backend"] -default = ["imap-backend", "maildir-backend"] - -[dependencies] -ammonia = "3.1.2" -chrono = "0.4.19" -convert_case = "0.5.0" -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"] } -shellexpand = "2.1.0" -thiserror = "1.0.31" -toml = "0.5.8" -tree_magic = "0.2.3" -uuid = { version = "0.8", features = ["v4"] } - -# [optional] -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 } diff --git a/lib/src/account/account_config.rs b/lib/src/account/account_config.rs deleted file mode 100644 index 15e3dc9..0000000 --- a/lib/src/account/account_config.rs +++ /dev/null @@ -1,536 +0,0 @@ -//! Account config module. -//! -//! This module contains the representation of the user account. - -use lettre::transport::smtp::authentication::Credentials as SmtpCredentials; -use log::{debug, info, trace}; -use mailparse::MailAddr; -use serde::Deserialize; -use shellexpand; -use std::{collections::HashMap, env, ffi::OsStr, fs, path::PathBuf}; -use thiserror::Error; - -use crate::process::{self, ProcessError}; - -use super::*; - -pub const DEFAULT_PAGE_SIZE: usize = 10; -pub const DEFAULT_SIG_DELIM: &str = "-- \n"; - -pub const DEFAULT_INBOX_FOLDER: &str = "INBOX"; -pub const DEFAULT_SENT_FOLDER: &str = "Sent"; -pub const DEFAULT_DRAFT_FOLDER: &str = "Drafts"; - -#[derive(Debug, Error)] -pub enum AccountError { - #[error("cannot encrypt file using pgp")] - EncryptFileError(#[source] ProcessError), - #[error("cannot find encrypt file command from config file")] - EncryptFileMissingCmdError, - - #[error("cannot decrypt file using pgp")] - DecryptFileError(#[source] ProcessError), - #[error("cannot find decrypt file command from config file")] - DecryptFileMissingCmdError, - - #[error("cannot get smtp password")] - GetSmtpPasswdError(#[source] ProcessError), - #[error("cannot get smtp password: password is empty")] - GetSmtpPasswdEmptyError, - - #[cfg(feature = "imap-backend")] - #[error("cannot get imap password")] - GetImapPasswdError(#[source] ProcessError), - #[cfg(feature = "imap-backend")] - #[error("cannot get imap password: password is empty")] - GetImapPasswdEmptyError, - - #[error("cannot find default account")] - FindDefaultAccountError, - #[error("cannot find account {0}")] - FindAccountError(String), - #[error("cannot parse account address {0}")] - ParseAccountAddrError(#[source] mailparse::MailParseError, String), - #[error("cannot find account address in {0}")] - ParseAccountAddrNotFoundError(String), - - #[cfg(feature = "maildir-backend")] - #[error("cannot expand maildir path")] - ExpandMaildirPathError(#[source] shellexpand::LookupError), - #[cfg(feature = "notmuch-backend")] - #[error("cannot expand notmuch path")] - ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError), - #[error("cannot expand mailbox alias {1}")] - ExpandMboxAliasError(#[source] shellexpand::LookupError, String), - - #[error("cannot parse download file name from {0}")] - ParseDownloadFileNameError(PathBuf), - - #[error("cannot start the notify mode")] - StartNotifyModeError(#[source] ProcessError), -} - -/// Represents the user account. -#[derive(Debug, Default, Clone)] -pub struct Account { - /// Represents the name of the user account. - pub name: String, - /// Makes this account the default one. - pub default: bool, - /// Represents the display name of the user account. - pub display_name: String, - /// Represents the email address of the user account. - pub email: String, - /// Represents the downloads directory (mostly for attachments). - pub downloads_dir: PathBuf, - /// Represents the signature of the user. - pub sig: Option, - /// Represents the default page size for listings. - pub default_page_size: usize, - /// Represents the notify command. - pub notify_cmd: Option, - /// Overrides the default IMAP query "NEW" used to fetch new messages - pub notify_query: String, - /// Represents the watch commands. - pub watch_cmds: Vec, - /// Represents the text/plain format as defined in the - /// [RFC2646](https://www.ietf.org/rfc/rfc2646.txt) - pub format: TextPlainFormat, - /// Overrides the default headers displayed at the top of - /// the read message. - pub read_headers: Vec, - - /// Represents mailbox aliases. - pub mailboxes: HashMap, - - /// Represents hooks. - pub hooks: Hooks, - - /// Represents the SMTP host. - pub smtp_host: String, - /// Represents the SMTP port. - pub smtp_port: u16, - /// Enables StartTLS. - pub smtp_starttls: bool, - /// Trusts any certificate. - pub smtp_insecure: bool, - /// Represents the SMTP login. - pub smtp_login: String, - /// Represents the SMTP password command. - pub smtp_passwd_cmd: String, - - /// Represents the command used to encrypt a message. - pub pgp_encrypt_cmd: Option, - /// Represents the command used to decrypt a message. - pub pgp_decrypt_cmd: Option, -} - -impl<'a> Account { - /// Tries to create an account from a config and an optional - /// account name. - pub fn from_config_and_opt_account_name( - config: &'a DeserializedConfig, - account_name: Option<&str>, - ) -> Result<(Account, BackendConfig), AccountError> { - info!("begin: parsing account and backend configs from config and account name"); - - debug!("account name: {:?}", account_name.unwrap_or("default")); - let (name, account) = match account_name.map(|name| name.trim()) { - Some("default") | Some("") | None => config - .accounts - .iter() - .find(|(_, account)| match account { - #[cfg(feature = "imap-backend")] - DeserializedAccountConfig::Imap(account) => account.default.unwrap_or_default(), - #[cfg(feature = "maildir-backend")] - DeserializedAccountConfig::Maildir(account) => { - account.default.unwrap_or_default() - } - #[cfg(feature = "notmuch-backend")] - DeserializedAccountConfig::Notmuch(account) => { - account.default.unwrap_or_default() - } - }) - .map(|(name, account)| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindDefaultAccountError), - Some(name) => config - .accounts - .get(name) - .map(|account| (name.to_owned(), account)) - .ok_or_else(|| AccountError::FindAccountError(name.to_owned())), - }?; - - let base_account = account.to_base(); - let downloads_dir = base_account - .downloads_dir - .as_ref() - .and_then(|dir| dir.to_str()) - .and_then(|dir| shellexpand::full(dir).ok()) - .map(|dir| PathBuf::from(dir.to_string())) - .or_else(|| { - config - .downloads_dir - .as_ref() - .and_then(|dir| dir.to_str()) - .and_then(|dir| shellexpand::full(dir).ok()) - .map(|dir| PathBuf::from(dir.to_string())) - }) - .unwrap_or_else(env::temp_dir); - - let default_page_size = base_account - .default_page_size - .as_ref() - .or_else(|| config.default_page_size.as_ref()) - .unwrap_or(&DEFAULT_PAGE_SIZE) - .to_owned(); - - let default_sig_delim = DEFAULT_SIG_DELIM.to_string(); - let sig_delim = base_account - .signature_delimiter - .as_ref() - .or_else(|| config.signature_delimiter.as_ref()) - .unwrap_or(&default_sig_delim); - let sig = base_account - .signature - .as_ref() - .or_else(|| config.signature.as_ref()); - let sig = sig - .and_then(|sig| shellexpand::full(sig).ok()) - .map(String::from) - .and_then(|sig| fs::read_to_string(sig).ok()) - .or_else(|| sig.map(|sig| sig.to_owned())) - .map(|sig| format!("{}{}", sig_delim, sig.trim_end())); - - let account_config = Account { - name, - display_name: base_account - .name - .as_ref() - .unwrap_or(&config.name) - .to_owned(), - downloads_dir, - sig, - default_page_size, - notify_cmd: base_account - .notify_cmd - .as_ref() - .or_else(|| config.notify_cmd.as_ref()) - .cloned(), - notify_query: base_account - .notify_query - .as_ref() - .or_else(|| config.notify_query.as_ref()) - .unwrap_or(&String::from("NEW")) - .to_owned(), - watch_cmds: base_account - .watch_cmds - .as_ref() - .or_else(|| config.watch_cmds.as_ref()) - .unwrap_or(&vec![]) - .to_owned(), - format: base_account.format.unwrap_or_default(), - read_headers: base_account.read_headers, - mailboxes: base_account.mailboxes.clone(), - hooks: base_account.hooks.unwrap_or_default(), - default: base_account.default.unwrap_or_default(), - email: base_account.email.to_owned(), - - smtp_host: base_account.smtp_host.to_owned(), - smtp_port: base_account.smtp_port, - smtp_starttls: base_account.smtp_starttls.unwrap_or_default(), - smtp_insecure: base_account.smtp_insecure.unwrap_or_default(), - smtp_login: base_account.smtp_login.to_owned(), - smtp_passwd_cmd: base_account.smtp_passwd_cmd.to_owned(), - - pgp_encrypt_cmd: base_account.pgp_encrypt_cmd.to_owned(), - pgp_decrypt_cmd: base_account.pgp_decrypt_cmd.to_owned(), - }; - trace!("account config: {:?}", account_config); - - let backend_config = match account { - #[cfg(feature = "imap-backend")] - DeserializedAccountConfig::Imap(config) => BackendConfig::Imap(ImapBackendConfig { - imap_host: config.imap_host.clone(), - imap_port: config.imap_port.clone(), - imap_starttls: config.imap_starttls.unwrap_or_default(), - imap_insecure: config.imap_insecure.unwrap_or_default(), - imap_login: config.imap_login.clone(), - imap_passwd_cmd: config.imap_passwd_cmd.clone(), - }), - #[cfg(feature = "maildir-backend")] - DeserializedAccountConfig::Maildir(config) => { - BackendConfig::Maildir(MaildirBackendConfig { - maildir_dir: shellexpand::full(&config.maildir_dir) - .map_err(AccountError::ExpandMaildirPathError)? - .to_string() - .into(), - }) - } - #[cfg(feature = "notmuch-backend")] - DeserializedAccountConfig::Notmuch(config) => { - BackendConfig::Notmuch(NotmuchBackendConfig { - notmuch_database_dir: shellexpand::full(&config.notmuch_database_dir) - .map_err(AccountError::ExpandNotmuchDatabasePathError)? - .to_string() - .into(), - }) - } - }; - trace!("backend config: {:?}", backend_config); - - info!("end: parsing account and backend configs from config and account name"); - Ok((account_config, backend_config)) - } - - /// Builds the full RFC822 compliant address of the user account. - pub fn address(&self) -> Result { - let has_special_chars = "()<>[]:;@.,".contains(|c| self.display_name.contains(c)); - let addr = if self.display_name.is_empty() { - self.email.clone() - } else if has_special_chars { - // Wraps the name with double quotes if it contains any special character. - format!("\"{}\" <{}>", self.display_name, self.email) - } else { - format!("{} <{}>", self.display_name, self.email) - }; - - Ok(mailparse::addrparse(&addr) - .map_err(|err| AccountError::ParseAccountAddrError(err, addr.to_owned()))? - .first() - .ok_or_else(|| AccountError::ParseAccountAddrNotFoundError(addr.to_owned()))? - .clone()) - } - - /// Builds the user account SMTP credentials. - pub fn smtp_creds(&self) -> Result { - let passwd = - process::run(&self.smtp_passwd_cmd).map_err(AccountError::GetSmtpPasswdError)?; - let passwd = passwd - .lines() - .next() - .ok_or_else(|| AccountError::GetSmtpPasswdEmptyError)?; - - Ok(SmtpCredentials::new( - self.smtp_login.to_owned(), - passwd.to_owned(), - )) - } - - /// Encrypts a file. - pub fn pgp_encrypt_file(&self, addr: &str, path: PathBuf) -> Result { - if let Some(cmd) = self.pgp_encrypt_cmd.as_ref() { - let encrypt_file_cmd = format!("{} {} {:?}", cmd, addr, path); - Ok(process::run(&encrypt_file_cmd).map_err(AccountError::EncryptFileError)?) - } else { - Err(AccountError::EncryptFileMissingCmdError) - } - } - - /// Decrypts a file. - pub fn pgp_decrypt_file(&self, path: PathBuf) -> Result { - if let Some(cmd) = self.pgp_decrypt_cmd.as_ref() { - let decrypt_file_cmd = format!("{} {:?}", cmd, path); - Ok(process::run(&decrypt_file_cmd).map_err(AccountError::DecryptFileError)?) - } else { - Err(AccountError::DecryptFileMissingCmdError) - } - } - - /// Gets the download path from a file name. - pub fn get_download_file_path>( - &self, - file_name: S, - ) -> Result { - let file_path = self.downloads_dir.join(file_name.as_ref()); - self.get_unique_download_file_path(&file_path, |path, _count| path.is_file()) - } - - /// Gets the unique download path from a file name by adding - /// suffixes in case of name conflicts. - pub fn get_unique_download_file_path( - &self, - original_file_path: &PathBuf, - is_file: impl Fn(&PathBuf, u8) -> bool, - ) -> Result { - let mut count = 0; - let file_ext = original_file_path - .extension() - .and_then(OsStr::to_str) - .map(|fext| String::from(".") + fext) - .unwrap_or_default(); - let mut file_path = original_file_path.clone(); - - while is_file(&file_path, count) { - count += 1; - file_path.set_file_name(OsStr::new( - &original_file_path - .file_stem() - .and_then(OsStr::to_str) - .map(|fstem| format!("{}_{}{}", fstem, count, file_ext)) - .ok_or_else(|| { - AccountError::ParseDownloadFileNameError(file_path.to_owned()) - })?, - )); - } - - Ok(file_path) - } - - /// Runs the notify command. - pub fn run_notify_cmd>(&self, subject: S, sender: S) -> Result<(), AccountError> { - let subject = subject.as_ref(); - let sender = sender.as_ref(); - - let default_cmd = format!(r#"notify-send "New message from {}" "{}""#, sender, subject); - let cmd = self - .notify_cmd - .as_ref() - .map(|cmd| format!(r#"{} {:?} {:?}"#, cmd, subject, sender)) - .unwrap_or(default_cmd); - - process::run(&cmd).map_err(AccountError::StartNotifyModeError)?; - Ok(()) - } - - /// Gets the mailbox alias if exists, otherwise returns the - /// mailbox. Also tries to expand shell variables. - pub fn get_mbox_alias(&self, mbox: &str) -> Result { - let mbox = self - .mailboxes - .get(&mbox.trim().to_lowercase()) - .map(|s| s.as_str()) - .unwrap_or(mbox); - let mbox = shellexpand::full(mbox) - .map(String::from) - .map_err(|err| AccountError::ExpandMboxAliasError(err, mbox.to_owned()))?; - Ok(mbox) - } -} - -/// Represents all existing kind of account (backend). -#[derive(Debug, Clone)] -pub enum BackendConfig { - #[cfg(feature = "imap-backend")] - Imap(ImapBackendConfig), - #[cfg(feature = "maildir-backend")] - Maildir(MaildirBackendConfig), - #[cfg(feature = "notmuch-backend")] - Notmuch(NotmuchBackendConfig), -} - -/// Represents the IMAP backend. -#[cfg(feature = "imap-backend")] -#[derive(Debug, Default, Clone)] -pub struct ImapBackendConfig { - /// Represents the IMAP host. - pub imap_host: String, - /// Represents the IMAP port. - pub imap_port: u16, - /// Enables StartTLS. - pub imap_starttls: bool, - /// Trusts any certificate. - pub imap_insecure: bool, - /// Represents the IMAP login. - pub imap_login: String, - /// Represents the IMAP password command. - pub imap_passwd_cmd: String, -} - -#[cfg(feature = "imap-backend")] -impl ImapBackendConfig { - /// Gets the IMAP password of the user account. - pub fn imap_passwd(&self) -> Result { - let passwd = - process::run(&self.imap_passwd_cmd).map_err(AccountError::GetImapPasswdError)?; - let passwd = passwd - .lines() - .next() - .ok_or_else(|| AccountError::GetImapPasswdEmptyError)?; - Ok(passwd.to_string()) - } -} - -/// Represents the Maildir backend. -#[cfg(feature = "maildir-backend")] -#[derive(Debug, Default, Clone)] -pub struct MaildirBackendConfig { - /// Represents the Maildir directory path. - pub maildir_dir: PathBuf, -} - -/// Represents the Notmuch backend. -#[cfg(feature = "notmuch-backend")] -#[derive(Debug, Default, Clone)] -pub struct NotmuchBackendConfig { - /// Represents the Notmuch database path. - pub notmuch_database_dir: PathBuf, -} - -/// Represents the text/plain format as defined in the [RFC2646]. -/// -/// [RFC2646]: https://www.ietf.org/rfc/rfc2646.txt -#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] -#[serde(tag = "type", content = "width", rename_all = "lowercase")] -pub enum TextPlainFormat { - // Forces the content width with a fixed amount of pixels. - Fixed(usize), - // Makes the content fit the terminal. - Auto, - // Does not restrict the content. - Flowed, -} - -impl Default for TextPlainFormat { - fn default() -> Self { - Self::Auto - } -} - -#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Hooks { - pub pre_send: Option, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_should_get_unique_download_file_path() { - let account = Account::default(); - let path = PathBuf::from("downloads/file.ext"); - - // When file path is unique - assert!(matches!( - account.get_unique_download_file_path(&path, |_, _| false), - Ok(path) if path == PathBuf::from("downloads/file.ext") - )); - - // When 1 file path already exist - assert!(matches!( - account.get_unique_download_file_path(&path, |_, count| count < 1), - Ok(path) if path == PathBuf::from("downloads/file_1.ext") - )); - - // When 5 file paths already exist - assert!(matches!( - account.get_unique_download_file_path(&path, |_, count| count < 5), - Ok(path) if path == PathBuf::from("downloads/file_5.ext") - )); - - // When file path has no extension - let path = PathBuf::from("downloads/file"); - assert!(matches!( - account.get_unique_download_file_path(&path, |_, count| count < 5), - Ok(path) if path == PathBuf::from("downloads/file_5") - )); - - // When file path has 2 extensions - let path = PathBuf::from("downloads/file.ext.ext2"); - assert!(matches!( - account.get_unique_download_file_path(&path, |_, count| count < 5), - Ok(path) if path == PathBuf::from("downloads/file.ext_5.ext2") - )); - } -} diff --git a/lib/src/account/deserialized_account_config.rs b/lib/src/account/deserialized_account_config.rs deleted file mode 100644 index 8ba6278..0000000 --- a/lib/src/account/deserialized_account_config.rs +++ /dev/null @@ -1,156 +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 serde::Deserialize; -use std::{collections::HashMap, path::PathBuf}; - -use super::*; - -pub trait ToDeserializedBaseAccountConfig { - fn to_base(&self) -> DeserializedBaseAccountConfig; -} - -/// Represents all existing kind of account config. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -pub enum DeserializedAccountConfig { - #[cfg(feature = "imap-backend")] - Imap(DeserializedImapAccountConfig), - #[cfg(feature = "maildir-backend")] - Maildir(DeserializedMaildirAccountConfig), - #[cfg(feature = "notmuch-backend")] - Notmuch(DeserializedNotmuchAccountConfig), -} - -impl ToDeserializedBaseAccountConfig for DeserializedAccountConfig { - fn to_base(&self) -> DeserializedBaseAccountConfig { - match self { - #[cfg(feature = "imap-backend")] - Self::Imap(config) => config.to_base(), - #[cfg(feature = "maildir-backend")] - Self::Maildir(config) => config.to_base(), - #[cfg(feature = "notmuch-backend")] - Self::Notmuch(config) => config.to_base(), - } - } -} - -macro_rules! make_account_config { - ($AccountConfig:ident, $($element: ident: $ty: ty),*) => { - #[derive(Debug, Default, Clone, PartialEq, Deserialize)] - #[serde(rename_all = "kebab-case")] - pub struct $AccountConfig { - /// Overrides the display name of the user for this account. - pub name: Option, - /// Overrides the downloads directory (mostly for attachments). - pub downloads_dir: Option, - /// Overrides the signature for this account. - pub signature: Option, - /// Overrides the signature delimiter for this account. - pub signature_delimiter: Option, - /// Overrides the default page size for this account. - pub default_page_size: Option, - /// Overrides the notify command for this account. - pub notify_cmd: Option, - /// Overrides the IMAP query used to fetch new messages for this account. - pub notify_query: Option, - /// Overrides the watch commands for this account. - pub watch_cmds: Option>, - /// Represents the text/plain format. - pub format: Option, - /// Represents the default headers displayed at the top of - /// the read message. - #[serde(default)] - pub read_headers: Vec, - - /// Makes this account the default one. - pub default: Option, - /// Represents the account email address. - pub email: String, - - /// Represents the SMTP host. - pub smtp_host: String, - /// Represents the SMTP port. - pub smtp_port: u16, - /// Enables StartTLS. - pub smtp_starttls: Option, - /// Trusts any certificate. - pub smtp_insecure: Option, - /// Represents the SMTP login. - pub smtp_login: String, - /// Represents the SMTP password command. - pub smtp_passwd_cmd: String, - - /// Represents the command used to encrypt a message. - pub pgp_encrypt_cmd: Option, - /// Represents the command used to decrypt a message. - pub pgp_decrypt_cmd: Option, - - /// Represents mailbox aliases. - #[serde(default)] - pub mailboxes: HashMap, - - /// Represents hooks. - pub hooks: Option, - - $(pub $element: $ty),* - } - - impl ToDeserializedBaseAccountConfig for $AccountConfig { - fn to_base(&self) -> DeserializedBaseAccountConfig { - DeserializedBaseAccountConfig { - name: self.name.clone(), - downloads_dir: self.downloads_dir.clone(), - signature: self.signature.clone(), - signature_delimiter: self.signature_delimiter.clone(), - default_page_size: self.default_page_size.clone(), - notify_cmd: self.notify_cmd.clone(), - notify_query: self.notify_query.clone(), - watch_cmds: self.watch_cmds.clone(), - format: self.format.clone(), - read_headers: self.read_headers.clone(), - - default: self.default.clone(), - email: self.email.clone(), - - smtp_host: self.smtp_host.clone(), - smtp_port: self.smtp_port.clone(), - smtp_starttls: self.smtp_starttls.clone(), - smtp_insecure: self.smtp_insecure.clone(), - smtp_login: self.smtp_login.clone(), - smtp_passwd_cmd: self.smtp_passwd_cmd.clone(), - - pgp_encrypt_cmd: self.pgp_encrypt_cmd.clone(), - pgp_decrypt_cmd: self.pgp_decrypt_cmd.clone(), - - mailboxes: self.mailboxes.clone(), - hooks: self.hooks.clone(), - } - } - } - } -} - -make_account_config!(DeserializedBaseAccountConfig,); - -#[cfg(feature = "imap-backend")] -make_account_config!( - DeserializedImapAccountConfig, - imap_host: String, - imap_port: u16, - imap_starttls: Option, - imap_insecure: Option, - imap_login: String, - imap_passwd_cmd: String -); - -#[cfg(feature = "maildir-backend")] -make_account_config!(DeserializedMaildirAccountConfig, maildir_dir: String); - -#[cfg(feature = "notmuch-backend")] -make_account_config!( - DeserializedNotmuchAccountConfig, - notmuch_database_dir: String -); diff --git a/lib/src/account/deserialized_config.rs b/lib/src/account/deserialized_config.rs deleted file mode 100644 index 9da1798..0000000 --- a/lib/src/account/deserialized_config.rs +++ /dev/null @@ -1,111 +0,0 @@ -//! Deserialized config module. -//! -//! This module contains the raw deserialized representation of the -//! user configuration file. - -use log::{debug, trace}; -use serde::Deserialize; -use std::{collections::HashMap, env, fs, io, path::PathBuf}; -use thiserror::Error; -use toml; - -use super::*; - -#[derive(Error, Debug)] -pub enum DeserializeConfigError { - #[error("cannot read config file")] - ReadConfigFile(#[source] io::Error), - #[error("cannot parse config file")] - ParseConfigFile(#[source] toml::de::Error), - #[error("cannot read environment variable {1}")] - ReadEnvVar(#[source] env::VarError, &'static str), -} - -/// Represents the user config file. -#[derive(Debug, Default, Clone, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct DeserializedConfig { - /// Represents the display name of the user. - pub name: String, - /// Represents the downloads directory (mostly for attachments). - pub downloads_dir: Option, - /// Represents the signature of the user. - pub signature: Option, - /// Overrides the default signature delimiter "`-- \n`". - pub signature_delimiter: Option, - /// Represents the default page size for listings. - pub default_page_size: Option, - /// Represents the notify command. - pub notify_cmd: Option, - /// Overrides the default IMAP query "NEW" used to fetch new messages - pub notify_query: Option, - /// Represents the watch commands. - pub watch_cmds: Option>, - - /// Represents all the user accounts. - #[serde(flatten)] - pub accounts: HashMap, -} - -impl DeserializedConfig { - /// Tries to create a config from an optional path. - pub fn from_opt_path(path: Option<&str>) -> Result { - 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).map_err(DeserializeConfigError::ReadConfigFile)?; - let config = toml::from_str(&content).map_err(DeserializeConfigError::ParseConfigFile)?; - - 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 { - let path = env::var("XDG_CONFIG_HOME") - .map_err(|err| DeserializeConfigError::ReadEnvVar(err, "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 { - let home_var = if cfg!(target_family = "windows") { - "USERPROFILE" - } else { - "HOME" - }; - let path = - env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, 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 { - let home_var = if cfg!(target_family = "windows") { - "USERPROFILE" - } else { - "HOME" - }; - let path = - env::var(home_var).map_err(|err| DeserializeConfigError::ReadEnvVar(err, home_var))?; - let path = PathBuf::from(path).join(".himalayarc"); - Ok(path) - } - - /// Tries to get the config file path. - pub fn path() -> Result { - Self::path_from_xdg() - .or_else(|_| Self::path_from_xdg_alt()) - .or_else(|_| Self::path_from_home()) - } -} diff --git a/lib/src/account/mod.rs b/lib/src/account/mod.rs deleted file mode 100644 index 01a8c4d..0000000 --- a/lib/src/account/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Account module. -//! -//! This module contains everything related to the user configuration. - -mod account_config; -pub use account_config::*; - -mod deserialized_config; -pub use deserialized_config::*; - -mod deserialized_account_config; -pub use deserialized_account_config::*; diff --git a/lib/src/backend/backend.rs b/lib/src/backend/backend.rs deleted file mode 100644 index b1d1d99..0000000 --- a/lib/src/backend/backend.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Backend module. -//! -//! This module exposes the backend trait, which can be used to create -//! custom backend implementations. - -use std::result; - -use thiserror::Error; - -use crate::{ - account, - mbox::Mboxes, - msg::{self, Envelopes, Msg}, -}; - -use super::id_mapper; - -#[cfg(feature = "maildir-backend")] -use super::MaildirError; - -#[cfg(feature = "notmuch-backend")] -use super::NotmuchError; - -#[derive(Error, Debug)] -pub enum Error { - #[error(transparent)] - ImapError(#[from] super::imap::Error), - - #[error(transparent)] - AccountError(#[from] account::AccountError), - - #[error(transparent)] - MsgError(#[from] msg::Error), - - #[error(transparent)] - IdMapperError(#[from] id_mapper::Error), - - #[cfg(feature = "maildir-backend")] - #[error(transparent)] - MaildirError(#[from] MaildirError), - - #[cfg(feature = "notmuch-backend")] - #[error(transparent)] - NotmuchError(#[from] NotmuchError), -} - -pub type Result = result::Result; - -pub trait Backend<'a> { - fn connect(&mut self) -> Result<()> { - Ok(()) - } - - fn add_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_mboxes(&mut self) -> Result; - fn del_mbox(&mut self, mbox: &str) -> Result<()>; - fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result; - fn search_envelopes( - &mut self, - mbox: &str, - query: &str, - sort: &str, - page_size: usize, - page: usize, - ) -> Result; - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result; - fn get_msg(&mut self, mbox: &str, id: &str) -> Result; - fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; - fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, ids: &str) -> Result<()>; - fn del_msg(&mut self, mbox: &str, ids: &str) -> Result<()>; - fn add_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>; - fn set_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>; - fn del_flags(&mut self, mbox: &str, ids: &str, flags: &str) -> Result<()>; - - fn disconnect(&mut self) -> Result<()> { - Ok(()) - } -} diff --git a/lib/src/backend/id_mapper.rs b/lib/src/backend/id_mapper.rs deleted file mode 100644 index d5ab5df..0000000 --- a/lib/src/backend/id_mapper.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::{ - collections, fs, - io::{self, prelude::*}, - ops, path, result, -}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum Error { - #[error("cannot parse id mapper cache line {0}")] - ParseLineError(String), - #[error("cannot find message id from short hash {0}")] - FindFromShortHashError(String), - #[error("the short hash {0} matches more than one hash: {1}")] - MatchShortHashError(String, String), - - #[error("cannot open id mapper file: {1}")] - OpenHashMapFileError(#[source] io::Error, path::PathBuf), - #[error("cannot write id mapper file: {1}")] - WriteHashMapFileError(#[source] io::Error, path::PathBuf), - #[error("cannot read line from id mapper file")] - ReadHashMapFileLineError(#[source] io::Error), -} - -type Result = result::Result; - -#[derive(Debug, Default)] -pub struct IdMapper { - path: path::PathBuf, - map: collections::HashMap, - short_hash_len: usize, -} - -impl IdMapper { - pub fn new(dir: &path::Path) -> Result { - let mut mapper = Self::default(); - mapper.path = dir.join(".himalaya-id-map"); - - let file = fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .open(&mapper.path) - .map_err(|err| Error::OpenHashMapFileError(err, mapper.path.to_owned()))?; - let reader = io::BufReader::new(file); - for line in reader.lines() { - let line = line.map_err(Error::ReadHashMapFileLineError)?; - if mapper.short_hash_len == 0 { - mapper.short_hash_len = 2.max(line.parse().unwrap_or(2)); - } else { - let (hash, id) = line - .split_once(' ') - .ok_or_else(|| Error::ParseLineError(line.to_owned()))?; - mapper.insert(hash.to_owned(), id.to_owned()); - } - } - - Ok(mapper) - } - - pub fn find(&self, short_hash: &str) -> Result { - let matching_hashes: Vec<_> = self - .keys() - .filter(|hash| hash.starts_with(short_hash)) - .collect(); - if matching_hashes.len() == 0 { - Err(Error::FindFromShortHashError(short_hash.to_owned())) - } else if matching_hashes.len() > 1 { - Err(Error::MatchShortHashError( - short_hash.to_owned(), - matching_hashes - .iter() - .map(|s| s.to_string()) - .collect::>() - .join(", "), - )) - } else { - Ok(self.get(matching_hashes[0]).unwrap().to_owned()) - } - } - - pub fn append(&mut self, lines: Vec<(String, String)>) -> Result { - self.extend(lines); - - let mut entries = String::new(); - let mut short_hash_len = self.short_hash_len; - - for (hash, id) in self.iter() { - loop { - let short_hash = &hash[0..short_hash_len]; - let conflict_found = self - .map - .keys() - .find(|cached_hash| cached_hash.starts_with(short_hash) && cached_hash != &hash) - .is_some(); - if short_hash_len > 32 || !conflict_found { - break; - } - short_hash_len += 1; - } - entries.push_str(&format!("{} {}\n", hash, id)); - } - - self.short_hash_len = short_hash_len; - - fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(&self.path) - .map_err(|err| Error::OpenHashMapFileError(err, self.path.to_owned()))? - .write(format!("{}\n{}", short_hash_len, entries).as_bytes()) - .map_err(|err| Error::WriteHashMapFileError(err, self.path.to_owned()))?; - - Ok(short_hash_len) - } -} - -impl ops::Deref for IdMapper { - type Target = collections::HashMap; - - fn deref(&self) -> &Self::Target { - &self.map - } -} - -impl ops::DerefMut for IdMapper { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.map - } -} diff --git a/lib/src/backend/imap/error.rs b/lib/src/backend/imap/error.rs deleted file mode 100644 index ff3b233..0000000 --- a/lib/src/backend/imap/error.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::result; -use thiserror::Error; - -use crate::{ - account, - msg::{self, Flags}, -}; - -#[derive(Error, Debug)] -pub enum Error { - #[error("cannot get envelope of message {0}")] - GetEnvelopeError(u32), - #[error("cannot get sender of message {0}")] - GetSenderError(u32), - #[error("cannot get imap session")] - GetSessionError, - #[error("cannot retrieve message {0}'s uid")] - GetMsgUidError(u32), - #[error("cannot find message {0}")] - FindMsgError(String), - #[error("cannot parse sort criterion {0}")] - ParseSortCriterionError(String), - - #[error("cannot decode subject of message {1}")] - DecodeSubjectError(#[source] rfc2047_decoder::Error, u32), - #[error("cannot decode sender name of message {1}")] - DecodeSenderNameError(#[source] rfc2047_decoder::Error, u32), - #[error("cannot decode sender mailbox of message {1}")] - DecodeSenderMboxError(#[source] rfc2047_decoder::Error, u32), - #[error("cannot decode sender host of message {1}")] - DecodeSenderHostError(#[source] rfc2047_decoder::Error, u32), - - #[error("cannot create tls connector")] - CreateTlsConnectorError(#[source] native_tls::Error), - #[error("cannot connect to imap server")] - ConnectImapServerError(#[source] imap::Error), - #[error("cannot login to imap server")] - LoginImapServerError(#[source] imap::Error), - #[error("cannot search new messages")] - SearchNewMsgsError(#[source] imap::Error), - #[error("cannot examine mailbox {1}")] - ExamineMboxError(#[source] imap::Error, String), - #[error("cannot start the idle mode")] - StartIdleModeError(#[source] imap::Error), - #[error("cannot parse message {1}")] - ParseMsgError(#[source] mailparse::MailParseError, String), - #[error("cannot fetch new messages envelope")] - FetchNewMsgsEnvelopeError(#[source] imap::Error), - #[error("cannot get uid of message {0}")] - GetUidError(u32), - #[error("cannot create mailbox {1}")] - CreateMboxError(#[source] imap::Error, String), - #[error("cannot list mailboxes")] - ListMboxesError(#[source] imap::Error), - #[error("cannot delete mailbox {1}")] - DeleteMboxError(#[source] imap::Error, String), - #[error("cannot select mailbox {1}")] - SelectMboxError(#[source] imap::Error, String), - #[error("cannot fetch messages within range {1}")] - FetchMsgsByRangeError(#[source] imap::Error, String), - #[error("cannot fetch messages by sequence {1}")] - FetchMsgsBySeqError(#[source] imap::Error, String), - #[error("cannot append message to mailbox {1}")] - AppendMsgError(#[source] imap::Error, String), - #[error("cannot sort messages in mailbox {1} with query: {2}")] - SortMsgsError(#[source] imap::Error, String, String), - #[error("cannot search messages in mailbox {1} with query: {2}")] - SearchMsgsError(#[source] imap::Error, String, String), - #[error("cannot expunge mailbox {1}")] - ExpungeError(#[source] imap::Error, String), - #[error("cannot add flags {1} to message(s) {2}")] - AddFlagsError(#[source] imap::Error, Flags, String), - #[error("cannot set flags {1} to message(s) {2}")] - SetFlagsError(#[source] imap::Error, Flags, String), - #[error("cannot delete flags {1} to message(s) {2}")] - DelFlagsError(#[source] imap::Error, Flags, String), - #[error("cannot logout from imap server")] - LogoutError(#[source] imap::Error), - - #[error(transparent)] - AccountError(#[from] account::AccountError), - #[error(transparent)] - MsgError(#[from] msg::Error), -} - -pub type Result = result::Result; diff --git a/lib/src/backend/imap/imap_backend.rs b/lib/src/backend/imap/imap_backend.rs deleted file mode 100644 index eb8f2a0..0000000 --- a/lib/src/backend/imap/imap_backend.rs +++ /dev/null @@ -1,441 +0,0 @@ -//! IMAP backend module. -//! -//! This module contains the definition of the IMAP backend. - -use imap::types::NameAttribute; -use log::{debug, log_enabled, trace, Level}; -use native_tls::{TlsConnector, TlsStream}; -use std::{collections::HashSet, convert::TryInto, net::TcpStream, thread}; - -use crate::{ - account::{Account, ImapBackendConfig}, - backend::{ - backend::Result, from_imap_fetch, from_imap_fetches, - imap::msg_sort_criterion::SortCriteria, imap::Error, into_imap_flags, Backend, - }, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Flags, Msg}, - process, -}; - -type ImapSess = imap::Session>; - -pub struct ImapBackend<'a> { - account_config: &'a Account, - imap_config: &'a ImapBackendConfig, - sess: Option, -} - -impl<'a> ImapBackend<'a> { - pub fn new(account_config: &'a Account, imap_config: &'a ImapBackendConfig) -> Self { - Self { - account_config, - imap_config, - sess: None, - } - } - - fn sess(&mut self) -> Result<&mut ImapSess> { - if self.sess.is_none() { - debug!("create TLS builder"); - debug!("insecure: {}", self.imap_config.imap_insecure); - let builder = TlsConnector::builder() - .danger_accept_invalid_certs(self.imap_config.imap_insecure) - .danger_accept_invalid_hostnames(self.imap_config.imap_insecure) - .build() - .map_err(Error::CreateTlsConnectorError)?; - - debug!("create client"); - debug!("host: {}", self.imap_config.imap_host); - debug!("port: {}", self.imap_config.imap_port); - debug!("starttls: {}", self.imap_config.imap_starttls); - let mut client_builder = - imap::ClientBuilder::new(&self.imap_config.imap_host, self.imap_config.imap_port); - if self.imap_config.imap_starttls { - client_builder.starttls(); - } - let client = client_builder - .connect(|domain, tcp| Ok(TlsConnector::connect(&builder, domain, tcp)?)) - .map_err(Error::ConnectImapServerError)?; - - debug!("create session"); - debug!("login: {}", self.imap_config.imap_login); - debug!("passwd cmd: {}", self.imap_config.imap_passwd_cmd); - let mut sess = client - .login( - &self.imap_config.imap_login, - &self.imap_config.imap_passwd()?, - ) - .map_err(|res| Error::LoginImapServerError(res.0))?; - sess.debug = log_enabled!(Level::Trace); - self.sess = Some(sess); - } - - let sess = match self.sess { - Some(ref mut sess) => Ok(sess), - None => Err(Error::GetSessionError), - }?; - - Ok(sess) - } - - fn search_new_msgs(&mut self, query: &str) -> Result> { - let uids: Vec = self - .sess()? - .uid_search(query) - .map_err(Error::SearchNewMsgsError)? - .into_iter() - .collect(); - debug!("found {} new messages", uids.len()); - trace!("uids: {:?}", uids); - - Ok(uids) - } - - pub fn notify(&mut self, keepalive: u64, mbox: &str) -> Result<()> { - debug!("notify"); - - debug!("examine mailbox {:?}", mbox); - self.sess()? - .examine(mbox) - .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; - - debug!("init messages hashset"); - let mut msgs_set: HashSet = self - .search_new_msgs(&self.account_config.notify_query)? - .iter() - .cloned() - .collect::>(); - trace!("messages hashset: {:?}", msgs_set); - - loop { - debug!("begin loop"); - self.sess()? - .idle() - .and_then(|mut idle| { - idle.set_keepalive(std::time::Duration::new(keepalive, 0)); - idle.wait_keepalive_while(|res| { - // TODO: handle response - trace!("idle response: {:?}", res); - false - }) - }) - .map_err(Error::StartIdleModeError)?; - - let uids: Vec = self - .search_new_msgs(&self.account_config.notify_query)? - .into_iter() - .filter(|uid| -> bool { msgs_set.get(uid).is_none() }) - .collect(); - debug!("found {} new messages not in hashset", uids.len()); - trace!("messages hashet: {:?}", msgs_set); - - if !uids.is_empty() { - let uids = uids - .iter() - .map(|uid| uid.to_string()) - .collect::>() - .join(","); - let fetches = self - .sess()? - .uid_fetch(uids, "(UID ENVELOPE)") - .map_err(Error::FetchNewMsgsEnvelopeError)?; - - for fetch in fetches.iter() { - let msg = from_imap_fetch(fetch)?; - let uid = fetch.uid.ok_or_else(|| Error::GetUidError(fetch.message))?; - - let from = msg.sender.to_owned().into(); - self.account_config.run_notify_cmd(&msg.subject, &from)?; - - debug!("notify message: {}", uid); - trace!("message: {:?}", msg); - - debug!("insert message {} in hashset", uid); - msgs_set.insert(uid); - trace!("messages hashset: {:?}", msgs_set); - } - } - - debug!("end loop"); - } - } - - pub fn watch(&mut self, keepalive: u64, mbox: &str) -> Result<()> { - debug!("examine mailbox: {}", mbox); - - self.sess()? - .examine(mbox) - .map_err(|err| Error::ExamineMboxError(err, mbox.to_owned()))?; - - loop { - debug!("begin loop"); - self.sess()? - .idle() - .and_then(|mut idle| { - idle.set_keepalive(std::time::Duration::new(keepalive, 0)); - idle.wait_keepalive_while(|res| { - // TODO: handle response - trace!("idle response: {:?}", res); - false - }) - }) - .map_err(Error::StartIdleModeError)?; - - let cmds = self.account_config.watch_cmds.clone(); - thread::spawn(move || { - debug!("batch execution of {} cmd(s)", cmds.len()); - cmds.iter().for_each(|cmd| { - debug!("running command {:?}…", cmd); - let res = process::run(cmd); - debug!("{:?}", res); - }) - }); - - debug!("end loop"); - } - } -} - -impl<'a> Backend<'a> for ImapBackend<'a> { - fn add_mbox(&mut self, mbox: &str) -> Result<()> { - trace!(">> add mailbox"); - - self.sess()? - .create(mbox) - .map_err(|err| Error::CreateMboxError(err, mbox.to_owned()))?; - - trace!("<< add mailbox"); - Ok(()) - } - - fn get_mboxes(&mut self) -> Result { - trace!(">> get imap mailboxes"); - - let imap_mboxes = self - .sess()? - .list(Some(""), Some("*")) - .map_err(Error::ListMboxesError)?; - let mboxes = Mboxes { - mboxes: imap_mboxes - .iter() - .map(|imap_mbox| Mbox { - delim: imap_mbox.delimiter().unwrap_or_default().into(), - name: imap_mbox.name().into(), - desc: imap_mbox - .attributes() - .iter() - .map(|attr| match attr { - NameAttribute::Marked => "Marked", - NameAttribute::Unmarked => "Unmarked", - NameAttribute::NoSelect => "NoSelect", - NameAttribute::NoInferiors => "NoInferiors", - NameAttribute::Custom(custom) => custom.trim_start_matches('\\'), - }) - .collect::>() - .join(", "), - }) - .collect(), - }; - - trace!("imap mailboxes: {:?}", mboxes); - trace!("<< get imap mailboxes"); - Ok(mboxes) - } - - fn del_mbox(&mut self, mbox: &str) -> Result<()> { - trace!(">> delete imap mailbox"); - - self.sess()? - .delete(mbox) - .map_err(|err| Error::DeleteMboxError(err, mbox.to_owned()))?; - - trace!("<< delete imap mailbox"); - Ok(()) - } - - fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result { - let last_seq = self - .sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? - .exists as usize; - debug!("last sequence number: {:?}", last_seq); - if last_seq == 0 { - return Ok(Envelopes::default()); - } - - let range = if page_size > 0 { - let cursor = page * page_size; - let begin = 1.max(last_seq - cursor); - let end = begin - begin.min(page_size) + 1; - format!("{}:{}", end, begin) - } else { - String::from("1:*") - }; - debug!("range: {:?}", range); - - let fetches = self - .sess()? - .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - - let envelopes = from_imap_fetches(fetches)?; - Ok(envelopes) - } - - fn search_envelopes( - &mut self, - mbox: &str, - query: &str, - sort: &str, - page_size: usize, - page: usize, - ) -> Result { - let last_seq = self - .sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? - .exists; - debug!("last sequence number: {:?}", last_seq); - if last_seq == 0 { - return Ok(Envelopes::default()); - } - - let begin = page * page_size; - let end = begin + (page_size - 1); - let seqs: Vec = if sort.is_empty() { - self.sess()? - .search(query) - .map_err(|err| Error::SearchMsgsError(err, mbox.to_owned(), query.to_owned()))? - .iter() - .map(|seq| seq.to_string()) - .collect() - } else { - let sort: SortCriteria = sort.try_into()?; - let charset = imap::extensions::sort::SortCharset::Utf8; - self.sess()? - .sort(&sort, charset, query) - .map_err(|err| Error::SortMsgsError(err, mbox.to_owned(), query.to_owned()))? - .iter() - .map(|seq| seq.to_string()) - .collect() - }; - if seqs.is_empty() { - return Ok(Envelopes::default()); - } - - let range = seqs[begin..end.min(seqs.len())].join(","); - let fetches = self - .sess()? - .fetch(&range, "(ENVELOPE FLAGS INTERNALDATE)") - .map_err(|err| Error::FetchMsgsByRangeError(err, range.to_owned()))?; - - let envelopes = from_imap_fetches(fetches)?; - Ok(envelopes) - } - - fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result { - let flags: Flags = flags.into(); - self.sess()? - .append(mbox, msg) - .flags(into_imap_flags(&flags)) - .finish() - .map_err(|err| Error::AppendMsgError(err, mbox.to_owned()))?; - let last_seq = self - .sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))? - .exists; - Ok(last_seq.to_string()) - } - - fn get_msg(&mut self, mbox: &str, seq: &str) -> Result { - self.sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; - let fetches = self - .sess()? - .fetch(seq, "(FLAGS INTERNALDATE BODY[])") - .map_err(|err| Error::FetchMsgsBySeqError(err, seq.to_owned()))?; - let fetch = fetches - .first() - .ok_or_else(|| Error::FindMsgError(seq.to_owned()))?; - let msg_raw = fetch.body().unwrap_or_default().to_owned(); - let mut msg = Msg::from_parsed_mail( - mailparse::parse_mail(&msg_raw) - .map_err(|err| Error::ParseMsgError(err, seq.to_owned()))?, - self.account_config, - )?; - msg.raw = msg_raw; - Ok(msg) - } - - fn copy_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> { - let msg = self.get_msg(&mbox_src, seq)?.raw; - println!("raw: {:?}", String::from_utf8(msg.to_vec()).unwrap()); - self.add_msg(&mbox_dst, &msg, "seen")?; - Ok(()) - } - - fn move_msg(&mut self, mbox_src: &str, mbox_dst: &str, seq: &str) -> Result<()> { - let msg = self.get_msg(mbox_src, seq)?.raw; - self.add_flags(mbox_src, seq, "seen deleted")?; - self.add_msg(&mbox_dst, &msg, "seen")?; - Ok(()) - } - - fn del_msg(&mut self, mbox: &str, seq: &str) -> Result<()> { - self.add_flags(mbox, seq, "deleted") - } - - fn add_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: Flags = flags.into(); - self.sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; - self.sess()? - .store(seq_range, format!("+FLAGS ({})", flags)) - .map_err(|err| Error::AddFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; - self.sess()? - .expunge() - .map_err(|err| Error::ExpungeError(err, mbox.to_owned()))?; - Ok(()) - } - - fn set_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: Flags = flags.into(); - self.sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; - self.sess()? - .store(seq_range, format!("FLAGS ({})", flags)) - .map_err(|err| Error::SetFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; - Ok(()) - } - - fn del_flags(&mut self, mbox: &str, seq_range: &str, flags: &str) -> Result<()> { - let flags: Flags = flags.into(); - self.sess()? - .select(mbox) - .map_err(|err| Error::SelectMboxError(err, mbox.to_owned()))?; - self.sess()? - .store(seq_range, format!("-FLAGS ({})", flags)) - .map_err(|err| Error::DelFlagsError(err, flags.to_owned(), seq_range.to_owned()))?; - Ok(()) - } - - fn disconnect(&mut self) -> Result<()> { - trace!(">> imap logout"); - - if let Some(ref mut sess) = self.sess { - debug!("logout from imap server"); - sess.logout().map_err(Error::LogoutError)?; - } else { - debug!("no session found"); - } - - trace!("<< imap logout"); - Ok(()) - } -} diff --git a/lib/src/backend/imap/imap_envelope.rs b/lib/src/backend/imap/imap_envelope.rs deleted file mode 100644 index 639d009..0000000 --- a/lib/src/backend/imap/imap_envelope.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! IMAP envelope module. -//! -//! This module provides IMAP types and conversion utilities related -//! to the envelope. - -use rfc2047_decoder; - -use crate::{ - backend::{ - from_imap_flags, - imap::{Error, Result}, - }, - msg::Envelope, -}; - -/// Represents the raw envelope returned by the `imap` crate. -pub type ImapFetch = imap::types::Fetch; - -pub fn from_imap_fetch(fetch: &ImapFetch) -> Result { - let envelope = fetch - .envelope() - .ok_or_else(|| Error::GetEnvelopeError(fetch.message))?; - - let id = fetch.message.to_string(); - - let flags = from_imap_flags(fetch.flags()); - - let subject = envelope - .subject - .as_ref() - .map(|subj| { - rfc2047_decoder::decode(subj) - .map_err(|err| Error::DecodeSubjectError(err, fetch.message)) - }) - .unwrap_or_else(|| Ok(String::default()))?; - - let sender = envelope - .sender - .as_ref() - .and_then(|addrs| addrs.get(0)) - .or_else(|| envelope.from.as_ref().and_then(|addrs| addrs.get(0))) - .ok_or_else(|| Error::GetSenderError(fetch.message))?; - let sender = if let Some(ref name) = sender.name { - rfc2047_decoder::decode(&name.to_vec()) - .map_err(|err| Error::DecodeSenderNameError(err, fetch.message))? - } else { - let mbox = sender - .mailbox - .as_ref() - .ok_or_else(|| Error::GetSenderError(fetch.message)) - .and_then(|mbox| { - rfc2047_decoder::decode(&mbox.to_vec()) - .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) - })?; - let host = sender - .host - .as_ref() - .ok_or_else(|| Error::GetSenderError(fetch.message)) - .and_then(|host| { - rfc2047_decoder::decode(&host.to_vec()) - .map_err(|err| Error::DecodeSenderNameError(err, fetch.message)) - })?; - format!("{}@{}", mbox, host) - }; - - let date = fetch - .internal_date() - .map(|date| date.naive_local().to_string()); - - Ok(Envelope { - id: id.clone(), - internal_id: id, - flags, - subject, - sender, - date, - }) -} diff --git a/lib/src/backend/imap/imap_envelopes.rs b/lib/src/backend/imap/imap_envelopes.rs deleted file mode 100644 index 3cbb010..0000000 --- a/lib/src/backend/imap/imap_envelopes.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{ - backend::{ - imap::{from_imap_fetch, Result}, - ImapFetch, - }, - msg::Envelopes, -}; - -/// Represents the list of raw envelopes returned by the `imap` crate. -pub type ImapFetches = imap::types::ZeroCopy>; - -pub fn from_imap_fetches(fetches: ImapFetches) -> Result { - let mut envelopes = Envelopes::default(); - for fetch in fetches.iter().rev() { - envelopes.push(from_imap_fetch(fetch)?); - } - Ok(envelopes) -} diff --git a/lib/src/backend/imap/imap_flag.rs b/lib/src/backend/imap/imap_flag.rs deleted file mode 100644 index 58ec612..0000000 --- a/lib/src/backend/imap/imap_flag.rs +++ /dev/null @@ -1,15 +0,0 @@ -use crate::msg::Flag; - -pub fn from_imap_flag(imap_flag: &imap::types::Flag<'_>) -> Flag { - match imap_flag { - imap::types::Flag::Seen => Flag::Seen, - imap::types::Flag::Answered => Flag::Answered, - imap::types::Flag::Flagged => Flag::Flagged, - imap::types::Flag::Deleted => Flag::Deleted, - imap::types::Flag::Draft => Flag::Draft, - imap::types::Flag::Recent => Flag::Recent, - imap::types::Flag::MayCreate => Flag::Custom(String::from("MayCreate")), - imap::types::Flag::Custom(flag) => Flag::Custom(flag.to_string()), - flag => Flag::Custom(flag.to_string()), - } -} diff --git a/lib/src/backend/imap/imap_flags.rs b/lib/src/backend/imap/imap_flags.rs deleted file mode 100644 index 3aa42d5..0000000 --- a/lib/src/backend/imap/imap_flags.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::{ - backend::from_imap_flag, - msg::{Flag, Flags}, -}; - -pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec> { - flags - .iter() - .map(|flag| match flag { - Flag::Seen => imap::types::Flag::Seen, - Flag::Answered => imap::types::Flag::Answered, - Flag::Flagged => imap::types::Flag::Flagged, - Flag::Deleted => imap::types::Flag::Deleted, - Flag::Draft => imap::types::Flag::Draft, - Flag::Recent => imap::types::Flag::Recent, - Flag::Custom(flag) => imap::types::Flag::Custom(flag.into()), - }) - .collect() -} - -pub fn from_imap_flags(imap_flags: &[imap::types::Flag<'_>]) -> Flags { - imap_flags.iter().map(from_imap_flag).collect() -} diff --git a/lib/src/backend/imap/msg_sort_criterion.rs b/lib/src/backend/imap/msg_sort_criterion.rs deleted file mode 100644 index 222677b..0000000 --- a/lib/src/backend/imap/msg_sort_criterion.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! Message sort criteria module. -//! -//! This module regroups everything related to deserialization of -//! message sort criteria. - -use std::{convert::TryFrom, ops::Deref}; - -use crate::backend::imap::Error; - -/// Represents the message sort criteria. It is just a wrapper around -/// the `imap::extensions::sort::SortCriterion`. -pub struct SortCriteria<'a>(Vec>); - -impl<'a> Deref for SortCriteria<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'a> TryFrom<&'a str> for SortCriteria<'a> { - type Error = Error; - - fn try_from(criteria_str: &'a str) -> Result { - let mut criteria = vec![]; - for criterion_str in criteria_str.split(" ") { - criteria.push(match criterion_str.trim() { - "arrival:asc" | "arrival" => Ok(imap::extensions::sort::SortCriterion::Arrival), - "arrival:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::Arrival, - )), - "cc:asc" | "cc" => Ok(imap::extensions::sort::SortCriterion::Cc), - "cc:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::Cc, - )), - "date:asc" | "date" => Ok(imap::extensions::sort::SortCriterion::Date), - "date:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::Date, - )), - "from:asc" | "from" => Ok(imap::extensions::sort::SortCriterion::From), - "from:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::From, - )), - "size:asc" | "size" => Ok(imap::extensions::sort::SortCriterion::Size), - "size:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::Size, - )), - "subject:asc" | "subject" => Ok(imap::extensions::sort::SortCriterion::Subject), - "subject:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::Subject, - )), - "to:asc" | "to" => Ok(imap::extensions::sort::SortCriterion::To), - "to:desc" => Ok(imap::extensions::sort::SortCriterion::Reverse( - &imap::extensions::sort::SortCriterion::To, - )), - _ => Err(Error::ParseSortCriterionError(criterion_str.to_owned())), - }?); - } - Ok(Self(criteria)) - } -} diff --git a/lib/src/backend/maildir/error.rs b/lib/src/backend/maildir/error.rs deleted file mode 100644 index 898d249..0000000 --- a/lib/src/backend/maildir/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::{io, path}; - -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum MaildirError { - #[error("cannot find maildir sender")] - FindSenderError, - #[error("cannot read maildir directory {0}")] - ReadDirError(path::PathBuf), - #[error("cannot parse maildir subdirectory {0}")] - ParseSubdirError(path::PathBuf), - #[error("cannot get maildir envelopes at page {0}")] - GetEnvelopesOutOfBoundsError(usize), - #[error("cannot search maildir envelopes: feature not implemented")] - SearchEnvelopesUnimplementedError, - #[error("cannot get maildir message {0}")] - GetMsgError(String), - #[error("cannot decode maildir entry")] - DecodeEntryError(#[source] io::Error), - #[error("cannot parse maildir message")] - ParseMsgError(#[source] maildir::MailEntryError), - #[error("cannot decode header {0}")] - DecodeHeaderError(#[source] rfc2047_decoder::Error, String), - #[error("cannot parse maildir message header {0}")] - ParseHeaderError(#[source] mailparse::MailParseError, String), - #[error("cannot create maildir subdirectory {1}")] - CreateSubdirError(#[source] io::Error, String), - #[error("cannot decode maildir subdirectory")] - DecodeSubdirError(#[source] io::Error), - #[error("cannot delete subdirectories at {1}")] - DeleteAllDirError(#[source] io::Error, path::PathBuf), - #[error("cannot get current directory")] - GetCurrentDirError(#[source] io::Error), - #[error("cannot store maildir message with flags")] - StoreWithFlagsError(#[source] maildir::MaildirError), - #[error("cannot copy maildir message")] - CopyMsgError(#[source] io::Error), - #[error("cannot move maildir message")] - MoveMsgError(#[source] io::Error), - #[error("cannot delete maildir message")] - DelMsgError(#[source] io::Error), - #[error("cannot add maildir flags")] - AddFlagsError(#[source] io::Error), - #[error("cannot set maildir flags")] - SetFlagsError(#[source] io::Error), - #[error("cannot remove maildir flags")] - DelFlagsError(#[source] io::Error), -} diff --git a/lib/src/backend/maildir/maildir_backend.rs b/lib/src/backend/maildir/maildir_backend.rs deleted file mode 100644 index 2c50328..0000000 --- a/lib/src/backend/maildir/maildir_backend.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! Maildir backend module. -//! -//! This module contains the definition of the maildir backend and its -//! traits implementation. - -use log::{debug, info, trace}; -use std::{env, ffi::OsStr, fs, path::PathBuf}; - -use crate::{ - account::{Account, MaildirBackendConfig}, - backend::{backend::Result, maildir_envelopes, maildir_flags, Backend, IdMapper}, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Flags, Msg}, -}; - -use super::MaildirError; - -/// Represents the maildir backend. -pub struct MaildirBackend<'a> { - account_config: &'a Account, - mdir: maildir::Maildir, -} - -impl<'a> MaildirBackend<'a> { - pub fn new(account_config: &'a Account, maildir_config: &'a MaildirBackendConfig) -> Self { - Self { - account_config, - mdir: maildir_config.maildir_dir.clone().into(), - } - } - - fn validate_mdir_path(&self, mdir_path: PathBuf) -> Result { - let path = if mdir_path.is_dir() { - Ok(mdir_path) - } else { - Err(MaildirError::ReadDirError(mdir_path.to_owned())) - }?; - Ok(path) - } - - /// Creates a maildir instance from a string slice. - pub fn get_mdir_from_dir(&self, dir: &str) -> Result { - let dir = self.account_config.get_mbox_alias(dir)?; - - // If the dir points to the inbox folder, creates a maildir - // instance from the root folder. - if &dir == "inbox" { - return self - .validate_mdir_path(self.mdir.path().to_owned()) - .map(maildir::Maildir::from); - } - - // If the dir is a valid maildir path, creates a maildir - // instance from it. First checks for absolute path, - self.validate_mdir_path((&dir).into()) - // then for relative path to `maildir-dir`, - .or_else(|_| self.validate_mdir_path(self.mdir.path().join(&dir))) - // and finally for relative path to the current directory. - .or_else(|_| { - self.validate_mdir_path( - env::current_dir() - .map_err(MaildirError::GetCurrentDirError)? - .join(&dir), - ) - }) - .or_else(|_| { - // Otherwise creates a maildir instance from a maildir - // subdirectory by adding a "." in front of the name - // as described in the [spec]. - // - // [spec]: http://www.courier-mta.org/imap/README.maildirquota.html - self.validate_mdir_path(self.mdir.path().join(format!(".{}", dir))) - }) - .map(maildir::Maildir::from) - } -} - -impl<'a> Backend<'a> for MaildirBackend<'a> { - fn add_mbox(&mut self, subdir: &str) -> Result<()> { - info!(">> add maildir subdir"); - debug!("subdir: {:?}", subdir); - - let path = self.mdir.path().join(format!(".{}", subdir)); - trace!("subdir path: {:?}", path); - - fs::create_dir(&path) - .map_err(|err| MaildirError::CreateSubdirError(err, subdir.to_owned()))?; - - info!("<< add maildir subdir"); - Ok(()) - } - - fn get_mboxes(&mut self) -> Result { - trace!(">> get maildir mailboxes"); - - let mut mboxes = Mboxes::default(); - for (name, desc) in &self.account_config.mailboxes { - mboxes.push(Mbox { - delim: String::from("/"), - name: name.into(), - desc: desc.into(), - }) - } - for entry in self.mdir.list_subdirs() { - let dir = entry.map_err(MaildirError::DecodeSubdirError)?; - let dirname = dir.path().file_name(); - mboxes.push(Mbox { - delim: String::from("/"), - name: dirname - .and_then(OsStr::to_str) - .and_then(|s| if s.len() < 2 { None } else { Some(&s[1..]) }) - .ok_or_else(|| MaildirError::ParseSubdirError(dir.path().to_owned()))? - .into(), - ..Mbox::default() - }); - } - - trace!("maildir mailboxes: {:?}", mboxes); - trace!("<< get maildir mailboxes"); - Ok(mboxes) - } - - fn del_mbox(&mut self, dir: &str) -> Result<()> { - info!(">> delete maildir dir"); - debug!("dir: {:?}", dir); - - let path = self.mdir.path().join(format!(".{}", dir)); - trace!("dir path: {:?}", path); - - fs::remove_dir_all(&path) - .map_err(|err| MaildirError::DeleteAllDirError(err, path.to_owned()))?; - - info!("<< delete maildir dir"); - Ok(()) - } - - fn get_envelopes(&mut self, dir: &str, page_size: usize, page: usize) -> Result { - info!(">> get maildir envelopes"); - debug!("dir: {:?}", dir); - debug!("page size: {:?}", page_size); - debug!("page: {:?}", page); - - let mdir = self.get_mdir_from_dir(dir)?; - - // Reads envelopes from the "cur" folder of the selected - // maildir. - let mut envelopes = maildir_envelopes::from_maildir_entries(mdir.list_cur())?; - debug!("envelopes len: {:?}", envelopes.len()); - trace!("envelopes: {:?}", envelopes); - - // Calculates pagination boundaries. - let page_begin = page * page_size; - debug!("page begin: {:?}", page_begin); - if page_begin > envelopes.len() { - return Err(MaildirError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; - } - let page_end = envelopes.len().min(page_begin + page_size); - debug!("page end: {:?}", page_end); - - // Sorts envelopes by most recent date. - envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); - - // Applies pagination boundaries. - envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); - - // Appends envelopes hash to the id mapper cache file and - // calculates the new short hash length. The short hash length - // represents the minimum hash length possible to avoid - // conflicts. - let short_hash_len = { - let mut mapper = IdMapper::new(mdir.path())?; - let entries = envelopes - .iter() - .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) - .collect(); - mapper.append(entries)? - }; - debug!("short hash length: {:?}", short_hash_len); - - // Shorten envelopes hash. - envelopes - .iter_mut() - .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); - - info!("<< get maildir envelopes"); - Ok(envelopes) - } - - fn search_envelopes( - &mut self, - _dir: &str, - _query: &str, - _sort: &str, - _page_size: usize, - _page: usize, - ) -> Result { - info!(">> search maildir envelopes"); - info!("<< search maildir envelopes"); - Err(MaildirError::SearchEnvelopesUnimplementedError)? - } - - fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result { - info!(">> add maildir message"); - debug!("dir: {:?}", dir); - debug!("flags: {:?}", flags); - - let flags = Flags::from(flags); - debug!("flags: {:?}", flags); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = mdir - .store_cur_with_flags(msg, &maildir_flags::to_normalized_string(&flags)) - .map_err(MaildirError::StoreWithFlagsError)?; - debug!("id: {:?}", id); - let hash = format!("{:x}", md5::compute(&id)); - debug!("hash: {:?}", hash); - - // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir.path())?; - mapper.append(vec![(hash.clone(), id.clone())])?; - - info!("<< add maildir message"); - Ok(hash) - } - - fn get_msg(&mut self, dir: &str, short_hash: &str) -> Result { - info!(">> get maildir message"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - debug!("id: {:?}", id); - let mut mail_entry = mdir - .find(&id) - .ok_or_else(|| MaildirError::GetMsgError(id.to_owned()))?; - let parsed_mail = mail_entry.parsed().map_err(MaildirError::ParseMsgError)?; - let msg = Msg::from_parsed_mail(parsed_mail, self.account_config)?; - trace!("message: {:?}", msg); - - info!("<< get maildir message"); - Ok(msg) - } - - fn copy_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { - info!(">> copy maildir message"); - debug!("source dir: {:?}", dir_src); - debug!("destination dir: {:?}", dir_dst); - - let mdir_src = self.get_mdir_from_dir(dir_src)?; - let mdir_dst = self.get_mdir_from_dir(dir_dst)?; - let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; - debug!("id: {:?}", id); - - mdir_src - .copy_to(&id, &mdir_dst) - .map_err(MaildirError::CopyMsgError)?; - - // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path())?; - let hash = format!("{:x}", md5::compute(&id)); - mapper.append(vec![(hash.clone(), id.clone())])?; - - info!("<< copy maildir message"); - Ok(()) - } - - fn move_msg(&mut self, dir_src: &str, dir_dst: &str, short_hash: &str) -> Result<()> { - info!(">> move maildir message"); - debug!("source dir: {:?}", dir_src); - debug!("destination dir: {:?}", dir_dst); - - let mdir_src = self.get_mdir_from_dir(dir_src)?; - let mdir_dst = self.get_mdir_from_dir(dir_dst)?; - let id = IdMapper::new(mdir_src.path())?.find(short_hash)?; - debug!("id: {:?}", id); - - mdir_src - .move_to(&id, &mdir_dst) - .map_err(MaildirError::MoveMsgError)?; - - // Appends hash entry to the id mapper cache file. - let mut mapper = IdMapper::new(mdir_dst.path())?; - let hash = format!("{:x}", md5::compute(&id)); - mapper.append(vec![(hash.clone(), id.clone())])?; - - info!("<< move maildir message"); - Ok(()) - } - - fn del_msg(&mut self, dir: &str, short_hash: &str) -> Result<()> { - info!(">> delete maildir message"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - debug!("id: {:?}", id); - mdir.delete(&id).map_err(MaildirError::DelMsgError)?; - - info!("<< delete maildir message"); - Ok(()) - } - - fn add_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> add maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - let flags = Flags::from(flags); - debug!("flags: {:?}", flags); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - debug!("id: {:?}", id); - - mdir.add_flags(&id, &maildir_flags::to_normalized_string(&flags)) - .map_err(MaildirError::AddFlagsError)?; - - info!("<< add maildir message flags"); - Ok(()) - } - - fn set_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> set maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - let flags = Flags::from(flags); - debug!("flags: {:?}", flags); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - debug!("id: {:?}", id); - mdir.set_flags(&id, &maildir_flags::to_normalized_string(&flags)) - .map_err(MaildirError::SetFlagsError)?; - - info!("<< set maildir message flags"); - Ok(()) - } - - fn del_flags(&mut self, dir: &str, short_hash: &str, flags: &str) -> Result<()> { - info!(">> delete maildir message flags"); - debug!("dir: {:?}", dir); - debug!("short hash: {:?}", short_hash); - let flags = Flags::from(flags); - debug!("flags: {:?}", flags); - - let mdir = self.get_mdir_from_dir(dir)?; - let id = IdMapper::new(mdir.path())?.find(short_hash)?; - debug!("id: {:?}", id); - mdir.remove_flags(&id, &maildir_flags::to_normalized_string(&flags)) - .map_err(MaildirError::DelFlagsError)?; - - info!("<< delete maildir message flags"); - Ok(()) - } -} diff --git a/lib/src/backend/maildir/maildir_envelope.rs b/lib/src/backend/maildir/maildir_envelope.rs deleted file mode 100644 index 58966fd..0000000 --- a/lib/src/backend/maildir/maildir_envelope.rs +++ /dev/null @@ -1,72 +0,0 @@ -use chrono::DateTime; -use log::trace; - -use crate::{ - backend::{backend::Result, maildir_flags}, - msg::{from_slice_to_addrs, Addr, Envelope}, -}; - -use super::MaildirError; - -/// Represents the raw envelope returned by the `maildir` crate. -pub type MaildirEnvelope = maildir::MailEntry; - -pub fn from_maildir_entry(mut entry: MaildirEnvelope) -> Result { - trace!(">> build envelope from maildir parsed mail"); - - let mut envelope = Envelope::default(); - - envelope.internal_id = entry.id().to_owned(); - envelope.id = format!("{:x}", md5::compute(&envelope.internal_id)); - envelope.flags = maildir_flags::from_maildir_entry(&entry); - - let parsed_mail = entry.parsed().map_err(MaildirError::ParseMsgError)?; - - trace!(">> parse headers"); - for h in parsed_mail.get_headers() { - let k = h.get_key(); - trace!("header key: {:?}", k); - - let v = rfc2047_decoder::decode(h.get_value_raw()) - .map_err(|err| MaildirError::DecodeHeaderError(err, k.to_owned()))?; - trace!("header value: {:?}", v); - - match k.to_lowercase().as_str() { - "date" => { - envelope.date = - DateTime::parse_from_rfc2822(v.split_at(v.find(" (").unwrap_or(v.len())).0) - .map(|date| date.naive_local().to_string()) - .ok() - } - "subject" => { - envelope.subject = v.into(); - } - "from" => { - envelope.sender = from_slice_to_addrs(v) - .map_err(|err| MaildirError::ParseHeaderError(err, k.to_owned()))? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => { - group_name.to_owned() - } - }) - .ok_or_else(|| MaildirError::FindSenderError)?; - } - _ => (), - } - } - trace!("<< parse headers"); - - trace!("envelope: {:?}", envelope); - trace!("<< build envelope from maildir parsed mail"); - Ok(envelope) -} diff --git a/lib/src/backend/maildir/maildir_envelopes.rs b/lib/src/backend/maildir/maildir_envelopes.rs deleted file mode 100644 index ff83a58..0000000 --- a/lib/src/backend/maildir/maildir_envelopes.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Maildir mailbox module. -//! -//! This module provides Maildir types and conversion utilities -//! related to the envelope. - -use crate::{backend::backend::Result, msg::Envelopes}; - -use super::{maildir_envelope, MaildirError}; - -/// Represents a list of raw envelopees returned by the `maildir` -/// crate. -pub type MaildirEnvelopes = maildir::MailEntries; - -pub fn from_maildir_entries(mail_entries: MaildirEnvelopes) -> Result { - let mut envelopes = Envelopes::default(); - for entry in mail_entries { - let entry = entry.map_err(MaildirError::DecodeEntryError)?; - envelopes.push(maildir_envelope::from_maildir_entry(entry)?); - } - Ok(envelopes) -} diff --git a/lib/src/backend/maildir/maildir_flag.rs b/lib/src/backend/maildir/maildir_flag.rs deleted file mode 100644 index f506e4a..0000000 --- a/lib/src/backend/maildir/maildir_flag.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::msg::Flag; - -pub fn from_char(c: char) -> Flag { - match c { - 'r' | 'R' => Flag::Answered, - 's' | 'S' => Flag::Seen, - 't' | 'T' => Flag::Deleted, - 'd' | 'D' => Flag::Draft, - 'f' | 'F' => Flag::Flagged, - 'p' | 'P' => Flag::Custom(String::from("Passed")), - flag => Flag::Custom(flag.to_string()), - } -} - -pub fn to_normalized_char(flag: &Flag) -> Option { - match flag { - Flag::Answered => Some('R'), - Flag::Seen => Some('S'), - Flag::Deleted => Some('T'), - Flag::Draft => Some('D'), - Flag::Flagged => Some('F'), - _ => None, - } -} diff --git a/lib/src/backend/maildir/maildir_flags.rs b/lib/src/backend/maildir/maildir_flags.rs deleted file mode 100644 index db537d7..0000000 --- a/lib/src/backend/maildir/maildir_flags.rs +++ /dev/null @@ -1,11 +0,0 @@ -use crate::msg::Flags; - -use super::maildir_flag; - -pub fn from_maildir_entry(entry: &maildir::MailEntry) -> Flags { - entry.flags().chars().map(maildir_flag::from_char).collect() -} - -pub fn to_normalized_string(flags: &Flags) -> String { - String::from_iter(flags.iter().filter_map(maildir_flag::to_normalized_char)) -} diff --git a/lib/src/backend/mod.rs b/lib/src/backend/mod.rs deleted file mode 100644 index 665c543..0000000 --- a/lib/src/backend/mod.rs +++ /dev/null @@ -1,73 +0,0 @@ -pub mod backend; -pub use backend::*; - -pub mod id_mapper; -pub use id_mapper::*; - -#[cfg(feature = "imap-backend")] -pub mod imap { - pub mod imap_backend; - pub use imap_backend::*; - - pub mod imap_envelopes; - pub use imap_envelopes::*; - - pub mod imap_envelope; - pub use imap_envelope::*; - - pub mod imap_flags; - pub use imap_flags::*; - - pub mod imap_flag; - pub use imap_flag::*; - - pub mod msg_sort_criterion; - - pub mod error; - pub use error::*; -} - -#[cfg(feature = "imap-backend")] -pub use self::imap::*; - -#[cfg(feature = "maildir-backend")] -pub mod maildir { - pub mod maildir_backend; - pub use maildir_backend::*; - - pub mod maildir_envelopes; - pub use maildir_envelopes::*; - - pub mod maildir_envelope; - pub use maildir_envelope::*; - - pub mod maildir_flags; - pub use maildir_flags::*; - - pub mod maildir_flag; - pub use maildir_flag::*; - - pub mod error; - pub use error::*; -} - -#[cfg(feature = "maildir-backend")] -pub use self::maildir::*; - -#[cfg(feature = "notmuch-backend")] -pub mod notmuch { - pub mod notmuch_backend; - pub use notmuch_backend::*; - - pub mod notmuch_envelopes; - pub use notmuch_envelopes::*; - - pub mod notmuch_envelope; - pub use notmuch_envelope::*; - - pub mod error; - pub use error::*; -} - -#[cfg(feature = "notmuch-backend")] -pub use self::notmuch::*; diff --git a/lib/src/backend/notmuch/error.rs b/lib/src/backend/notmuch/error.rs deleted file mode 100644 index 5ff1485..0000000 --- a/lib/src/backend/notmuch/error.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::io; - -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum NotmuchError { - #[error("cannot parse notmuch message header {1}")] - ParseMsgHeaderError(#[source] notmuch::Error, String), - #[error("cannot parse notmuch message date {1}")] - ParseMsgDateError(#[source] chrono::ParseError, String), - #[error("cannot find notmuch message header {0}")] - FindMsgHeaderError(String), - #[error("cannot find notmuch message sender")] - FindSenderError, - #[error("cannot parse notmuch message senders {1}")] - ParseSendersError(#[source] mailparse::MailParseError, String), - #[error("cannot open notmuch database")] - OpenDbError(#[source] notmuch::Error), - #[error("cannot build notmuch query")] - BuildQueryError(#[source] notmuch::Error), - #[error("cannot search notmuch envelopes")] - SearchEnvelopesError(#[source] notmuch::Error), - #[error("cannot get notmuch envelopes at page {0}")] - GetEnvelopesOutOfBoundsError(usize), - #[error("cannot add notmuch mailbox: feature not implemented")] - AddMboxUnimplementedError, - #[error("cannot delete notmuch mailbox: feature not implemented")] - DelMboxUnimplementedError, - #[error("cannot copy notmuch message: feature not implemented")] - CopyMsgUnimplementedError, - #[error("cannot move notmuch message: feature not implemented")] - MoveMsgUnimplementedError, - #[error("cannot index notmuch message")] - IndexFileError(#[source] notmuch::Error), - #[error("cannot find notmuch message")] - FindMsgError(#[source] notmuch::Error), - #[error("cannot find notmuch message")] - FindMsgEmptyError, - #[error("cannot read notmuch raw message from file")] - ReadMsgError(#[source] io::Error), - #[error("cannot parse notmuch raw message")] - ParseMsgError(#[source] mailparse::MailParseError), - #[error("cannot delete notmuch message")] - DelMsgError(#[source] notmuch::Error), - #[error("cannot add notmuch tag")] - AddTagError(#[source] notmuch::Error), - #[error("cannot delete notmuch tag")] - DelTagError(#[source] notmuch::Error), -} diff --git a/lib/src/backend/notmuch/notmuch_backend.rs b/lib/src/backend/notmuch/notmuch_backend.rs deleted file mode 100644 index e249d46..0000000 --- a/lib/src/backend/notmuch/notmuch_backend.rs +++ /dev/null @@ -1,366 +0,0 @@ -use log::{debug, info, trace}; -use std::fs; - -use crate::{ - account::{Account, NotmuchBackendConfig}, - backend::{ - backend::Result, notmuch_envelopes, Backend, IdMapper, MaildirBackend, NotmuchError, - }, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Msg}, -}; - -/// Represents the Notmuch backend. -pub struct NotmuchBackend<'a> { - account_config: &'a Account, - notmuch_config: &'a NotmuchBackendConfig, - pub mdir: &'a mut MaildirBackend<'a>, - db: notmuch::Database, -} - -impl<'a> NotmuchBackend<'a> { - pub fn new( - account_config: &'a Account, - notmuch_config: &'a NotmuchBackendConfig, - mdir: &'a mut MaildirBackend<'a>, - ) -> Result> { - info!(">> create new notmuch backend"); - - let backend = Self { - account_config, - notmuch_config, - mdir, - db: notmuch::Database::open( - notmuch_config.notmuch_database_dir.clone(), - notmuch::DatabaseMode::ReadWrite, - ) - .map_err(NotmuchError::OpenDbError)?, - }; - - info!("<< create new notmuch backend"); - Ok(backend) - } - - fn _search_envelopes( - &mut self, - query: &str, - page_size: usize, - page: usize, - ) -> Result { - // Gets envelopes matching the given Notmuch query. - let query_builder = self - .db - .create_query(query) - .map_err(NotmuchError::BuildQueryError)?; - let mut envelopes = notmuch_envelopes::from_notmuch_msgs( - query_builder - .search_messages() - .map_err(NotmuchError::SearchEnvelopesError)?, - )?; - debug!("envelopes len: {:?}", envelopes.len()); - trace!("envelopes: {:?}", envelopes); - - // Calculates pagination boundaries. - let page_begin = page * page_size; - debug!("page begin: {:?}", page_begin); - if page_begin > envelopes.len() { - return Err(NotmuchError::GetEnvelopesOutOfBoundsError(page_begin + 1))?; - } - let page_end = envelopes.len().min(page_begin + page_size); - debug!("page end: {:?}", page_end); - - // Sorts envelopes by most recent date. - envelopes.sort_by(|a, b| b.date.partial_cmp(&a.date).unwrap()); - - // Applies pagination boundaries. - envelopes.envelopes = envelopes[page_begin..page_end].to_owned(); - - // Appends envelopes hash to the id mapper cache file and - // calculates the new short hash length. The short hash length - // represents the minimum hash length possible to avoid - // conflicts. - let short_hash_len = { - let mut mapper = IdMapper::new(&self.notmuch_config.notmuch_database_dir)?; - let entries = envelopes - .iter() - .map(|env| (env.id.to_owned(), env.internal_id.to_owned())) - .collect(); - mapper.append(entries)? - }; - debug!("short hash length: {:?}", short_hash_len); - - // Shorten envelopes hash. - envelopes - .iter_mut() - .for_each(|env| env.id = env.id[0..short_hash_len].to_owned()); - - Ok(envelopes) - } -} - -impl<'a> Backend<'a> for NotmuchBackend<'a> { - fn add_mbox(&mut self, _mbox: &str) -> Result<()> { - info!(">> add notmuch mailbox"); - info!("<< add notmuch mailbox"); - Err(NotmuchError::AddMboxUnimplementedError)? - } - - fn get_mboxes(&mut self) -> Result { - trace!(">> get notmuch virtual mailboxes"); - - let mut mboxes = Mboxes::default(); - for (name, desc) in &self.account_config.mailboxes { - mboxes.push(Mbox { - name: name.into(), - desc: desc.into(), - ..Mbox::default() - }) - } - mboxes.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap()); - - trace!("notmuch virtual mailboxes: {:?}", mboxes); - trace!("<< get notmuch virtual mailboxes"); - Ok(mboxes) - } - - fn del_mbox(&mut self, _mbox: &str) -> Result<()> { - info!(">> delete notmuch mailbox"); - info!("<< delete notmuch mailbox"); - Err(NotmuchError::DelMboxUnimplementedError)? - } - - fn get_envelopes( - &mut self, - virt_mbox: &str, - page_size: usize, - page: usize, - ) -> Result { - info!(">> get notmuch envelopes"); - debug!("virtual mailbox: {:?}", virt_mbox); - debug!("page size: {:?}", page_size); - debug!("page: {:?}", page); - - let query = self - .account_config - .mailboxes - .get(virt_mbox) - .map(|s| s.as_str()) - .unwrap_or("all"); - debug!("query: {:?}", query); - let envelopes = self._search_envelopes(query, page_size, page)?; - - info!("<< get notmuch envelopes"); - Ok(envelopes) - } - - fn search_envelopes( - &mut self, - virt_mbox: &str, - query: &str, - _sort: &str, - page_size: usize, - page: usize, - ) -> Result { - info!(">> search notmuch envelopes"); - debug!("virtual mailbox: {:?}", virt_mbox); - debug!("query: {:?}", query); - debug!("page size: {:?}", page_size); - debug!("page: {:?}", page); - - let query = if query.is_empty() { - self.account_config - .mailboxes - .get(virt_mbox) - .map(|s| s.as_str()) - .unwrap_or("all") - } else { - query - }; - debug!("final query: {:?}", query); - let envelopes = self._search_envelopes(query, page_size, page)?; - - info!("<< search notmuch envelopes"); - Ok(envelopes) - } - - fn add_msg(&mut self, _: &str, msg: &[u8], tags: &str) -> Result { - info!(">> add notmuch envelopes"); - debug!("tags: {:?}", tags); - - let dir = &self.notmuch_config.notmuch_database_dir; - - // Adds the message to the maildir folder and gets its hash. - let hash = self.mdir.add_msg("", msg, "seen")?; - debug!("hash: {:?}", hash); - - // Retrieves the file path of the added message by its maildir - // identifier. - let mut mapper = IdMapper::new(dir)?; - let id = mapper.find(&hash)?; - debug!("id: {:?}", id); - let file_path = dir.join("cur").join(format!("{}:2,S", id)); - debug!("file path: {:?}", file_path); - - println!("file_path: {:?}", file_path); - // Adds the message to the notmuch database by indexing it. - let id = self - .db - .index_file(&file_path, None) - .map_err(NotmuchError::IndexFileError)? - .id() - .to_string(); - let hash = format!("{:x}", md5::compute(&id)); - - // Appends hash entry to the id mapper cache file. - mapper.append(vec![(hash.clone(), id.clone())])?; - - // Attaches tags to the notmuch message. - self.add_flags("", &hash, tags)?; - - info!("<< add notmuch envelopes"); - Ok(hash) - } - - fn get_msg(&mut self, _: &str, short_hash: &str) -> Result { - info!(">> add notmuch envelopes"); - debug!("short hash: {:?}", short_hash); - - let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir)?.find(short_hash)?; - debug!("id: {:?}", id); - let msg_file_path = self - .db - .find_message(&id) - .map_err(NotmuchError::FindMsgError)? - .ok_or_else(|| NotmuchError::FindMsgEmptyError)? - .filename() - .to_owned(); - debug!("message file path: {:?}", msg_file_path); - let raw_msg = fs::read(&msg_file_path).map_err(NotmuchError::ReadMsgError)?; - let msg = mailparse::parse_mail(&raw_msg).map_err(NotmuchError::ParseMsgError)?; - let msg = Msg::from_parsed_mail(msg, &self.account_config)?; - trace!("message: {:?}", msg); - - info!("<< get notmuch message"); - Ok(msg) - } - - fn copy_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { - info!(">> copy notmuch message"); - info!("<< copy notmuch message"); - Err(NotmuchError::CopyMsgUnimplementedError)? - } - - fn move_msg(&mut self, _dir_src: &str, _dir_dst: &str, _short_hash: &str) -> Result<()> { - info!(">> move notmuch message"); - info!("<< move notmuch message"); - Err(NotmuchError::MoveMsgUnimplementedError)? - } - - fn del_msg(&mut self, _virt_mbox: &str, short_hash: &str) -> Result<()> { - info!(">> delete notmuch message"); - debug!("short hash: {:?}", short_hash); - - let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir)?.find(short_hash)?; - debug!("id: {:?}", id); - let msg_file_path = self - .db - .find_message(&id) - .map_err(NotmuchError::FindMsgError)? - .ok_or_else(|| NotmuchError::FindMsgEmptyError)? - .filename() - .to_owned(); - debug!("message file path: {:?}", msg_file_path); - self.db - .remove_message(msg_file_path) - .map_err(NotmuchError::DelMsgError)?; - - info!("<< delete notmuch message"); - Ok(()) - } - - fn add_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { - info!(">> add notmuch message flags"); - debug!("tags: {:?}", tags); - - let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir)?.find(short_hash)?; - debug!("id: {:?}", id); - let query = format!("id:{}", id); - debug!("query: {:?}", query); - let tags: Vec<_> = tags.split_whitespace().collect(); - let query_builder = self - .db - .create_query(&query) - .map_err(NotmuchError::BuildQueryError)?; - let msgs = query_builder - .search_messages() - .map_err(NotmuchError::SearchEnvelopesError)?; - - for msg in msgs { - for tag in tags.iter() { - msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; - } - } - - info!("<< add notmuch message flags"); - Ok(()) - } - - fn set_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { - info!(">> set notmuch message flags"); - debug!("tags: {:?}", tags); - - let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir)?.find(short_hash)?; - debug!("id: {:?}", id); - let query = format!("id:{}", id); - debug!("query: {:?}", query); - let tags: Vec<_> = tags.split_whitespace().collect(); - let query_builder = self - .db - .create_query(&query) - .map_err(NotmuchError::BuildQueryError)?; - let msgs = query_builder - .search_messages() - .map_err(NotmuchError::SearchEnvelopesError)?; - for msg in msgs { - msg.remove_all_tags().map_err(NotmuchError::DelTagError)?; - - for tag in tags.iter() { - msg.add_tag(*tag).map_err(NotmuchError::AddTagError)?; - } - } - - info!("<< set notmuch message flags"); - Ok(()) - } - - fn del_flags(&mut self, _virt_mbox: &str, short_hash: &str, tags: &str) -> Result<()> { - info!(">> delete notmuch message flags"); - debug!("tags: {:?}", tags); - - let dir = &self.notmuch_config.notmuch_database_dir; - let id = IdMapper::new(dir)?.find(short_hash)?; - debug!("id: {:?}", id); - let query = format!("id:{}", id); - debug!("query: {:?}", query); - let tags: Vec<_> = tags.split_whitespace().collect(); - let query_builder = self - .db - .create_query(&query) - .map_err(NotmuchError::BuildQueryError)?; - let msgs = query_builder - .search_messages() - .map_err(NotmuchError::SearchEnvelopesError)?; - for msg in msgs { - for tag in tags.iter() { - msg.remove_tag(*tag).map_err(NotmuchError::DelTagError)?; - } - } - - info!("<< delete notmuch message flags"); - Ok(()) - } -} diff --git a/lib/src/backend/notmuch/notmuch_envelope.rs b/lib/src/backend/notmuch/notmuch_envelope.rs deleted file mode 100644 index 6361a9a..0000000 --- a/lib/src/backend/notmuch/notmuch_envelope.rs +++ /dev/null @@ -1,73 +0,0 @@ -//! Notmuch mailbox module. -//! -//! This module provides Notmuch types and conversion utilities -//! related to the envelope - -use chrono::DateTime; -use log::{info, trace}; - -use crate::{ - backend::{backend::Result, NotmuchError}, - msg::{from_slice_to_addrs, Addr, Envelope, Flag}, -}; - -/// Represents the raw envelope returned by the `notmuch` crate. -pub type RawNotmuchEnvelope = notmuch::Message; - -pub fn from_notmuch_msg(raw_envelope: RawNotmuchEnvelope) -> Result { - info!("begin: try building envelope from notmuch parsed mail"); - - let internal_id = raw_envelope.id().to_string(); - let id = format!("{:x}", md5::compute(&internal_id)); - let subject = raw_envelope - .header("subject") - .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("subject")))? - .unwrap_or_default() - .to_string(); - let sender = raw_envelope - .header("from") - .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("from")))? - .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("from")))? - .to_string(); - let sender = from_slice_to_addrs(&sender) - .map_err(|err| NotmuchError::ParseSendersError(err, sender.to_owned()))? - .and_then(|senders| { - if senders.is_empty() { - None - } else { - Some(senders) - } - }) - .map(|senders| match &senders[0] { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => { - display_name.as_ref().unwrap_or_else(|| addr).to_owned() - } - Addr::Group(mailparse::GroupInfo { group_name, .. }) => group_name.to_owned(), - }) - .ok_or_else(|| NotmuchError::FindSenderError)?; - let date = raw_envelope - .header("date") - .map_err(|err| NotmuchError::ParseMsgHeaderError(err, String::from("date")))? - .ok_or_else(|| NotmuchError::FindMsgHeaderError(String::from("date")))? - .to_string(); - let date = DateTime::parse_from_rfc2822(date.split_at(date.find(" (").unwrap_or(date.len())).0) - .map_err(|err| NotmuchError::ParseMsgDateError(err, date.to_owned())) - .map(|date| date.naive_local().to_string()) - .ok(); - - let envelope = Envelope { - id, - internal_id, - flags: raw_envelope - .tags() - .map(|tag| Flag::Custom(tag.to_string())) - .collect(), - subject, - sender, - date, - }; - trace!("envelope: {:?}", envelope); - - info!("end: try building envelope from notmuch parsed mail"); - Ok(envelope) -} diff --git a/lib/src/backend/notmuch/notmuch_envelopes.rs b/lib/src/backend/notmuch/notmuch_envelopes.rs deleted file mode 100644 index 7bf1240..0000000 --- a/lib/src/backend/notmuch/notmuch_envelopes.rs +++ /dev/null @@ -1,16 +0,0 @@ -use crate::{backend::backend::Result, msg::Envelopes}; - -use super::notmuch_envelope; - -/// Represents a list of raw envelopees returned by the `notmuch` -/// crate. -pub type RawNotmuchEnvelopes = notmuch::Messages; - -pub fn from_notmuch_msgs(msgs: RawNotmuchEnvelopes) -> Result { - let mut envelopes = Envelopes::default(); - for msg in msgs { - let envelope = notmuch_envelope::from_notmuch_msg(msg)?; - envelopes.push(envelope); - } - Ok(envelopes) -} diff --git a/lib/src/lib.rs b/lib/src/lib.rs deleted file mode 100644 index ab692bc..0000000 --- a/lib/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod process; - -pub mod account; -pub mod backend; -pub mod mbox; -pub mod msg; diff --git a/lib/src/mbox/mbox.rs b/lib/src/mbox/mbox.rs deleted file mode 100644 index ceab669..0000000 --- a/lib/src/mbox/mbox.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Mailbox module. -//! -//! This module contains the representation of the mailbox. - -use serde::Serialize; -use std::fmt; - -/// Represents the mailbox. -#[derive(Debug, Default, PartialEq, Eq, Serialize)] -pub struct Mbox { - /// Represents the mailbox hierarchie delimiter. - pub delim: String, - /// Represents the mailbox name. - pub name: String, - /// Represents the mailbox description. - pub desc: String, -} - -impl fmt::Display for Mbox { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} diff --git a/lib/src/mbox/mboxes.rs b/lib/src/mbox/mboxes.rs deleted file mode 100644 index 0adca85..0000000 --- a/lib/src/mbox/mboxes.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Mailboxes module. -//! -//! This module contains the representation of the mailboxes. - -use serde::Serialize; -use std::ops; - -use super::Mbox; - -/// Represents the list of mailboxes. -#[derive(Debug, Default, Serialize)] -pub struct Mboxes { - #[serde(rename = "response")] - pub mboxes: Vec, -} - -impl ops::Deref for Mboxes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.mboxes - } -} - -impl ops::DerefMut for Mboxes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.mboxes - } -} diff --git a/lib/src/mbox/mod.rs b/lib/src/mbox/mod.rs deleted file mode 100644 index 25e70b5..0000000 --- a/lib/src/mbox/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Mailbox module. -//! -//! This module contains everything related to mailboxes. - -mod mbox; -pub use mbox::*; - -mod mboxes; -pub use mboxes::*; diff --git a/lib/src/msg/addr.rs b/lib/src/msg/addr.rs deleted file mode 100644 index 0a8b6d5..0000000 --- a/lib/src/msg/addr.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Module related to email addresses. -//! -//! This module regroups email address entities and converters. - -use mailparse; -use std::{fmt, result}; - -use crate::msg::Result; - -/// Defines a single email address. -pub type Addr = mailparse::MailAddr; - -/// Defines a list of email addresses. -pub type Addrs = mailparse::MailAddrList; - -/// Converts a slice into an optional list of addresses. -pub fn from_slice_to_addrs + fmt::Debug>( - addrs: S, -) -> result::Result, mailparse::MailParseError> { - let addrs = mailparse::addrparse(addrs.as_ref())?; - Ok(if addrs.is_empty() { None } else { Some(addrs) }) -} - -/// Converts a list of addresses into a list of [`lettre::message::Mailbox`]. -pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result> { - let mut sendable_addrs: Vec = vec![]; - for addr in addrs.iter() { - match addr { - Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push( - lettre::message::Mailbox::new(display_name.clone(), addr.parse()?), - ), - Addr::Group(mailparse::GroupInfo { group_name, addrs }) => { - for addr in addrs { - sendable_addrs.push(lettre::message::Mailbox::new( - addr.display_name.clone().or(Some(group_name.clone())), - addr.to_string().parse()?, - )) - } - } - } - } - Ok(sendable_addrs) -} - -/// Converts a list of addresses into a list of [`lettre::Address`]. -pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result> { - let mut sendable_addrs = vec![]; - for addr in addrs.iter() { - match addr { - mailparse::MailAddr::Single(mailparse::SingleInfo { - display_name: _, - addr, - }) => { - sendable_addrs.push(addr.parse()?); - } - mailparse::MailAddr::Group(mailparse::GroupInfo { - group_name: _, - addrs, - }) => { - for addr in addrs { - sendable_addrs.push(addr.addr.parse()?); - } - } - }; - } - Ok(sendable_addrs) -} diff --git a/lib/src/msg/envelope.rs b/lib/src/msg/envelope.rs deleted file mode 100644 index cc04ee3..0000000 --- a/lib/src/msg/envelope.rs +++ /dev/null @@ -1,21 +0,0 @@ -use serde::Serialize; - -use super::Flags; - -/// Represents the message envelope. The envelope is just a message -/// subset, and is mostly used for listings. -#[derive(Debug, Default, Clone, Serialize)] -pub struct Envelope { - /// Represents the message identifier. - pub id: String, - /// Represents the internal message identifier. - pub internal_id: String, - /// Represents the message flags. - pub flags: Flags, - /// Represents the subject of the message. - pub subject: String, - /// Represents the first sender of the message. - pub sender: String, - /// Represents the internal date of the message. - pub date: Option, -} diff --git a/lib/src/msg/envelopes.rs b/lib/src/msg/envelopes.rs deleted file mode 100644 index 9cf85c9..0000000 --- a/lib/src/msg/envelopes.rs +++ /dev/null @@ -1,25 +0,0 @@ -use serde::Serialize; -use std::ops; - -use super::Envelope; - -/// Represents the list of envelopes. -#[derive(Debug, Default, Serialize)] -pub struct Envelopes { - #[serde(rename = "response")] - pub envelopes: Vec, -} - -impl ops::Deref for Envelopes { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.envelopes - } -} - -impl ops::DerefMut for Envelopes { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.envelopes - } -} diff --git a/lib/src/msg/error.rs b/lib/src/msg/error.rs deleted file mode 100644 index 33d4473..0000000 --- a/lib/src/msg/error.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{ - env, io, - path::{self, PathBuf}, - result, -}; -use thiserror::Error; - -use crate::account; - -#[derive(Error, Debug)] -pub enum Error { - #[error("cannot expand attachment path {1}")] - ExpandAttachmentPathError(#[source] shellexpand::LookupError, String), - #[error("cannot read attachment at {1}")] - ReadAttachmentError(#[source] io::Error, PathBuf), - #[error("cannot parse template")] - ParseTplError(#[source] mailparse::MailParseError), - #[error("cannot parse content type of attachment {1}")] - ParseAttachmentContentTypeError(#[source] lettre::message::header::ContentTypeErr, String), - #[error("cannot write temporary multipart on the disk")] - WriteTmpMultipartError(#[source] io::Error), - #[error("cannot write temporary multipart on the disk")] - BuildSendableMsgError(#[source] lettre::error::Error), - #[error("cannot parse {1} value: {2}")] - ParseHeaderError(#[source] mailparse::MailParseError, String, String), - #[error("cannot build envelope")] - BuildEnvelopeError(#[source] lettre::error::Error), - #[error("cannot get file name of attachment {0}")] - GetAttachmentFilenameError(PathBuf), - #[error("cannot parse recipient")] - ParseRecipientError, - - #[error("cannot parse message or address")] - ParseAddressError(#[from] lettre::address::AddressError), - - #[error(transparent)] - AccountError(#[from] account::AccountError), - - #[error("cannot get content type of multipart")] - GetMultipartContentTypeError, - #[error("cannot find encrypted part of multipart")] - GetEncryptedPartMultipartError, - #[error("cannot parse encrypted part of multipart")] - ParseEncryptedPartError(#[source] mailparse::MailParseError), - #[error("cannot get body from encrypted part")] - GetEncryptedPartBodyError(#[source] mailparse::MailParseError), - #[error("cannot write encrypted part to temporary file")] - WriteEncryptedPartBodyError(#[source] io::Error), - #[error("cannot write encrypted part to temporary file")] - DecryptPartError(#[source] account::AccountError), - - #[error("cannot delete local draft: {1}")] - DeleteLocalDraftError(#[source] io::Error, path::PathBuf), -} - -pub type Result = result::Result; diff --git a/lib/src/msg/flag.rs b/lib/src/msg/flag.rs deleted file mode 100644 index 1d37e18..0000000 --- a/lib/src/msg/flag.rs +++ /dev/null @@ -1,27 +0,0 @@ -use serde::Serialize; - -/// Represents the flag variants. -#[derive(Debug, Clone, PartialEq, Eq, Serialize)] -pub enum Flag { - Seen, - Answered, - Flagged, - Deleted, - Draft, - Recent, - Custom(String), -} - -impl From<&str> for Flag { - fn from(flag_str: &str) -> Self { - match flag_str { - "seen" => Flag::Seen, - "answered" | "replied" => Flag::Answered, - "flagged" => Flag::Flagged, - "deleted" | "trashed" => Flag::Deleted, - "draft" => Flag::Draft, - "recent" => Flag::Recent, - flag => Flag::Custom(flag.into()), - } - } -} diff --git a/lib/src/msg/flags.rs b/lib/src/msg/flags.rs deleted file mode 100644 index 28faad1..0000000 --- a/lib/src/msg/flags.rs +++ /dev/null @@ -1,88 +0,0 @@ -use serde::Serialize; -use std::{fmt, ops}; - -use super::Flag; - -/// Represents the list of flags. -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)] -pub struct Flags(pub Vec); - -impl Flags { - /// Builds a symbols string. - pub fn to_symbols_string(&self) -> String { - let mut flags = String::new(); - flags.push_str(if self.contains(&Flag::Seen) { - " " - } else { - "✷" - }); - flags.push_str(if self.contains(&Flag::Answered) { - "↵" - } else { - " " - }); - flags.push_str(if self.contains(&Flag::Flagged) { - "⚑" - } else { - " " - }); - flags - } -} - -impl ops::Deref for Flags { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl ops::DerefMut for Flags { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl fmt::Display for Flags { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut glue = ""; - - for flag in &self.0 { - write!(f, "{}", glue)?; - match flag { - Flag::Seen => write!(f, "\\Seen")?, - Flag::Answered => write!(f, "\\Answered")?, - Flag::Flagged => write!(f, "\\Flagged")?, - Flag::Deleted => write!(f, "\\Deleted")?, - Flag::Draft => write!(f, "\\Draft")?, - Flag::Recent => write!(f, "\\Recent")?, - Flag::Custom(flag) => write!(f, "{}", flag)?, - } - glue = " "; - } - - Ok(()) - } -} - -impl From<&str> for Flags { - fn from(flags: &str) -> Self { - Flags( - flags - .split_whitespace() - .map(|flag| flag.trim().into()) - .collect(), - ) - } -} - -impl FromIterator for Flags { - fn from_iter>(iter: T) -> Self { - let mut flags = Flags::default(); - for flag in iter { - flags.push(flag); - } - flags - } -} diff --git a/lib/src/msg/mod.rs b/lib/src/msg/mod.rs deleted file mode 100644 index 9470357..0000000 --- a/lib/src/msg/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -mod error; -pub use error::*; - -mod flag; -pub use flag::*; - -mod flags; -pub use flags::*; - -mod envelope; -pub use envelope::*; - -mod envelopes; -pub use envelopes::*; - -mod parts; -pub use parts::*; - -mod addr; -pub use addr::*; - -mod tpl; -pub use tpl::*; - -mod msg; -pub use msg::*; - -mod msg_utils; -pub use msg_utils::*; diff --git a/lib/src/msg/msg.rs b/lib/src/msg/msg.rs deleted file mode 100644 index 264624c..0000000 --- a/lib/src/msg/msg.rs +++ /dev/null @@ -1,971 +0,0 @@ -use ammonia; -use chrono::{DateTime, Local, TimeZone, Utc}; -use convert_case::{Case, Casing}; -use html_escape; -use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart}; -use log::{info, trace, warn}; -use regex::Regex; -use std::{ - collections::{HashMap, HashSet}, - convert::TryInto, - env::temp_dir, - fmt::Debug, - fs, - path::PathBuf, -}; -use tree_magic; -use uuid::Uuid; - -use crate::{ - account::{Account, DEFAULT_SIG_DELIM}, - msg::{ - from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr, - Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride, - }, -}; - -/// Representation of a message. -#[derive(Debug, Clone, Default)] -pub struct Msg { - /// The sequence number of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2 - pub id: u32, - - /// The subject of the message. - pub subject: String, - - pub from: Option, - pub reply_to: Option, - pub to: Option, - pub cc: Option, - pub bcc: Option, - pub in_reply_to: Option, - pub message_id: Option, - pub headers: HashMap, - - /// The internal date of the message. - /// - /// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3 - pub date: Option>, - pub parts: Parts, - - pub encrypt: bool, - - pub raw: Vec, -} - -impl Msg { - pub fn attachments(&self) -> Vec { - self.parts - .iter() - .filter_map(|part| match part { - Part::Binary(part) => Some(part.to_owned()), - _ => None, - }) - .collect() - } - - /// Folds string body from all plain text parts into a single - /// string body. If no plain text parts are found, HTML parts are - /// used instead. The result is sanitized (all HTML markup is - /// removed). - pub fn fold_text_plain_parts(&self) -> String { - let (plain, html) = self.parts.iter().fold( - (String::default(), String::default()), - |(mut plain, mut html), part| { - match part { - Part::TextPlain(part) => { - let glue = if plain.is_empty() { "" } else { "\n\n" }; - plain.push_str(glue); - plain.push_str(&part.content); - } - Part::TextHtml(part) => { - let glue = if html.is_empty() { "" } else { "\n\n" }; - html.push_str(glue); - html.push_str(&part.content); - } - _ => (), - }; - (plain, html) - }, - ); - if plain.is_empty() { - // Remove HTML markup - let sanitized_html = ammonia::Builder::new() - .tags(HashSet::default()) - .clean(&html) - .to_string(); - // Merge new line chars - let sanitized_html = Regex::new(r"(\r?\n\s*){2,}") - .unwrap() - .replace_all(&sanitized_html, "\n\n") - .to_string(); - // Replace tabulations and &npsp; by spaces - let sanitized_html = Regex::new(r"(\t| )") - .unwrap() - .replace_all(&sanitized_html, " ") - .to_string(); - // Merge spaces - let sanitized_html = Regex::new(r" {2,}") - .unwrap() - .replace_all(&sanitized_html, " ") - .to_string(); - // Decode HTML entities - let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string(); - - sanitized_html - } else { - // Merge new line chars - let sanitized_plain = Regex::new(r"(\r?\n\s*){2,}") - .unwrap() - .replace_all(&plain, "\n\n") - .to_string(); - // Replace tabulations by spaces - let sanitized_plain = Regex::new(r"\t") - .unwrap() - .replace_all(&sanitized_plain, " ") - .to_string(); - // Merge spaces - let sanitized_plain = Regex::new(r" {2,}") - .unwrap() - .replace_all(&sanitized_plain, " ") - .to_string(); - - sanitized_plain - } - } - - /// Fold string body from all HTML parts into a single string - /// body. - fn fold_text_html_parts(&self) -> String { - let text_parts = self - .parts - .iter() - .filter_map(|part| match part { - Part::TextHtml(part) => Some(part.content.to_owned()), - _ => None, - }) - .collect::>() - .join("\n\n"); - let text_parts = Regex::new(r"(\r?\n){2,}") - .unwrap() - .replace_all(&text_parts, "\n\n") - .to_string(); - text_parts - } - - /// Fold string body from all text parts into a single string - /// body. The mime allows users to choose between plain text parts - /// and html text parts. - pub fn fold_text_parts(&self, text_mime: &str) -> String { - if text_mime == "html" { - self.fold_text_html_parts() - } else { - self.fold_text_plain_parts() - } - } - - pub fn into_reply(mut self, all: bool, account: &Account) -> Result { - let account_addr = account.address()?; - - // In-Reply-To - self.in_reply_to = self.message_id.to_owned(); - - // Message-Id - self.message_id = None; - - // To - let addrs = self - .reply_to - .as_deref() - .or_else(|| self.from.as_deref()) - .map(|addrs| { - addrs.iter().cloned().filter(|addr| match addr { - Addr::Group(_) => false, - Addr::Single(a) => match &account_addr { - Addr::Group(_) => false, - Addr::Single(b) => a.addr != b.addr, - }, - }) - }); - if all { - self.to = addrs.map(|addrs| addrs.collect::>().into()); - } else { - self.to = addrs - .and_then(|mut addrs| addrs.next()) - .map(|addr| vec![addr].into()); - } - - // Cc - self.cc = if all { - self.cc.as_deref().map(|addrs| { - addrs - .iter() - .cloned() - .filter(|addr| match addr { - Addr::Group(_) => false, - Addr::Single(a) => match &account_addr { - Addr::Group(_) => false, - Addr::Single(b) => a.addr != b.addr, - }, - }) - .collect::>() - .into() - }) - } else { - None - }; - - // Bcc - self.bcc = None; - - // Body - let plain_content = { - let date = self - .date - .as_ref() - .map(|date| date.format("%d %b %Y, at %H:%M (%z)").to_string()) - .unwrap_or_else(|| "unknown date".into()); - let sender = self - .reply_to - .as_ref() - .or_else(|| self.from.as_ref()) - .and_then(|addrs| addrs.clone().extract_single_info()) - .map(|addr| addr.display_name.clone().unwrap_or_else(|| addr.addr)) - .unwrap_or_else(|| "unknown sender".into()); - let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender); - - let mut glue = ""; - for line in self.fold_text_parts("plain").trim().lines() { - if line == DEFAULT_SIG_DELIM { - break; - } - content.push_str(glue); - content.push('>'); - content.push_str(if line.starts_with('>') { "" } else { " " }); - content.push_str(line); - glue = "\n"; - } - - content - }; - - self.parts = Parts(vec![Part::new_text_plain(plain_content)]); - - // Subject - if !self.subject.starts_with("Re:") { - self.subject = format!("Re: {}", self.subject); - } - - // From - self.from = Some(vec![account_addr.clone()].into()); - - Ok(self) - } - - pub fn into_forward(mut self, account: &Account) -> Result { - let account_addr = account.address()?; - - let prev_subject = self.subject.to_owned(); - let prev_date = self.date.to_owned(); - let prev_from = self.reply_to.to_owned().or_else(|| self.from.to_owned()); - let prev_to = self.to.to_owned(); - - // Message-Id - self.message_id = None; - - // In-Reply-To - self.in_reply_to = None; - - // From - self.from = Some(vec![account_addr].into()); - - // To - self.to = Some(vec![].into()); - - // Cc - self.cc = None; - - // Bcc - self.bcc = None; - - // Subject - if !self.subject.starts_with("Fwd:") { - self.subject = format!("Fwd: {}", self.subject); - } - - // Body - let mut content = String::default(); - content.push_str("\n\n-------- Forwarded Message --------\n"); - content.push_str(&format!("Subject: {}\n", prev_subject)); - if let Some(date) = prev_date { - content.push_str(&format!("Date: {}\n", date.to_rfc2822())); - } - if let Some(addrs) = prev_from.as_ref() { - content.push_str("From: "); - content.push_str(&addrs.to_string()); - content.push('\n'); - } - if let Some(addrs) = prev_to.as_ref() { - content.push_str("To: "); - content.push_str(&addrs.to_string()); - content.push('\n'); - } - content.push('\n'); - content.push_str(&self.fold_text_parts("plain")); - self.parts - .replace_text_plain_parts_with(TextPlainPart { content }); - - Ok(self) - } - - pub fn encrypt(mut self, encrypt: bool) -> Self { - self.encrypt = encrypt; - self - } - - pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result { - for path in attachments_paths { - let path = shellexpand::full(path) - .map_err(|err| Error::ExpandAttachmentPathError(err, path.to_owned()))?; - let path = PathBuf::from(path.to_string()); - let filename: String = path - .file_name() - .ok_or_else(|| Error::GetAttachmentFilenameError(path.to_owned()))? - .to_string_lossy() - .into(); - let content = - fs::read(&path).map_err(|err| Error::ReadAttachmentError(err, path.to_owned()))?; - let mime = tree_magic::from_u8(&content); - - self.parts.push(Part::Binary(BinaryPart { - filename, - mime, - content, - })) - } - - Ok(self) - } - - pub fn merge_with(&mut self, msg: Msg) { - self.from = msg.from; - self.reply_to = msg.reply_to; - self.to = msg.to; - self.cc = msg.cc; - self.bcc = msg.bcc; - self.subject = msg.subject; - - if msg.message_id.is_some() { - self.message_id = msg.message_id; - } - - if msg.in_reply_to.is_some() { - self.in_reply_to = msg.in_reply_to; - } - - for part in msg.parts.0.into_iter() { - match part { - Part::Binary(_) => self.parts.push(part), - Part::TextPlain(_) => { - self.parts.retain(|p| !matches!(p, Part::TextPlain(_))); - self.parts.push(part); - } - Part::TextHtml(_) => { - self.parts.retain(|p| !matches!(p, Part::TextHtml(_))); - self.parts.push(part); - } - } - } - } - - pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result { - let account_addr: Addrs = vec![account.address()?].into(); - let mut tpl = String::default(); - - tpl.push_str("Content-Type: text/plain; charset=utf-8\n"); - - if let Some(in_reply_to) = self.in_reply_to.as_ref() { - tpl.push_str(&format!("In-Reply-To: {}\n", in_reply_to)) - } - - // From - tpl.push_str(&format!( - "From: {}\n", - opts.from - .map(|addrs| addrs.join(", ")) - .unwrap_or_else(|| account_addr.to_string()) - )); - - // To - tpl.push_str(&format!( - "To: {}\n", - opts.to - .map(|addrs| addrs.join(", ")) - .or_else(|| self.to.clone().map(|addrs| addrs.to_string())) - .unwrap_or_default() - )); - - // Cc - if let Some(addrs) = opts - .cc - .map(|addrs| addrs.join(", ")) - .or_else(|| self.cc.clone().map(|addrs| addrs.to_string())) - { - tpl.push_str(&format!("Cc: {}\n", addrs)); - } - - // Bcc - if let Some(addrs) = opts - .bcc - .map(|addrs| addrs.join(", ")) - .or_else(|| self.bcc.clone().map(|addrs| addrs.to_string())) - { - tpl.push_str(&format!("Bcc: {}\n", addrs)); - } - - // Subject - tpl.push_str(&format!( - "Subject: {}\n", - opts.subject.unwrap_or(&self.subject) - )); - - // Headers <=> body separator - tpl.push('\n'); - - // Body - if let Some(body) = opts.body { - tpl.push_str(body); - } else { - tpl.push_str(&self.fold_text_plain_parts()) - } - - // Signature - if let Some(sig) = opts.sig { - tpl.push_str("\n\n"); - tpl.push_str(sig); - } else if let Some(ref sig) = account.sig { - tpl.push_str("\n\n"); - tpl.push_str(sig); - } - - tpl.push('\n'); - - trace!("template: {:?}", tpl); - Ok(tpl) - } - - pub fn from_tpl(tpl: &str) -> Result { - info!("begin: building message from template"); - trace!("template: {:?}", tpl); - - let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?; - - info!("end: building message from template"); - Self::from_parsed_mail(parsed_mail, &Account::default()) - } - - pub fn into_sendable_msg(&self, account: &Account) -> Result { - let mut msg_builder = lettre::Message::builder() - .message_id(self.message_id.to_owned()) - .subject(self.subject.to_owned()); - - if let Some(id) = self.in_reply_to.as_ref() { - msg_builder = msg_builder.in_reply_to(id.to_owned()); - }; - - if let Some(addrs) = self.from.as_ref() { - for addr in from_addrs_to_sendable_mbox(addrs)? { - msg_builder = msg_builder.from(addr) - } - }; - - if let Some(addrs) = self.to.as_ref() { - for addr in from_addrs_to_sendable_mbox(addrs)? { - msg_builder = msg_builder.to(addr) - } - }; - - if let Some(addrs) = self.reply_to.as_ref() { - for addr in from_addrs_to_sendable_mbox(addrs)? { - msg_builder = msg_builder.reply_to(addr) - } - }; - - if let Some(addrs) = self.cc.as_ref() { - for addr in from_addrs_to_sendable_mbox(addrs)? { - msg_builder = msg_builder.cc(addr) - } - }; - - if let Some(addrs) = self.bcc.as_ref() { - for addr in from_addrs_to_sendable_mbox(addrs)? { - msg_builder = msg_builder.bcc(addr) - } - }; - - let mut multipart = { - let mut multipart = - MultiPart::mixed().singlepart(SinglePart::plain(self.fold_text_plain_parts())); - for part in self.attachments() { - multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body( - part.content, - part.mime.parse().map_err(|err| { - Error::ParseAttachmentContentTypeError(err, part.filename) - })?, - )) - } - multipart - }; - - if self.encrypt { - let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string()); - fs::write(multipart_buffer.clone(), multipart.formatted()) - .map_err(Error::WriteTmpMultipartError)?; - let addr = self - .to - .as_ref() - .and_then(|addrs| addrs.clone().extract_single_info()) - .map(|addr| addr.addr) - .ok_or_else(|| Error::ParseRecipientError)?; - let encrypted_multipart = account.pgp_encrypt_file(&addr, multipart_buffer.clone())?; - trace!("encrypted multipart: {:#?}", encrypted_multipart); - multipart = MultiPart::encrypted(String::from("application/pgp-encrypted")) - .singlepart( - SinglePart::builder() - .header(ContentType::parse("application/pgp-encrypted").unwrap()) - .body(String::from("Version: 1")), - ) - .singlepart( - SinglePart::builder() - .header(ContentType::parse("application/octet-stream").unwrap()) - .body(encrypted_multipart), - ) - } - - msg_builder - .multipart(multipart) - .map_err(Error::BuildSendableMsgError) - } - - pub fn from_parsed_mail( - parsed_mail: mailparse::ParsedMail<'_>, - config: &Account, - ) -> Result { - trace!(">> build message from parsed mail"); - trace!("parsed mail: {:?}", parsed_mail); - - let mut msg = Msg::default(); - for header in parsed_mail.get_headers() { - trace!(">> parse header {:?}", header); - - let key = header.get_key(); - trace!("header key: {:?}", key); - - let val = header.get_value(); - trace!("header value: {:?}", val); - - match key.to_lowercase().as_str() { - "message-id" => msg.message_id = Some(val), - "in-reply-to" => msg.in_reply_to = Some(val), - "subject" => { - msg.subject = val; - } - "date" => match mailparse::dateparse(&val) { - Ok(timestamp) => { - msg.date = Some(Utc.timestamp(timestamp, 0).with_timezone(&Local)) - } - Err(err) => { - warn!("cannot parse message date {:?}, skipping it", val); - warn!("{}", err); - } - }, - "from" => { - msg.from = from_slice_to_addrs(&val) - .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? - } - "to" => { - msg.to = from_slice_to_addrs(&val) - .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? - } - "reply-to" => { - msg.reply_to = from_slice_to_addrs(&val) - .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? - } - "cc" => { - msg.cc = from_slice_to_addrs(&val) - .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? - } - "bcc" => { - msg.bcc = from_slice_to_addrs(&val) - .map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))? - } - key => { - msg.headers.insert(key.to_lowercase(), val); - } - } - trace!("<< parse header"); - } - - msg.parts = Parts::from_parsed_mail(config, &parsed_mail)?; - trace!("message: {:?}", msg); - - info!("<< build message from parsed mail"); - Ok(msg) - } - - /// Transforms a message into a readable string. A readable - /// message is like a template, except that: - /// - headers part is customizable (can be omitted if empty filter given in argument) - /// - body type is customizable (plain or html) - pub fn to_readable_string( - &self, - text_mime: &str, - headers: Vec<&str>, - config: &Account, - ) -> Result { - let mut all_headers = vec![]; - for h in config.read_headers.iter() { - let h = h.to_lowercase(); - if !all_headers.contains(&h) { - all_headers.push(h) - } - } - for h in headers.iter() { - let h = h.to_lowercase(); - if !all_headers.contains(&h) { - all_headers.push(h) - } - } - - let mut readable_msg = String::new(); - for h in all_headers { - match h.as_str() { - "message-id" => match self.message_id { - Some(ref message_id) if !message_id.is_empty() => { - readable_msg.push_str(&format!("Message-Id: {}\n", message_id)); - } - _ => (), - }, - "in-reply-to" => match self.in_reply_to { - Some(ref in_reply_to) if !in_reply_to.is_empty() => { - readable_msg.push_str(&format!("In-Reply-To: {}\n", in_reply_to)); - } - _ => (), - }, - "subject" => { - readable_msg.push_str(&format!("Subject: {}\n", self.subject)); - } - "date" => { - if let Some(ref date) = self.date { - readable_msg.push_str(&format!("Date: {}\n", date.to_rfc2822())); - } - } - "from" => match self.from { - Some(ref addrs) if !addrs.is_empty() => { - readable_msg.push_str(&format!("From: {}\n", addrs)); - } - _ => (), - }, - "to" => match self.to { - Some(ref addrs) if !addrs.is_empty() => { - readable_msg.push_str(&format!("To: {}\n", addrs)); - } - _ => (), - }, - "reply-to" => match self.reply_to { - Some(ref addrs) if !addrs.is_empty() => { - readable_msg.push_str(&format!("Reply-To: {}\n", addrs)); - } - _ => (), - }, - "cc" => match self.cc { - Some(ref addrs) if !addrs.is_empty() => { - readable_msg.push_str(&format!("Cc: {}\n", addrs)); - } - _ => (), - }, - "bcc" => match self.bcc { - Some(ref addrs) if !addrs.is_empty() => { - readable_msg.push_str(&format!("Bcc: {}\n", addrs)); - } - _ => (), - }, - key => match self.headers.get(key) { - Some(ref val) if !val.is_empty() => { - readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Train), val)); - } - _ => (), - }, - }; - } - - if !readable_msg.is_empty() { - readable_msg.push_str("\n"); - } - - readable_msg.push_str(&self.fold_text_parts(text_mime)); - Ok(readable_msg) - } -} - -impl TryInto for Msg { - type Error = Error; - - fn try_into(self) -> Result { - (&self).try_into() - } -} - -impl TryInto for &Msg { - type Error = Error; - - fn try_into(self) -> Result { - let from = match self - .from - .as_ref() - .and_then(|addrs| addrs.clone().extract_single_info()) - { - Some(addr) => addr.addr.parse().map(Some), - None => Ok(None), - }?; - let to = self - .to - .as_ref() - .map(from_addrs_to_sendable_addrs) - .unwrap_or(Ok(vec![]))?; - Ok(lettre::address::Envelope::new(from, to).map_err(Error::BuildEnvelopeError)?) - } -} - -#[cfg(test)] -mod tests { - use mailparse::SingleInfo; - use std::iter::FromIterator; - - use crate::msg::Addr; - - use super::*; - - #[test] - fn test_into_reply() { - let config = Account { - display_name: "Test".into(), - email: "test-account@local".into(), - ..Account::default() - }; - - // Checks that: - // - "message_id" moves to "in_reply_to" - // - "subject" starts by "Re: " - // - "to" is replaced by "from" - // - "from" is replaced by the address from the account config - - let msg = Msg { - message_id: Some("msg-id".into()), - subject: "subject".into(), - from: Some( - vec![Addr::Single(SingleInfo { - addr: "test-sender@local".into(), - display_name: None, - })] - .into(), - ), - ..Msg::default() - } - .into_reply(false, &config) - .unwrap(); - - assert_eq!(msg.message_id, None); - assert_eq!(msg.in_reply_to.unwrap(), "msg-id"); - assert_eq!(msg.subject, "Re: subject"); - assert_eq!( - msg.from.unwrap().to_string(), - "\"Test\" " - ); - assert_eq!(msg.to.unwrap().to_string(), "test-sender@local"); - - // Checks that: - // - "subject" does not contains additional "Re: " - // - "to" is replaced by reply_to - // - "to" contains one address when "all" is false - // - "cc" are empty when "all" is false - - let msg = Msg { - subject: "Re: subject".into(), - from: Some( - vec![Addr::Single(SingleInfo { - addr: "test-sender@local".into(), - display_name: None, - })] - .into(), - ), - reply_to: Some( - vec![ - Addr::Single(SingleInfo { - addr: "test-sender-to-reply@local".into(), - display_name: Some("Sender".into()), - }), - Addr::Single(SingleInfo { - addr: "test-sender-to-reply-2@local".into(), - display_name: Some("Sender 2".into()), - }), - ] - .into(), - ), - cc: Some( - vec![Addr::Single(SingleInfo { - addr: "test-cc@local".into(), - display_name: None, - })] - .into(), - ), - ..Msg::default() - } - .into_reply(false, &config) - .unwrap(); - - assert_eq!(msg.subject, "Re: subject"); - assert_eq!( - msg.to.unwrap().to_string(), - "\"Sender\" " - ); - assert_eq!(msg.cc, None); - - // Checks that: - // - "to" contains all addresses except for the sender when "all" is true - // - "cc" contains all addresses except for the sender when "all" is true - - let msg = Msg { - from: Some( - vec![ - Addr::Single(SingleInfo { - addr: "test-sender-1@local".into(), - display_name: Some("Sender 1".into()), - }), - Addr::Single(SingleInfo { - addr: "test-sender-2@local".into(), - display_name: Some("Sender 2".into()), - }), - Addr::Single(SingleInfo { - addr: "test-account@local".into(), - display_name: Some("Test".into()), - }), - ] - .into(), - ), - cc: Some( - vec![ - Addr::Single(SingleInfo { - addr: "test-sender-1@local".into(), - display_name: Some("Sender 1".into()), - }), - Addr::Single(SingleInfo { - addr: "test-sender-2@local".into(), - display_name: Some("Sender 2".into()), - }), - Addr::Single(SingleInfo { - addr: "test-account@local".into(), - display_name: None, - }), - ] - .into(), - ), - ..Msg::default() - } - .into_reply(true, &config) - .unwrap(); - - assert_eq!( - msg.to.unwrap().to_string(), - "\"Sender 1\" , \"Sender 2\" " - ); - assert_eq!( - msg.cc.unwrap().to_string(), - "\"Sender 1\" , \"Sender 2\" " - ); - } - - #[test] - fn test_to_readable() { - let config = Account::default(); - let msg = Msg { - parts: Parts(vec![Part::TextPlain(TextPlainPart { - content: String::from("hello, world!"), - })]), - ..Msg::default() - }; - - // empty msg headers, empty headers, empty config - assert_eq!( - "hello, world!", - msg.to_readable_string("plain", vec![], &config).unwrap() - ); - // empty msg headers, basic headers - assert_eq!( - "hello, world!", - msg.to_readable_string("plain", vec!["From", "DATE", "custom-hEader"], &config) - .unwrap() - ); - // empty msg headers, multiple subject headers - assert_eq!( - "Subject: \n\nhello, world!", - msg.to_readable_string("plain", vec!["subject", "Subject", "SUBJECT"], &config) - .unwrap() - ); - - let msg = Msg { - headers: HashMap::from_iter([("custom-header".into(), "custom value".into())]), - message_id: Some("".into()), - from: Some( - vec![Addr::Single(SingleInfo { - addr: "test@local".into(), - display_name: Some("Test".into()), - })] - .into(), - ), - cc: Some(vec![].into()), - parts: Parts(vec![Part::TextPlain(TextPlainPart { - content: String::from("hello, world!"), - })]), - ..Msg::default() - }; - - // header present in msg headers, empty config - assert_eq!( - "From: \"Test\" \n\nhello, world!", - msg.to_readable_string("plain", vec!["from"], &config) - .unwrap() - ); - // header present but empty in msg headers, empty config - assert_eq!( - "hello, world!", - msg.to_readable_string("plain", vec!["cc"], &config) - .unwrap() - ); - // multiple same custom headers present in msg headers, empty - // config - assert_eq!( - "Custom-Header: custom value\n\nhello, world!", - msg.to_readable_string("plain", vec!["custom-header", "cuSTom-HeaDer"], &config) - .unwrap() - ); - - let config = Account { - read_headers: vec![ - "CusTOM-heaDER".into(), - "Subject".into(), - "from".into(), - "cc".into(), - ], - ..Account::default() - }; - // header present but empty in msg headers, empty config - assert_eq!( - "Custom-Header: custom value\nSubject: \nFrom: \"Test\" \nMessage-Id: \n\nhello, world!", - msg.to_readable_string("plain", vec!["cc", "message-ID"], &config) - .unwrap() - ); - } -} diff --git a/lib/src/msg/msg_utils.rs b/lib/src/msg/msg_utils.rs deleted file mode 100644 index 3c61d7d..0000000 --- a/lib/src/msg/msg_utils.rs +++ /dev/null @@ -1,24 +0,0 @@ -use log::{debug, trace}; -use std::{env, fs, path}; - -use crate::msg::{Error, Result}; - -pub fn local_draft_path() -> path::PathBuf { - trace!(">> get local draft path"); - - let path = env::temp_dir().join("himalaya-draft.eml"); - debug!("local draft path: {:?}", path); - - trace!("<< get local draft path"); - path -} - -pub fn remove_local_draft() -> Result<()> { - trace!(">> remove local draft"); - - let path = local_draft_path(); - fs::remove_file(&path).map_err(|err| Error::DeleteLocalDraftError(err, path))?; - - trace!("<< remove local draft"); - Ok(()) -} diff --git a/lib/src/msg/parts.rs b/lib/src/msg/parts.rs deleted file mode 100644 index b64c01a..0000000 --- a/lib/src/msg/parts.rs +++ /dev/null @@ -1,150 +0,0 @@ -use mailparse::MailHeaderMap; -use serde::Serialize; -use std::{ - env, fs, - ops::{Deref, DerefMut}, -}; -use uuid::Uuid; - -use crate::{account::Account, msg}; - -#[derive(Debug, Clone, Default, Serialize)] -pub struct TextPlainPart { - pub content: String, -} - -#[derive(Debug, Clone, Default, Serialize)] -pub struct TextHtmlPart { - pub content: String, -} - -#[derive(Debug, Clone, Default, Serialize)] -pub struct BinaryPart { - pub filename: String, - pub mime: String, - pub content: Vec, -} - -#[derive(Debug, Clone, Serialize)] -#[serde(rename_all = "camelCase")] -pub enum Part { - TextPlain(TextPlainPart), - TextHtml(TextHtmlPart), - Binary(BinaryPart), -} - -impl Part { - pub fn new_text_plain(content: String) -> Self { - Self::TextPlain(TextPlainPart { content }) - } -} - -#[derive(Debug, Clone, Default, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Parts(pub Vec); - -impl Parts { - pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) { - self.retain(|part| !matches!(part, Part::TextPlain(_))); - self.push(Part::TextPlain(part)); - } - - pub fn from_parsed_mail<'a>( - account: &'a Account, - part: &'a mailparse::ParsedMail<'a>, - ) -> msg::Result { - let mut parts = vec![]; - if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none() - { - let content = part.get_body().unwrap_or_default(); - parts.push(Part::TextPlain(TextPlainPart { content })) - } else { - build_parts_map_rec(account, part, &mut parts)?; - } - Ok(Self(parts)) - } -} - -impl Deref for Parts { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Parts { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -fn build_parts_map_rec( - account: &Account, - parsed_mail: &mailparse::ParsedMail, - parts: &mut Vec, -) -> msg::Result<()> { - if parsed_mail.subparts.is_empty() { - let cdisp = parsed_mail.get_content_disposition(); - match cdisp.disposition { - mailparse::DispositionType::Attachment => { - let filename = cdisp - .params - .get("filename") - .map(String::from) - .unwrap_or_else(|| String::from("noname")); - let content = parsed_mail.get_body_raw().unwrap_or_default(); - let mime = tree_magic::from_u8(&content); - parts.push(Part::Binary(BinaryPart { - filename, - mime, - content, - })); - } - // TODO: manage other use cases - _ => { - if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") { - let content = parsed_mail.get_body().unwrap_or_default(); - if ctype.starts_with("text/plain") { - parts.push(Part::TextPlain(TextPlainPart { content })) - } else if ctype.starts_with("text/html") { - parts.push(Part::TextHtml(TextHtmlPart { content })) - } - } - } - }; - } else { - let ctype = parsed_mail - .get_headers() - .get_first_value("content-type") - .ok_or_else(|| msg::Error::GetMultipartContentTypeError)?; - if ctype.starts_with("multipart/encrypted") { - let decrypted_part = parsed_mail - .subparts - .get(1) - .ok_or_else(|| msg::Error::GetEncryptedPartMultipartError) - .and_then(|part| decrypt_part(account, part))?; - let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes()) - .map_err(msg::Error::ParseEncryptedPartError)?; - build_parts_map_rec(account, &parsed_mail, parts)?; - } else { - for part in parsed_mail.subparts.iter() { - build_parts_map_rec(account, part, parts)?; - } - } - } - - Ok(()) -} - -fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> msg::Result { - let msg_path = env::temp_dir().join(Uuid::new_v4().to_string()); - let msg_body = msg - .get_body() - .map_err(msg::Error::GetEncryptedPartBodyError)?; - fs::write(msg_path.clone(), &msg_body).map_err(msg::Error::WriteEncryptedPartBodyError)?; - let content = account - .pgp_decrypt_file(msg_path.clone()) - .map_err(msg::Error::DecryptPartError)?; - Ok(content) -} diff --git a/lib/src/msg/tpl.rs b/lib/src/msg/tpl.rs deleted file mode 100644 index b7ba08a..0000000 --- a/lib/src/msg/tpl.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! Module related to message template CLI. -//! -//! This module provides subcommands, arguments and a command matcher related to message template. - -#[derive(Debug, Default, PartialEq, Eq, Clone)] -pub struct TplOverride<'a> { - pub subject: Option<&'a str>, - pub from: Option>, - pub to: Option>, - pub cc: Option>, - pub bcc: Option>, - pub headers: Option>, - pub body: Option<&'a str>, - pub sig: Option<&'a str>, -} diff --git a/lib/src/process.rs b/lib/src/process.rs deleted file mode 100644 index c9e282c..0000000 --- a/lib/src/process.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Process module. -//! -//! This module contains cross platform helpers around the -//! `std::process` crate. - -use log::{debug, trace}; -use std::{io, process::Command, string}; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum ProcessError { - #[error("cannot run command {1:?}")] - RunCmdError(#[source] io::Error, String), - - #[error("cannot parse command output")] - ParseCmdOutputError(#[source] string::FromUtf8Error), -} - -pub fn run(cmd: &str) -> Result { - debug!(">> run command"); - debug!("command: {}", cmd); - - let output = if cfg!(target_os = "windows") { - Command::new("cmd").args(&["/C", cmd]).output() - } else { - Command::new("sh").arg("-c").arg(cmd).output() - }; - let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?; - let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?; - - trace!("command output: {}", output); - debug!("<< run command"); - Ok(output) -} diff --git a/lib/tests/emails/alice-to-patrick-encrypted.eml b/lib/tests/emails/alice-to-patrick-encrypted.eml deleted file mode 100644 index 8d2c631..0000000 --- a/lib/tests/emails/alice-to-patrick-encrypted.eml +++ /dev/null @@ -1,19 +0,0 @@ -From: alice@localhost -To: patrick@localhost -Subject: Encrypted message -Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="boundary" - ---boundary -Content-Type: application/pgp-encrypted - -Version: 1 - ---boundary -Content-Type: application/octet-stream - ------BEGIN PGP MESSAGE----- - - ------END PGP MESSAGE----- - ---boundary \ No newline at end of file diff --git a/lib/tests/emails/alice-to-patrick.eml b/lib/tests/emails/alice-to-patrick.eml deleted file mode 100644 index 2cef116..0000000 --- a/lib/tests/emails/alice-to-patrick.eml +++ /dev/null @@ -1,7 +0,0 @@ -From: alice@localhost -To: patrick@localhost -Subject: Plain message -Content-Type: text/plain; charset=utf-8 -Date: Tue, 1 Mar 2022 12:00:00 +0000 - -Ceci est un message. \ No newline at end of file diff --git a/lib/tests/keys/alice.asc b/lib/tests/keys/alice.asc deleted file mode 100644 index e389b19..0000000 --- a/lib/tests/keys/alice.asc +++ /dev/null @@ -1,81 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -lQVYBGH/vyQBDADVehPB0r9rq8zZmntBh1XZPfaKW00R+RGfUCenWFBG0i1nT/LT -9FMKeJiuZF1FdGNEG6Fj/Lv3mGP8dLa83qAL76nkXRXjQ3IfcxY5c87ex6Z5pcPO -Rbi8GPhHK/HkAsE5eqPCOPhIo+Uf6ZAowfgX4b32wvPHcJ7WFVMXlTs7Z053+MWG -AyYMjSwtwzCVlo8vZh3hbudty8SrL6b9j56nElPNnl+kL+FCPq4kSecpLKzRiGDU -DehMhuibWcAuIXHxQHYzBB7asBoEL5cm1aR/D626YmBMn0fjr4HT5iEC67UBEFhJ -pGxTp6IlFerDtGBYdAAksVA7StsWYAMVSI84Zxeq5nCCOBhTqyhp2yA6auvawKRJ -81d/x6FWEaJLsG/HcuEnt0ZAHL7Tos/sPkQY3B3xmfE34SpWJUtCnqQK+F/7awx/ -F4n+KFZX+rUNLj/2uHstuKl9RfW8jVVFnB0WRF2FHIiBuYUXOSj78ggssoJrSnED -WpF5+O+LiCRol4EAEQEAAQAL/2Mk2CorW5WA65mgQmAzn25OdcLaFlgjiciorFHv -FRFfKZESs1822J5DVf2gRSUtobCO+Ix8YzvhfYZRGlFrP39rpkaV6MVsnIL4qzix -jUEwDiPvFZomDV7mZeCAC05u7Rhp2cYpOT5bR91jVv1m4HcO82+4KQnWRx58NuP7 -/c9f8jSLyAiuS6yGoB78yQKgMw27amM5Y6g9e7BZaD/YxMEpJNyZEigpyH9ApxXZ -cM9RnU2O/hFeCCYKfdsweq2x/+TOIJoUiYfgg237kD14swrLNvSa8954866nVH/3 -uBEfb8DDXjuve8QL2otWV+y/vtwpSWvUMUwShCDwqFY1gLTRCE8MhHkBSEojLqJr -FA018asXn6Xw3842ewsUoPWzFqpbqHE1znh/sWAOTEg5f9dTOnT8U4IUhvwq1zgG -3geU7Vf0CJcFr3+XTlNryGsH9UH0FEYNACdZw5o7bkIgddiSS6zAEIsQHG3qZs2X -Y4jc7AFNUcQ08yWMr41cHdGSJQYA4Hvz8fOK7IKBrfrXcCzQ8U+bDG+KcjkmUq70 -e42ryMMtga2myb4OFNasyz7FBTnYv2yFEfMMzczQo9uhaTnjYQjcIW4/AM/seU7A -Ly68lJZLO4guIDBq6s1VEWt4YpBgpX1WzM792LCTVkBNkedm5SaDi3cPhObHXzcM -GefkRx148bRkcO32o7kV2GrIDwuoCjrDEcNwf7B23aFXoDQYKXySIVIbTqBZpqdr -b60NN3cjOVjQTIBFt4wMmppJYPpjBgDzcoRJr0bB9kqXZfm7JJh6+8zfCO001WNZ -yPjf99WMlqc0Zu60ZOey6feaen3fLsKKoxe/uSpWBPLXvjqSQz97aAwD4/Cg5AJ6 -BP7WLMsQkoCrQQR+n0XYXwYRF/HkUFewYprs7xCLkiMqSeebNrnNZk7K1z0wRhEJ -kgtKaChvEw3BAdpeTGALglY3ocqrdCJGJ+1MUVpcmgVgZ/QlR0A8289mwOcuOzq2 -qp0S5lc7GupmjydEHWCsR/QoXhrWOcsF/3a0r9d0qQgBEmxz6CJEt/tz/7oR8oLp -u5dhap+KJpXga8GKmbuzMfNCAoVVTCwn0Vnm9W4b3KTiYubFkqD2wuzkxny9LnQq -EXKyB4FrEeFWDiDy8PquAJu5+19F6m59t6EmxOwClqHtj7C7l99PBg2obFt8qy2S -S0Qpd5WiRkwQDlOPatA8os77jk+cFNe5QZnHk9aMGKPbr4W8jGuJ1Ylu/mGBI70R -3bmUfwsVY74vgHpPwLWIPlz/Bz6YYRnDOdh8tBdBbGljZSA8YWxpY2VAbG9jYWxo -b3N0PokB0gQTAQoAPBYhBF67j7/seymOwYo+hXgI+wInPAqhBQJh/78kAhsDBQkD -wmcABAsJCAcEFQoJCAUWAgMBAAIeAQIXgAAKCRB4CPsCJzwKoREuDACM5YOyPOig -wtXFPEqd2TNqGrQsBqMAoN138MXtddj5wOo64egkyAvq/dLAOxaDh/zdzNyXmjP7 -GWc84QwE+0XwWZxwk7uWEB97U40KMbVsDFUNJ0SekfjJdpc9tHPaFzPRvQYbLCo8 -nh3phmZ5IgYlbyp7q1bZ2CJV7OEDN4vfDRzWHmTK5YNzQ3hRtmTMnCjAaOjmJ7eJ -NwSKNnSJo81HFwR+Nd9Yj39i8sy3DWb8Ax1R9d6tXP9xWQ3PtEEqS1jwkkP9Lsu0 -FqLvuZqdjMs7vfd+m/nrGXQnDHv35LU6Yb2urYSCMY/RJAsolTfI+msgu4juy8Kj -XmPKpru+GllDHdmzkL37vhjwaUzz8LTLAQ5/EZExLWB9/8bi9B+M+Be6ndi9xQnD -fxRBaesItrEFSHNfp4+/mHqeOiOw5Ad40+cI2K3Cw3ynhbTEF61fSDqgKpmS7IJ2 -er/Z2ZjjeZSEBpQu5Xo42XMeN9NLOjjbMUZV8per7MHe61qRBsfpFlCdBVgEYf+/ -JAEMAMFI/2JmSd5LoeSr+hr+RLDXL4qTUXgX1D1/BuddK3VJ6W05HG1Qd2tEXcCW -79l/rCb03WvsSQIeJIufosZ5pNq60c/61JM60u0BIrpEYzwexn5kf/2MTEHE+yi3 -wAJ59L7AOYZ/MLh97K5jtzuyUDiORJo7e9iYp3lnvoVfIKnDXLqtwpeU8dxcsfXd -GonCKuzUNiQlRzn8IWXFVRsmoXdV30I0zUVUlVnrkszeIevyiWWLMkO0bRqZFCzF -jCPUydRYfORxtleqsgACA7qSlCi9H8Jir6grBxLqgOJz1OfRPAzRgQm8oXQf7Kbl -Tqk2FYRQVyoyBEqbfbBeOD+XRM+iAHFC55emQqMGKfVmyoSo+sZUPPz5B9H0cgXS -YAosuoSAQjbTg1XEBrIRfUcmR1qgcrkBfZCOukLbJcLNnDEr7wGEPmjfy45n2uNo -68YJfGH4YmPVU2UDzREFG4rU6Df+BsfF8CtGHZs59rCsIuPPXqyeoh4mBkbSL61L -EzEuuwARAQABAAv8CU+P5diRlGDGUrKqIKTBAFfNVXqVQRi8w52b4odNcZ/226kV -onpu1j772SwsL6kDzPictfcy6SQ0lHlDKRZxB4xaUQ9/L/x0brBQUPK8aQf+fdYv -iDI69iwcATEg0b24OXwfCUiVOz3tqdTp3blQPfk0es2EwMFRx/pkZh5X/3WGwQNf -zVeCcyAP/o0BG0O8N55dYU5eaP+pSDLCT8WDn7EGSTUr8jwJ2cQMVUwaDDipv7d9 -218UpmRbYXC+uHcmkFhApZ4B47NcGQ0tWKtzJCbI++rDipojyFPrnB42ASdeqznG -Zy4hZ9LvYAZrWr9UabaM+ETkVTp8MEVgD8rjUOnalhuh3apWMIrNKpnyxRwLemei -8fAvUl/YL48IgqJ5Hzf/VRCZ6/kOQUk24tdsN33pK9crAfmPD4biF0iZLxwJul+P -LNy0pvzYhxNAEfs8PpDWVHgs/0/kyEjgYcGUDhXc9zuqZ3SMpEO2ADwum4hGOMFl -bb1GLvYuEMNR+iXhBgDRHF8Ig4KDg884TO6329J5c7c8H//UkK1mu1HX6VtVXIwV -M4CkWsU0ofGwQsW4/1iE1L1HIEQVGN3N1bCURtrBEtq93oegDBx+UHu+KP4rw3rS -ObtO5MFfqHrn/9YTO9tnCHHK856zvqjcCsZ8vaeKSSUVYTDk5u9IsaZLspFr5f/w -kX5sW+dPqb1xXCq8QonQDptZS2Rd0x3gUh7clxttpUk3bSu0DfnBXrLzcmRjiTCp -HVcTNOsio+slyIkM0+sGAOygLpL6Uycq4CbiYQEHDPfeMmF3W6A3y5DM07srL0Ov -+nC6qAMO8HFqa+ytc4Rj5GdxVBVbK1GU/4JleOWz5wg4bAIxiKZqPJ1z8MH5+iiA -QJYHvxlubP/yZZvmKDKLCu2yUPGEBQWulQfG9q9MuYazh46tcsVlYKlmwGePxfL9 -Xy4JP5ZaFrUsmTHYRvrAMuPjYT+xTjARdQjUqpENZ54oz/ahdAPVHymzglhBDhK7 -SwqXQOVCXTXULMZSt8HscQX/QFtAI30iGf/BeMun2La4mTSB3WXanb+4m+YtZ6G0 -slmWG9619AEYJ2mfDs0O64BJzLvA+B1hUTNlmspfoCxk/DPYZ/k3z6Bz7yzAGZe+ -XbDMqUzjmbXqIItsocqBFjpbVLmjHiKq4SMCTi/Py/s9K/+lfGib6ApEFksWFMn+ -yTx7qHR9XHxIWXT8sCYmkdPMnBXOsgvoEq6vhtffzCdIpySzQn34Z60XC/5Qi9S7 -z9xzpzizFCTkFavGWHDveBA+3pyJAbwEGAEKACYWIQReu4+/7HspjsGKPoV4CPsC -JzwKoQUCYf+/JAIbDAUJA8JnAAAKCRB4CPsCJzwKoV10DADCJDUgCEffjNQwV0JX -30iJ41vCaKPRKDuBVtfvrXC6CPeOXO3zJpGd0JzuDBMvvj2/XNcghgUEUbOdEfsF -Gq5ezae7PjiYZaZ2E12m0OkGQ5KHLKH2Rp+Z7ZokDvGZlLY6IwKfQCUJGBBhwRZr -tnr+sKY8jtPWpSaERFS6Dl/SFZUmwFdJcnIBageVCMWLTrHALES+G34Z+05lD4Wp -Rb+Q2V9Tm+E67FKMjqDBZLY4g8F/JeqCkk1YcLBwnUuebd7GHIIC4vu4AlOBlnrM -6OnPwevX7V9HkmFrI8bUvuNhX80MttoB7gnt7rkrpko26jOyaIVdaAkfonjXKEKC -x5HI+X71jGhmUFbrCwUPRxMPbHuTbl6ONy6QlwZf7anwuIKoHe2Qb8RoqySzw7r7 -Htzhvw+e/QyzDEyey0acLgjIlRLr/fhuBjfaH9XaHbK7oqW5u4XT1erDnkXLFoMN -hWMFomzjnkxtnMHwDhBb/VJF5wMEharbkhyakTNNZ7l33Es= -=XrAt ------END PGP PRIVATE KEY BLOCK----- diff --git a/lib/tests/keys/alice.pub.asc b/lib/tests/keys/alice.pub.asc deleted file mode 100644 index 728c8ff..0000000 --- a/lib/tests/keys/alice.pub.asc +++ /dev/null @@ -1,41 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQGNBGH/vyQBDADVehPB0r9rq8zZmntBh1XZPfaKW00R+RGfUCenWFBG0i1nT/LT -9FMKeJiuZF1FdGNEG6Fj/Lv3mGP8dLa83qAL76nkXRXjQ3IfcxY5c87ex6Z5pcPO -Rbi8GPhHK/HkAsE5eqPCOPhIo+Uf6ZAowfgX4b32wvPHcJ7WFVMXlTs7Z053+MWG -AyYMjSwtwzCVlo8vZh3hbudty8SrL6b9j56nElPNnl+kL+FCPq4kSecpLKzRiGDU -DehMhuibWcAuIXHxQHYzBB7asBoEL5cm1aR/D626YmBMn0fjr4HT5iEC67UBEFhJ -pGxTp6IlFerDtGBYdAAksVA7StsWYAMVSI84Zxeq5nCCOBhTqyhp2yA6auvawKRJ -81d/x6FWEaJLsG/HcuEnt0ZAHL7Tos/sPkQY3B3xmfE34SpWJUtCnqQK+F/7awx/ -F4n+KFZX+rUNLj/2uHstuKl9RfW8jVVFnB0WRF2FHIiBuYUXOSj78ggssoJrSnED -WpF5+O+LiCRol4EAEQEAAbQXQWxpY2UgPGFsaWNlQGxvY2FsaG9zdD6JAdIEEwEK -ADwWIQReu4+/7HspjsGKPoV4CPsCJzwKoQUCYf+/JAIbAwUJA8JnAAQLCQgHBBUK -CQgFFgIDAQACHgECF4AACgkQeAj7Aic8CqERLgwAjOWDsjzooMLVxTxKndkzahq0 -LAajAKDdd/DF7XXY+cDqOuHoJMgL6v3SwDsWg4f83czcl5oz+xlnPOEMBPtF8Fmc -cJO7lhAfe1ONCjG1bAxVDSdEnpH4yXaXPbRz2hcz0b0GGywqPJ4d6YZmeSIGJW8q -e6tW2dgiVezhAzeL3w0c1h5kyuWDc0N4UbZkzJwowGjo5ie3iTcEijZ0iaPNRxcE -fjXfWI9/YvLMtw1m/AMdUfXerVz/cVkNz7RBKktY8JJD/S7LtBai77manYzLO733 -fpv56xl0Jwx79+S1OmG9rq2EgjGP0SQLKJU3yPprILuI7svCo15jyqa7vhpZQx3Z -s5C9+74Y8GlM8/C0ywEOfxGRMS1gff/G4vQfjPgXup3YvcUJw38UQWnrCLaxBUhz -X6ePv5h6njojsOQHeNPnCNitwsN8p4W0xBetX0g6oCqZkuyCdnq/2dmY43mUhAaU -LuV6ONlzHjfTSzo42zFGVfKXq+zB3utakQbH6RZQuQGNBGH/vyQBDADBSP9iZkne -S6Hkq/oa/kSw1y+Kk1F4F9Q9fwbnXSt1SeltORxtUHdrRF3Alu/Zf6wm9N1r7EkC -HiSLn6LGeaTautHP+tSTOtLtASK6RGM8HsZ+ZH/9jExBxPsot8ACefS+wDmGfzC4 -feyuY7c7slA4jkSaO3vYmKd5Z76FXyCpw1y6rcKXlPHcXLH13RqJwirs1DYkJUc5 -/CFlxVUbJqF3Vd9CNM1FVJVZ65LM3iHr8ollizJDtG0amRQsxYwj1MnUWHzkcbZX -qrIAAgO6kpQovR/CYq+oKwcS6oDic9Tn0TwM0YEJvKF0H+ym5U6pNhWEUFcqMgRK -m32wXjg/l0TPogBxQueXpkKjBin1ZsqEqPrGVDz8+QfR9HIF0mAKLLqEgEI204NV -xAayEX1HJkdaoHK5AX2QjrpC2yXCzZwxK+8BhD5o38uOZ9rjaOvGCXxh+GJj1VNl -A80RBRuK1Og3/gbHxfArRh2bOfawrCLjz16snqIeJgZG0i+tSxMxLrsAEQEAAYkB -vAQYAQoAJhYhBF67j7/seymOwYo+hXgI+wInPAqhBQJh/78kAhsMBQkDwmcAAAoJ -EHgI+wInPAqhXXQMAMIkNSAIR9+M1DBXQlffSInjW8Joo9EoO4FW1++tcLoI945c -7fMmkZ3QnO4MEy++Pb9c1yCGBQRRs50R+wUarl7Np7s+OJhlpnYTXabQ6QZDkocs -ofZGn5ntmiQO8ZmUtjojAp9AJQkYEGHBFmu2ev6wpjyO09alJoREVLoOX9IVlSbA -V0lycgFqB5UIxYtOscAsRL4bfhn7TmUPhalFv5DZX1Ob4TrsUoyOoMFktjiDwX8l -6oKSTVhwsHCdS55t3sYcggLi+7gCU4GWeszo6c/B69ftX0eSYWsjxtS+42FfzQy2 -2gHuCe3uuSumSjbqM7JohV1oCR+ieNcoQoLHkcj5fvWMaGZQVusLBQ9HEw9se5Nu -Xo43LpCXBl/tqfC4gqgd7ZBvxGirJLPDuvse3OG/D579DLMMTJ7LRpwuCMiVEuv9 -+G4GN9of1dodsruipbm7hdPV6sOeRcsWgw2FYwWibOOeTG2cwfAOEFv9UkXnAwSF -qtuSHJqRM01nuXfcSw== -=JGp0 ------END PGP PUBLIC KEY BLOCK----- diff --git a/lib/tests/keys/patrick.asc b/lib/tests/keys/patrick.asc deleted file mode 100644 index 3461a66..0000000 --- a/lib/tests/keys/patrick.asc +++ /dev/null @@ -1,81 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -lQVYBGH/wDYBDADlRkqjj5jOTBc0p+9Fk8sIstXjLbxUl4lMsw9Mh6rnuoCVc49D -nlG8ZbqS/j2jpNE8e4F3rFCkLnirGLT9tYIDE0xC6/B8AtDJNSaxb0AJKqIR4v6O -qunndGrg616H7U55NcLCT9zEJ8+lo/i7b0KcKt7RVdw064Vj1KwhEeEgdQ8WCrsq -TA18f3HBRS5ChqEDxYwYfet5rn5BF0ok5/aWHJkxOh+VnZwszjahxkzJ6BtDOJq+ -HGrhCFT+YCxLmFJIGZF95RPOH2TBCqJweh83opY/cnbu8zV4Zh2tGQu/ohZC2uPM -G/n6QoXv/7n/7/8dTtHH01enoCJxxSONfPg/F4PlUyZJcQOI+FR8HVrrhlVBf2co -G506J9C31san59jjtsMxHnrDvinusnr/wpy25R0KwHBXseNk9YInKc5tnjVNNLOa -XZGAcKD7WMtbG20N9oqJl2aWf50CTj4IMBbSclw7fcok81Z7DK8a2uYINPk2ozTQ -6En5iIvFwTmFJwkAEQEAAQAL/2AWR22o3reGuCr/Po4AVJT+rhkZr9Yb9BTK7lx6 -dyvKw9zeo2oJTeQRFlJIbvjIOFCKykWnV9yXBUdfgWrayPQVAF8DlrPCUlIhDmhK -YaH11hp88YZFJuYzqh89RU7eK4cs+sSIx9MFhEa9I58aD+Z3KQ6+Vx1un2apWMI7 -RgheRsZMFQiy+uv0VW5UWgDTf2OfRQl2rFtAv/Tzl8VD2dorfiBdZaNEfJFikw7V -lpT/y30umduW+Uv6O/Snxaig2v/98IRgNbnwwxrC9l4nxftDJEURkzkQOZkC+pjZ -+8uzrND3aF7o3lXKDmW0gw4ECW9GQpkebde2xLfyvTh+3kLHTYWjf1UoaY4R/3U7 -wxQySH5d1tOf3fUw0C1XNVL8octTT5AFIOvPhCwh4yyhX1HYzE63Eu2qlptANj0S -uFMpuFsGmxQV4W7ULVRf1MFHV+upq73hCuT2Rtx7GHFlhm6e41XcIF67B4n3rG1p -BIByaNGGy/iGnsQXxJUEUy0pyQYA8M7whL84GazJ6zrR9cBkrWxhIupX5nxJUqTu -wofSkc0DAL4fllIi7PkE8EQZsyyGZ4zljHs4VdikNnh8eAkB0VwMlZBqE6dZAmqz -CVbD943q661INdBxvKU+SlVSHDBeBHLjlxV2pTnmYP+iTLUyyCZlO8m7Hj6z5ZbB -dxpObA/7K0w6Tm9Dja9fMqiFkcrz5s+lEqwRBuHSGoJlcNijmbqQSPkIs3jC9Z81 -jzK4oZvp05yEcyadQc4SWupEcsQ3BgDzvRTJytnNVEQLy9CLJaj9JKIWC8gQ1u/w -Us/sEmHk9/xEg9cI6E6OAExNa8we1wzIoBJNkNaxH5ssvuTUp72rXUAf77nftHbi -iII7QDO+qZM/JmMCtchwh1AQRJqliQTMif5UJI8eO6NHjRX3460yisNx8yHSQbDG -pYUBU86eAtBWJoeM+tX8Pzba4+X1yply5SK5SxsLz91VpkG5HqulrqmySTHcTHSR -RawNnDEdiM/SIaYZ6mTLDey+SbrETr8F/0pKkNRdX5Jt0pKI5AyiqU9a50RsggG3 -7W+5SwbcMlNXx/FzM7XklmuLb0tjbo2tmWSZVC6ewrmWOSsJ58Hz447BWM+e3gJF -8+81Ko0fKidQBPDTJlR1xQhuIAfqVti2QMl9P81moIp/yks9V0fBmhhBTvpSG4nA -fE6x1n6+13la1GHAHMbbtLv7rLZ7ly5yTaYewoZZZgJbms9oTrRWzsq7wDwYXzWI -VeAVTFLkUnxk2aD7+XEL7QrkIHwHjWveHua+tBtQYXRyaWNrIDxwYXRyaWNrQGxv -Y2FsaG9zdD6JAdIEEwEKADwWIQQgIAsdfZhSAa/Tv/C756VEEqufYwUCYf/ANgIb -AwUJA8JnAAQLCQgHBBUKCQgFFgIDAQACHgECF4AACgkQu+elRBKrn2Poywv+KeLR -3aHRmPioVjmiXdDnkQFoAXlmhgtUcfnCHaLJ9bPuoe/2PiI5O+gEHpLfwufn+7Dq -I3ve3oZL3BaCuUy1qboU2yT8vCEMkUlrqErrrYws6Fz3Gn3uLcHeoycfvrhN6FVk -40+btcApnRKWdUq0XOgS6MdCz5nfHq9RQZ73zNVYIIlK6HeuUj2OSFbmHogmI+wO -OopU0ZE48PLKKkP38N9Rr6SKk8VPyRrfLq+Guq50LfYz2gMuyEzoaYQT0A8oPVHu -6fquoLaKHnKgW62PPriBQB0pITmkmDNUNMJZ60fKZtNF/EI3jSYgquILyFaKkYKm -Sd8ghqp3LXTzH1JX2N4ant3z5AQQGcL2HafCxPw+C+ipVnfSH2qTvqUDjTuIxAFx -4l75o/B16zI4t7cQlQzeBNAu4TyFAKkUUKfzshi99PNQ4pPxMFBNROWzDb8/GXeP -T+P4gQo4CwukP+/GAxtqpOuvlDu8sfFo66F0FQWOvR8QGLdIxiadEwqesWMxnQVY -BGH/wDYBDACj11gdzw0YfmwrjLKae4z/J5D5ivHjE9GD4a1zHOQmgrt4mYIUjVt5 -F30EERnHEl1fIlAZkMuLcgmCfGwmjz/mJsji8yb+dbZlIGPBs2aw2Ikznzx7lsO/ -u6SK2w+SkJhYmhW3zMyFSYLgxINVxQWBhUNaJhFHZnHD1iE20QLVQEunh8ReuoQH -a0ErG/g0Url1vBlmAg99R5YR2uwRPbdso3PDA5f3EbDzCRg/XZtK/yQhPSt7DAhl -Ya+2+Ovh5oZ2GowiFuXYteE8yEiyP4IPy5DvuB20c2QtBkHyBr2a3/+DujJGL5Fh -U+E0+ClHrsfCWOD4+sHSn+NUCz+8FvGVMepJPWyx3rdd4rLnzb9h45Q9lXEBfIEQ -KdltxE+EdYFIPDpz0a4AOeBghdpQe5fREaSomGgGyqUFLqVJRNbE6509gtfMiGld -11lRaZ9PgKSm7JbIjSDF4ZbA859ipPicuu8eW2Y7PAUOLfc5QLzBOQHA/uMadWnY -WZwFJLIYROkAEQEAAQAL+QEoZcrjIk9uoEbQAhiZoCnS7qE20EYHpzLAguRl+z5C -7P55jjvlMlTpG7TuRoF7wZ1pHYoKtgeEnSjXBoAgwcW3dzK0X22LqSfuikntgb+k -7hZHbSrd6kD1+2AQU3w4iZ0RrK7dc4ILHpHGTbvKzkLHrW3LCFL5+DqXLimoITYe -09IJoXN+a62uPjoG4vKCtaUNeNv5zoB3A6pZYtLt3diWkJw7j6S7MyYKhcl32L+3 -TRrvhtnCIGKQBcj8GhWg9oYkWoA5bDg10lZiEhh98EWKoFWMbZ327VOENYAkYgr7 -ApyupgzWqKf9yt2jUHaBL4UnAYFgnq824+9e0oNohDGstXt5C7JcX/+x+JzHYwti -FOKsfj627QOW0F/wiIn2up9ZvF1yMLqwgIA2EsjYY291p7OD0PGWIqhmQvOacsBD -ZXIuY8F2+2CPmwtvrqBafFrA8oEpv/2vMuLnfdFtaiMUUnXzUcz2kI5f6uphIl4M -wWwfVN7v+qhNVhBDTMOkwQYAxTTSfVcg3SV9WalguAj2mDpvEg/JEEAKgNM2mnz8 -Y/3JHVdFNFdSylc8mh8+3MW2xkfnHYA6+D5YyHb0hd3qlJuef2M8HzbJXlrtFiG3 -t5Kd4W9t+RE4wW8hnBc8pfHhUeIMxky0rldhl70+Sj8cjFx/FWNLBQydEo3OXdm6 -/en11hOu0jktbE8P/ohK91PmWZwGTYPJcktddgUh71ajnKkUa+hhXSopy87V/pgc -JnEYQFsTZvIf5qBFGCG0lospBgDUsAcgQ/sUjc6qTj+gF9vWJXsKfm+l51KElohr -KBbUmZxZHTfWpvtqLA12MjNp7hi+ayDA8hjxsa3HNHP9M8nilYSxK2v6VENcnnkx -F/x18OitDsV97Py1XNY4IHnBI3cDfV4DcasZyhF+vbHVoqhDwmS0KBO7kPvWNJRi -zV/J9xrSAG8ww4ppoWEAHcDxgWiyt/8KwNfzO0EuiBr28W5//Rp1xDS7mKbZXEZO -vPF7sF2mo/QI/4ovoyo8M7AU48EGAIRCfwPmGstu/3GW/YyOPrQaNBpB9G+Rnpvo -lQ8K++hhRIQmGPpbUTLydmY1U7V8ZPob8PpT+wVgkAq8OYYHoHSYK1EhmqBEJaXT -3YtKLYVtwg+frKO2k+WKhrxbxL5aBa6Vsx+YQzcz8L/mTtwlCORzyertdJ+IyY9y -eXW/3Pp/HrxN9s5Ioa/HKL3idhABKCx/mqKhfJ28dKWjTn/RVImgBZKGkPvUrzFN -0uT9WYHSW29yzWVtLnENKVQ3bz+OJ+SqiQG8BBgBCgAmFiEEICALHX2YUgGv07/w -u+elRBKrn2MFAmH/wDYCGwwFCQPCZwAACgkQu+elRBKrn2Mn3wwAjITl+3zbS2RA -L6MUUqCxmqRmWRoSjU8R4nb45NJvm11C0IYk/0MvZg8FTSjqf65uRrYnZzJPWW/0 -UTS314bQaezLZTwUfrjrGRnUMKayVpPr+24ZZoRFDIs6Wnd8PtLzh0jy8jnwQVjV -DN/9ktruNMf5lB6kIuAHQtXyUNepxdRFaF79Z21zKUeTcyfLR7jKicC/55NakWI3 -GwbGCvUS0oaWXEHTIT+OjfA0jyfAo1cBvGU2tfUTYjLcFwWxV4KDJNAXfZWm9u6G -zXJ4IVwtHTdztbR4PzP9VnPbxGeGL+UyRj+kdh1WBGg5pXnWeoHaAQjT/DXScFON -OQ/MCj/Ch5lxdl8kLoY8Hn5ADn3WiXeBONZiP6lIDhh3jFdPZOQWxBjFHozLQTok -RRAYjPLTrppnDH+s5FDZzbeWwRv+yBqfo0s/97bjQEw4HeiJwX4yPupV+5gnovca -3994zx37Xsw54NJaoln7fZ4qBYqgL3Z74sTuF62usumUM1KHbkeC -=OpBu ------END PGP PRIVATE KEY BLOCK----- diff --git a/lib/tests/keys/patrick.pub.asc b/lib/tests/keys/patrick.pub.asc deleted file mode 100644 index ffe351d..0000000 --- a/lib/tests/keys/patrick.pub.asc +++ /dev/null @@ -1,41 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQGNBGH/wDYBDADlRkqjj5jOTBc0p+9Fk8sIstXjLbxUl4lMsw9Mh6rnuoCVc49D -nlG8ZbqS/j2jpNE8e4F3rFCkLnirGLT9tYIDE0xC6/B8AtDJNSaxb0AJKqIR4v6O -qunndGrg616H7U55NcLCT9zEJ8+lo/i7b0KcKt7RVdw064Vj1KwhEeEgdQ8WCrsq -TA18f3HBRS5ChqEDxYwYfet5rn5BF0ok5/aWHJkxOh+VnZwszjahxkzJ6BtDOJq+ -HGrhCFT+YCxLmFJIGZF95RPOH2TBCqJweh83opY/cnbu8zV4Zh2tGQu/ohZC2uPM -G/n6QoXv/7n/7/8dTtHH01enoCJxxSONfPg/F4PlUyZJcQOI+FR8HVrrhlVBf2co -G506J9C31san59jjtsMxHnrDvinusnr/wpy25R0KwHBXseNk9YInKc5tnjVNNLOa -XZGAcKD7WMtbG20N9oqJl2aWf50CTj4IMBbSclw7fcok81Z7DK8a2uYINPk2ozTQ -6En5iIvFwTmFJwkAEQEAAbQbUGF0cmljayA8cGF0cmlja0Bsb2NhbGhvc3Q+iQHS -BBMBCgA8FiEEICALHX2YUgGv07/wu+elRBKrn2MFAmH/wDYCGwMFCQPCZwAECwkI -BwQVCgkIBRYCAwEAAh4BAheAAAoJELvnpUQSq59j6MsL/ini0d2h0Zj4qFY5ol3Q -55EBaAF5ZoYLVHH5wh2iyfWz7qHv9j4iOTvoBB6S38Ln5/uw6iN73t6GS9wWgrlM -tam6FNsk/LwhDJFJa6hK662MLOhc9xp97i3B3qMnH764TehVZONPm7XAKZ0SlnVK -tFzoEujHQs+Z3x6vUUGe98zVWCCJSuh3rlI9jkhW5h6IJiPsDjqKVNGROPDyyipD -9/DfUa+kipPFT8ka3y6vhrqudC32M9oDLshM6GmEE9APKD1R7un6rqC2ih5yoFut -jz64gUAdKSE5pJgzVDTCWetHymbTRfxCN40mIKriC8hWipGCpknfIIaqdy108x9S -V9jeGp7d8+QEEBnC9h2nwsT8PgvoqVZ30h9qk76lA407iMQBceJe+aPwdesyOLe3 -EJUM3gTQLuE8hQCpFFCn87IYvfTzUOKT8TBQTUTlsw2/Pxl3j0/j+IEKOAsLpD/v -xgMbaqTrr5Q7vLHxaOuhdBUFjr0fEBi3SMYmnRMKnrFjMbkBjQRh/8A2AQwAo9dY -Hc8NGH5sK4yymnuM/yeQ+Yrx4xPRg+GtcxzkJoK7eJmCFI1beRd9BBEZxxJdXyJQ -GZDLi3IJgnxsJo8/5ibI4vMm/nW2ZSBjwbNmsNiJM588e5bDv7ukitsPkpCYWJoV -t8zMhUmC4MSDVcUFgYVDWiYRR2Zxw9YhNtEC1UBLp4fEXrqEB2tBKxv4NFK5dbwZ -ZgIPfUeWEdrsET23bKNzwwOX9xGw8wkYP12bSv8kIT0rewwIZWGvtvjr4eaGdhqM -Ihbl2LXhPMhIsj+CD8uQ77gdtHNkLQZB8ga9mt//g7oyRi+RYVPhNPgpR67Hwljg -+PrB0p/jVAs/vBbxlTHqST1ssd63XeKy582/YeOUPZVxAXyBECnZbcRPhHWBSDw6 -c9GuADngYIXaUHuX0RGkqJhoBsqlBS6lSUTWxOudPYLXzIhpXddZUWmfT4CkpuyW -yI0gxeGWwPOfYqT4nLrvHltmOzwFDi33OUC8wTkBwP7jGnVp2FmcBSSyGETpABEB -AAGJAbwEGAEKACYWIQQgIAsdfZhSAa/Tv/C756VEEqufYwUCYf/ANgIbDAUJA8Jn -AAAKCRC756VEEqufYyffDACMhOX7fNtLZEAvoxRSoLGapGZZGhKNTxHidvjk0m+b -XULQhiT/Qy9mDwVNKOp/rm5GtidnMk9Zb/RRNLfXhtBp7MtlPBR+uOsZGdQwprJW -k+v7bhlmhEUMizpad3w+0vOHSPLyOfBBWNUM3/2S2u40x/mUHqQi4AdC1fJQ16nF -1EVoXv1nbXMpR5NzJ8tHuMqJwL/nk1qRYjcbBsYK9RLShpZcQdMhP46N8DSPJ8Cj -VwG8ZTa19RNiMtwXBbFXgoMk0Bd9lab27obNcnghXC0dN3O1tHg/M/1Wc9vEZ4Yv -5TJGP6R2HVYEaDmledZ6gdoBCNP8NdJwU405D8wKP8KHmXF2XyQuhjwefkAOfdaJ -d4E41mI/qUgOGHeMV09k5BbEGMUejMtBOiRFEBiM8tOummcMf6zkUNnNt5bBG/7I -Gp+jSz/3tuNATDgd6InBfjI+6lX7mCei9xrf33jPHftezDng0lqiWft9nioFiqAv -dnvixO4Xra6y6ZQzUoduR4I= -=CQBw ------END PGP PUBLIC KEY BLOCK----- diff --git a/lib/tests/test_imap_backend.rs b/lib/tests/test_imap_backend.rs deleted file mode 100644 index 081d045..0000000 --- a/lib/tests/test_imap_backend.rs +++ /dev/null @@ -1,77 +0,0 @@ -#[cfg(feature = "imap-backend")] -use himalaya_lib::{ - account::{Account, ImapBackendConfig}, - backend::{Backend, ImapBackend}, -}; - -#[cfg(feature = "imap-backend")] -#[test] -fn test_imap_backend() { - // configure accounts - let account_config = Account { - smtp_host: "localhost".into(), - smtp_port: 3465, - smtp_starttls: false, - smtp_insecure: true, - smtp_login: "inbox@localhost".into(), - smtp_passwd_cmd: "echo 'password'".into(), - ..Account::default() - }; - let imap_config = ImapBackendConfig { - imap_host: "localhost".into(), - imap_port: 3993, - imap_starttls: false, - imap_insecure: true, - imap_login: "inbox@localhost".into(), - imap_passwd_cmd: "echo 'password'".into(), - }; - let mut imap = ImapBackend::new(&account_config, &imap_config); - imap.connect().unwrap(); - - // set up mailboxes - if let Err(_) = imap.add_mbox("Mailbox1") {}; - if let Err(_) = imap.add_mbox("Mailbox2") {}; - imap.del_msg("Mailbox1", "1:*").unwrap(); - imap.del_msg("Mailbox2", "1:*").unwrap(); - - // check that a message can be added - let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let id = imap.add_msg("Mailbox1", msg, "seen").unwrap().to_string(); - - // check that the added message exists - let msg = imap.get_msg("Mailbox1", &id).unwrap(); - assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); - assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); - assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); - - // check that the envelope of the added message exists - let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - assert_eq!(1, envelopes.len()); - let envelope = envelopes.first().unwrap(); - assert_eq!("alice@localhost", envelope.sender); - assert_eq!("Plain message", envelope.subject); - - // check that the message can be copied - imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) - .unwrap(); - let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - assert_eq!(1, envelopes.len()); - let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - assert_eq!(1, envelopes.len()); - - // check that the message can be moved - imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string()) - .unwrap(); - let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap(); - assert_eq!(0, envelopes.len()); - let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap(); - assert_eq!(2, envelopes.len()); - let id = envelopes.first().unwrap().id.to_string(); - - // check that the message can be deleted - imap.del_msg("Mailbox2", &id).unwrap(); - assert!(imap.get_msg("Mailbox2", &id).is_err()); - - // check that disconnection works - imap.disconnect().unwrap(); -} diff --git a/lib/tests/test_maildir_backend.rs b/lib/tests/test_maildir_backend.rs deleted file mode 100644 index 8077aaf..0000000 --- a/lib/tests/test_maildir_backend.rs +++ /dev/null @@ -1,91 +0,0 @@ -use maildir::Maildir; -use std::{collections::HashMap, env, fs, iter::FromIterator}; - -use himalaya_lib::{ - account::{Account, MaildirBackendConfig}, - backend::{Backend, MaildirBackend}, - msg::Flag, -}; - -#[test] -fn test_maildir_backend() { - // set up maildir folders - let mdir: Maildir = env::temp_dir().join("himalaya-test-mdir").into(); - if let Err(_) = fs::remove_dir_all(mdir.path()) {} - mdir.create_dirs().unwrap(); - - let mdir_sub: Maildir = mdir.path().join(".Subdir").into(); - if let Err(_) = fs::remove_dir_all(mdir_sub.path()) {} - mdir_sub.create_dirs().unwrap(); - - // configure accounts - let account_config = Account { - mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]), - ..Account::default() - }; - let mdir_config = MaildirBackendConfig { - maildir_dir: mdir.path().to_owned(), - }; - let mut mdir = MaildirBackend::new(&account_config, &mdir_config); - let mdir_sub_config = MaildirBackendConfig { - maildir_dir: mdir_sub.path().to_owned(), - }; - let mut mdir_subdir = MaildirBackend::new(&account_config, &mdir_sub_config); - - // check that a message can be added - let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let hash = mdir.add_msg("inbox", msg, "seen").unwrap(); - - // check that the added message exists - let msg = mdir.get_msg("inbox", &hash).unwrap(); - assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); - assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); - assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); - - // check that the envelope of the added message exists - let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert_eq!(1, envelopes.len()); - assert_eq!("alice@localhost", envelope.sender); - assert_eq!("Plain message", envelope.subject); - - // check that a flag can be added to the message - mdir.add_flags("inbox", &envelope.id, "flagged").unwrap(); - let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&Flag::Seen)); - assert!(envelope.flags.contains(&Flag::Flagged)); - - // check that the message flags can be changed - mdir.set_flags("inbox", &envelope.id, "answered").unwrap(); - let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&Flag::Seen)); - assert!(!envelope.flags.contains(&Flag::Flagged)); - assert!(envelope.flags.contains(&Flag::Answered)); - - // check that a flag can be removed from the message - mdir.del_flags("inbox", &envelope.id, "answered").unwrap(); - let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(!envelope.flags.contains(&Flag::Seen)); - assert!(!envelope.flags.contains(&Flag::Flagged)); - assert!(!envelope.flags.contains(&Flag::Answered)); - - // check that the message can be copied - mdir.copy_msg("inbox", "subdir", &envelope.id).unwrap(); - assert!(mdir.get_msg("inbox", &hash).is_ok()); - assert!(mdir.get_msg("subdir", &hash).is_ok()); - assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); - - // check that the message can be moved - mdir.move_msg("inbox", "subdir", &envelope.id).unwrap(); - assert!(mdir.get_msg("inbox", &hash).is_err()); - assert!(mdir.get_msg("subdir", &hash).is_ok()); - assert!(mdir_subdir.get_msg("inbox", &hash).is_ok()); - - // check that the message can be deleted - mdir.del_msg("subdir", &hash).unwrap(); - assert!(mdir.get_msg("subdir", &hash).is_err()); - assert!(mdir_subdir.get_msg("inbox", &hash).is_err()); -} diff --git a/lib/tests/test_notmuch_backend.rs b/lib/tests/test_notmuch_backend.rs deleted file mode 100644 index dae4e43..0000000 --- a/lib/tests/test_notmuch_backend.rs +++ /dev/null @@ -1,86 +0,0 @@ -#[cfg(feature = "notmuch-backend")] -use std::{collections::HashMap, env, fs, iter::FromIterator}; - -#[cfg(feature = "notmuch-backend")] -use himalaya_lib::{ - account::{Account, MaildirBackendConfig, NotmuchBackendConfig}, - backend::{Backend, MaildirBackend, NotmuchBackend}, -}; - -#[cfg(feature = "notmuch-backend")] -#[test] -fn test_notmuch_backend() { - use himalaya_lib::msg::Flag; - - // set up maildir folders and notmuch database - let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into(); - if let Err(_) = fs::remove_dir_all(mdir.path()) {} - mdir.create_dirs().unwrap(); - notmuch::Database::create(mdir.path()).unwrap(); - - // configure accounts - let account_config = AccountConfig { - mailboxes: HashMap::from_iter([("inbox".into(), "*".into())]), - ..AccountConfig::default() - }; - let mdir_config = MaildirBackendConfig { - maildir_dir: mdir.path().to_owned(), - }; - let notmuch_config = NotmuchBackendConfig { - notmuch_database_dir: mdir.path().to_owned(), - }; - let mut mdir = MaildirBackend::new(&account_config, &mdir_config); - let mut notmuch = NotmuchBackend::new(&account_config, ¬much_config, &mut mdir).unwrap(); - - // check that a message can be added - let msg = include_bytes!("./emails/alice-to-patrick.eml"); - let hash = notmuch.add_msg("", msg, "inbox seen").unwrap().to_string(); - - // check that the added message exists - let msg = notmuch.get_msg("", &hash).unwrap(); - assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string()); - assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string()); - assert_eq!("Ceci est un message.", msg.fold_text_plain_parts()); - - // check that the envelope of the added message exists - let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert_eq!(1, envelopes.len()); - assert_eq!("alice@localhost", envelope.sender); - assert_eq!("Plain message", envelope.subject); - - // check that a flag can be added to the message - notmuch - .add_flags("", &envelope.id, "flagged answered") - .unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); - assert!(envelope.flags.contains(&Flag::Custom("seen".into()))); - assert!(envelope.flags.contains(&Flag::Custom("flagged".into()))); - assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); - - // check that the message flags can be changed - notmuch - .set_flags("", &envelope.id, "inbox answered") - .unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); - assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); - assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); - assert!(envelope.flags.contains(&Flag::Custom("answered".into()))); - - // check that a flag can be removed from the message - notmuch.del_flags("", &envelope.id, "answered").unwrap(); - let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap(); - let envelope = envelopes.first().unwrap(); - assert!(envelope.flags.contains(&Flag::Custom("inbox".into()))); - assert!(!envelope.flags.contains(&Flag::Custom("seen".into()))); - assert!(!envelope.flags.contains(&Flag::Custom("flagged".into()))); - assert!(!envelope.flags.contains(&Flag::Custom("answered".into()))); - - // check that the message can be deleted - notmuch.del_msg("", &hash).unwrap(); - assert!(notmuch.get_msg("inbox", &hash).is_err()); -} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index a04de65..0000000 --- a/rustfmt.toml +++ /dev/null @@ -1,74 +0,0 @@ -max_width = 100 -hard_tabs = false -tab_spaces = 4 -newline_style = "Auto" -indent_style = "Block" -use_small_heuristics = "Default" -fn_call_width = 60 -attr_fn_like_width = 70 -struct_lit_width = 18 -struct_variant_width = 35 -array_width = 60 -chain_width = 60 -single_line_if_else_max_width = 50 -wrap_comments = false -format_code_in_doc_comments = false -comment_width = 80 -normalize_comments = false -normalize_doc_attributes = false -license_template_path = "" -format_strings = false -format_macro_matchers = false -format_macro_bodies = true -empty_item_single_line = true -struct_lit_single_line = true -fn_single_line = false -where_single_line = false -imports_indent = "Block" -imports_layout = "Mixed" -imports_granularity = "Preserve" -group_imports = "Preserve" -reorder_imports = true -reorder_modules = true -reorder_impl_items = false -type_punctuation_density = "Wide" -space_before_colon = false -space_after_colon = true -spaces_around_ranges = false -binop_separator = "Front" -remove_nested_parens = true -combine_control_expr = true -overflow_delimited_expr = false -struct_field_align_threshold = 0 -enum_discrim_align_threshold = 0 -match_arm_blocks = true -match_arm_leading_pipes = "Never" -force_multiline_blocks = false -fn_args_layout = "Tall" -brace_style = "SameLineWhere" -control_brace_style = "AlwaysSameLine" -trailing_semicolon = true -trailing_comma = "Vertical" -match_block_trailing_comma = false -blank_lines_upper_bound = 1 -blank_lines_lower_bound = 0 -edition = "2015" -version = "One" -inline_attribute_width = 0 -merge_derives = true -use_try_shorthand = false -use_field_init_shorthand = false -force_explicit_abi = true -condense_wildcard_suffixes = false -color = "Auto" -unstable_features = false -disable_all_formatting = false -skip_children = false -hide_parse_errors = false -error_on_line_overflow = false -error_on_unformatted = false -report_todo = "Never" -report_fixme = "Never" -ignore = [] -emit_mode = "Files" -make_backup = false diff --git a/cli/src/compl/compl_args.rs b/src/compl/args.rs similarity index 100% rename from cli/src/compl/compl_args.rs rename to src/compl/args.rs diff --git a/cli/src/compl/compl_handlers.rs b/src/compl/handlers.rs similarity index 100% rename from cli/src/compl/compl_handlers.rs rename to src/compl/handlers.rs diff --git a/src/compl/mod.rs b/src/compl/mod.rs new file mode 100644 index 0000000..12d2685 --- /dev/null +++ b/src/compl/mod.rs @@ -0,0 +1,8 @@ +//! 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; diff --git a/cli/src/config/config_args.rs b/src/config/args.rs similarity index 100% rename from cli/src/config/config_args.rs rename to src/config/args.rs diff --git a/src/config/config.rs b/src/config/config.rs new file mode 100644 index 0000000..7e20f9a --- /dev/null +++ b/src/config/config.rs @@ -0,0 +1,576 @@ +// himalaya-lib, a Rust library for email management. +// Copyright (C) 2022 soywod +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! 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, + pub signature_delim: Option, + pub signature: Option, + pub downloads_dir: Option, + + pub folder_listing_page_size: Option, + pub folder_aliases: Option>, + + pub email_listing_page_size: Option, + pub email_reading_headers: Option>, + #[serde(default, with = "email_text_plain_format")] + pub email_reading_format: Option, + pub email_reading_decrypt_cmd: Option, + pub email_writing_encrypt_cmd: Option, + #[serde(default, with = "email_hooks")] + pub email_hooks: Option, + + #[serde(flatten)] + pub accounts: HashMap, +} + +impl DeserializedConfig { + /// Tries to create a config from an optional path. + pub fn from_opt_path(path: Option<&str>) -> Result { + 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 { + 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 { + 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 { + 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 { + 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::{ + EmailSendCmd, EmailSender, ImapConfig, MaildirConfig, NotmuchConfig, SmtpConfig, + }; + use std::io::Write; + use tempfile::NamedTempFile; + + use crate::account::{ + DeserializedBaseAccountConfig, DeserializedImapAccountConfig, + DeserializedMaildirAccountConfig, DeserializedNotmuchAccountConfig, + }; + + use super::*; + + fn make_config(config: &str) -> Result { + 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_eq!( + config.unwrap_err().root_cause().to_string(), + "unknown variant `bad`, expected one of `none`, `imap`, `maildir`, `notmuch` at line 1 column 1" + ); + } + + #[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" + ); + } + + #[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`, `internal`, `external` at line 1 column 1", + ); + } + + #[test] + fn account_internal_sender_missing_host_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"internal\"", + ); + + assert_eq!( + config.unwrap_err().root_cause().to_string(), + "missing field `smtp-host` at line 1 column 1" + ); + } + + #[test] + fn account_internal_sender_missing_port_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"internal\" + smtp-host = \"localhost\"", + ); + + assert_eq!( + config.unwrap_err().root_cause().to_string(), + "missing field `smtp-port` at line 1 column 1" + ); + } + + #[test] + fn account_internal_sender_missing_login_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"internal\" + 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_internal_sender_missing_passwd_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"internal\" + 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_external_sender_missing_cmd_field() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"external\"", + ); + + assert_eq!( + config.unwrap_err().root_cause().to_string(), + "missing field `send-cmd` at line 1 column 1" + ); + } + + #[test] + fn account_internal_sender_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"internal\" + 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::Internal(SmtpConfig { + host: "localhost".into(), + port: 25, + login: "login".into(), + passwd_cmd: "echo password".into(), + ..SmtpConfig::default() + }), + ..DeserializedBaseAccountConfig::default() + }) + )]), + ..DeserializedConfig::default() + } + ); + } + + #[test] + fn account_external_sender_minimum_config() { + let config = make_config( + "[account] + email = \"test@localhost\" + backend = \"none\" + sender = \"external\" + send-cmd = \"echo send\"", + ); + + assert_eq!( + config.unwrap(), + DeserializedConfig { + accounts: HashMap::from_iter([( + "account".into(), + DeserializedAccountConfig::None(DeserializedBaseAccountConfig { + email: "test@localhost".into(), + email_sender: EmailSender::External(EmailSendCmd { + 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() + } + ); + } + + #[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() + } + ); + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..20ee5fb --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,5 @@ +pub mod args; +pub mod config; +pub mod prelude; + +pub use config::*; diff --git a/src/config/prelude.rs b/src/config/prelude.rs new file mode 100644 index 0000000..dc4034e --- /dev/null +++ b/src/config/prelude.rs @@ -0,0 +1,139 @@ +use himalaya_lib::{EmailHooks, EmailSendCmd, EmailSender, EmailTextPlainFormat, 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, + #[serde(rename = "smtp-insecure")] + pub insecure: Option, + #[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, + #[serde(rename = "imap-insecure")] + pub insecure: Option, + #[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, + #[serde(rename = "imap-notify-query")] + pub notify_query: Option, + #[serde(rename = "imap-watch-cmds")] + pub watch_cmds: Option>, +} + +#[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, 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")] + Internal(SmtpConfig), + #[serde(with = "EmailSendCmdDef")] + External(EmailSendCmd), +} + +#[derive(Debug, Clone, Eq, PartialEq, Deserialize)] +#[serde(remote = "EmailSendCmd")] +pub struct EmailSendCmdDef { + #[serde(rename = "send-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, +} + +pub mod email_hooks { + use himalaya_lib::EmailHooks; + use serde::{Deserialize, Deserializer}; + + use super::EmailHooksDef; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, 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)) + } +} diff --git a/src/domain/account/account.rs b/src/domain/account/account.rs new file mode 100644 index 0000000..8ddae67 --- /dev/null +++ b/src/domain/account/account.rs @@ -0,0 +1,54 @@ +//! 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()) + } +} diff --git a/src/domain/account/accounts.rs b/src/domain/account/accounts.rs new file mode 100644 index 0000000..b948ac8 --- /dev/null +++ b/src/domain/account/accounts.rs @@ -0,0 +1,61 @@ +//! 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); + +impl Deref for Accounts { + type Target = Vec; + + 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> 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) + } +} diff --git a/cli/src/config/account_args.rs b/src/domain/account/args.rs similarity index 95% rename from cli/src/config/account_args.rs rename to src/domain/account/args.rs index e14fb2f..c5e75b1 100644 --- a/cli/src/config/account_args.rs +++ b/src/domain/account/args.rs @@ -4,7 +4,7 @@ use anyhow::Result; use clap::{App, Arg, ArgMatches, SubCommand}; use log::{debug, info}; -use crate::ui::table_arg; +use crate::ui::table; type MaxTableWidth = Option; @@ -41,7 +41,7 @@ pub fn subcmds<'a>() -> Vec> { vec![SubCommand::with_name("accounts") .aliases(&["account", "acc", "a"]) .about("Lists accounts") - .arg(table_arg::max_width())] + .arg(table::args::max_width())] } /// Represents the user account name argument. diff --git a/src/domain/account/config.rs b/src/domain/account/config.rs new file mode 100644 index 0000000..bfaa30d --- /dev/null +++ b/src/domain/account/config.rs @@ -0,0 +1,224 @@ +// himalaya-lib, a Rust library for email management. +// Copyright (C) 2022 soywod +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! 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, ImapConfig, + MaildirConfig, 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, + pub display_name: Option, + pub signature_delim: Option, + pub signature: Option, + pub downloads_dir: Option, + + pub folder_listing_page_size: Option, + pub folder_aliases: Option>, + + pub email_listing_page_size: Option, + pub email_reading_headers: Option>, + #[serde(default, with = "email_text_plain_format")] + pub email_reading_format: Option, + pub email_reading_decrypt_cmd: Option, + pub email_writing_encrypt_cmd: Option, + #[serde(flatten, with = "EmailSenderDef")] + pub email_sender: EmailSender, + #[serde(default, with = "email_hooks")] + pub email_hooks: Option, +} + +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, +} diff --git a/cli/src/config/account_handlers.rs b/src/domain/account/handlers.rs similarity index 72% rename from cli/src/config/account_handlers.rs rename to src/domain/account/handlers.rs index 4e0e082..b8a1c2f 100644 --- a/cli/src/config/account_handlers.rs +++ b/src/domain/account/handlers.rs @@ -3,30 +3,31 @@ //! This module gathers all account actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::account::{Account, DeserializedConfig}; +use himalaya_lib::AccountConfig; use log::{info, trace}; use crate::{ - config::Accounts, - output::{PrintTableOpts, PrinterService}, + config::DeserializedConfig, + printer::{PrintTableOpts, Printer}, + Accounts, }; /// Lists all accounts. -pub fn list<'a, P: PrinterService>( +pub fn list<'a, P: Printer>( max_width: Option, - config: &DeserializedConfig, - account_config: &Account, + config: &AccountConfig, + deserialized_config: &DeserializedConfig, printer: &mut P, ) -> Result<()> { info!(">> account list handler"); - let accounts: Accounts = config.accounts.iter().into(); + let accounts: Accounts = deserialized_config.accounts.iter().into(); trace!("accounts: {:?}", accounts); printer.print_table( Box::new(accounts), PrintTableOpts { - format: &account_config.format, + format: &config.email_reading_format, max_width, }, )?; @@ -37,13 +38,16 @@ pub fn list<'a, P: PrinterService>( #[cfg(test)] mod tests { - use himalaya_lib::account::{ - Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig, - }; - use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator}; + use himalaya_lib::{AccountConfig, ImapConfig}; + use std::{collections::HashMap, fmt::Debug, io}; use termcolor::ColorSpec; - use crate::output::{Print, PrintTable, WriteColor}; + use crate::{ + account::{ + DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedImapAccountConfig, + }, + printer::{Print, PrintTable, WriteColor}, + }; use super::*; @@ -88,7 +92,7 @@ mod tests { pub writer: StringWriter, } - impl PrinterService for PrinterServiceTest { + impl Printer for PrinterServiceTest { fn print_table( &mut self, data: Box, @@ -111,21 +115,23 @@ mod tests { } } - let config = DeserializedConfig { + let mut printer = PrinterServiceTest::default(); + let config = AccountConfig::default(); + let deserialized_config = DeserializedConfig { accounts: HashMap::from_iter([( "account-1".into(), DeserializedAccountConfig::Imap(DeserializedImapAccountConfig { - default: Some(true), - ..DeserializedImapAccountConfig::default() + base: DeserializedBaseAccountConfig { + default: Some(true), + ..DeserializedBaseAccountConfig::default() + }, + backend: ImapConfig::default(), }), )]), ..DeserializedConfig::default() }; - let account_config = Account::default(); - let mut printer = PrinterServiceTest::default(); - - assert!(list(None, &config, &account_config, &mut printer).is_ok()); + assert!(list(None, &config, &deserialized_config, &mut printer).is_ok()); assert_eq!( concat![ "\n", diff --git a/src/domain/account/mod.rs b/src/domain/account/mod.rs new file mode 100644 index 0000000..2a2854a --- /dev/null +++ b/src/domain/account/mod.rs @@ -0,0 +1,9 @@ +pub mod account; +pub mod accounts; +pub mod args; +pub mod config; +pub mod handlers; + +pub use account::*; +pub use accounts::*; +pub use config::*; diff --git a/cli/src/msg/msg_args.rs b/src/domain/email/args.rs similarity index 94% rename from cli/src/msg/msg_args.rs rename to src/domain/email/args.rs index b5b7862..1e69682 100644 --- a/cli/src/msg/msg_args.rs +++ b/src/domain/email/args.rs @@ -4,17 +4,10 @@ use anyhow::Result; use clap::{self, App, Arg, ArgMatches, SubCommand}; -use himalaya_lib::msg::TplOverride; +use himalaya_lib::email::TplOverride; use log::{debug, info, trace}; -use crate::{ - mbox::mbox_args, - msg::{ - flag_args, msg_args, - tpl_args::{self, from_args}, - }, - ui::table_arg, -}; +use crate::{email, flag, folder, tpl, ui::table}; type Seq<'a> = &'a str; type PageSize = usize; @@ -48,8 +41,8 @@ pub enum Cmd<'a> { Send(RawMsg<'a>), Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt), - Flag(Option>), - Tpl(Option>), + Flag(Option>), + Tpl(Option>), } /// Message command matcher. @@ -67,7 +60,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { info!("copy command matched"); let seq = m.value_of("seq").unwrap(); debug!("seq: {}", seq); - let mbox = m.value_of("mbox-target").unwrap(); + let mbox = m.value_of("folder-target").unwrap(); debug!(r#"target mailbox: "{:?}""#, mbox); return Ok(Some(Cmd::Copy(seq, mbox))); } @@ -113,7 +106,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { info!("move command matched"); let seq = m.value_of("seq").unwrap(); debug!("seq: {}", seq); - let mbox = m.value_of("mbox-target").unwrap(); + let mbox = m.value_of("folder-target").unwrap(); debug!("target mailbox: {:?}", mbox); return Ok(Some(Cmd::Move(seq, mbox))); } @@ -265,16 +258,16 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result>> { debug!("attachments paths: {:?}", attachment_paths); let encrypt = m.is_present("encrypt"); debug!("encrypt: {}", encrypt); - let tpl = from_args(m); + let tpl = tpl::args::from_args(m); return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt))); } if let Some(m) = m.subcommand_matches("template") { - return Ok(Some(Cmd::Tpl(tpl_args::matches(m)?))); + return Ok(Some(Cmd::Tpl(tpl::args::matches(m)?))); } if let Some(m) = m.subcommand_matches("flag") { - return Ok(Some(Cmd::Flag(flag_args::matches(m)?))); + return Ok(Some(Cmd::Flag(flag::args::matches(m)?))); } info!("default list command matched"); @@ -356,25 +349,25 @@ pub fn encrypt_arg<'a>() -> Arg<'a, 'a> { /// Message subcommands. pub fn subcmds<'a>() -> Vec> { vec![ - flag_args::subcmds(), - tpl_args::subcmds(), + flag::args::subcmds(), + tpl::args::subcmds(), vec![ SubCommand::with_name("attachments") .aliases(&["attachment", "att", "a"]) .about("Downloads all message attachments") - .arg(msg_args::seq_arg()), + .arg(email::args::seq_arg()), SubCommand::with_name("list") .aliases(&["lst", "l"]) .about("Lists all messages") .arg(page_size_arg()) .arg(page_arg()) - .arg(table_arg::max_width()), + .arg(table::args::max_width()), SubCommand::with_name("search") .aliases(&["s", "query", "q"]) .about("Lists messages matching the given IMAP query") .arg(page_size_arg()) .arg(page_arg()) - .arg(table_arg::max_width()) + .arg(table::args::max_width()) .arg( Arg::with_name("query") .help("IMAP query") @@ -387,7 +380,7 @@ pub fn subcmds<'a>() -> Vec> { .about("Sorts messages by the given criteria and matching the given IMAP query") .arg(page_size_arg()) .arg(page_arg()) - .arg(table_arg::max_width()) + .arg(table::args::max_width()) .arg( Arg::with_name("criterion") .long("criterion") @@ -417,7 +410,7 @@ pub fn subcmds<'a>() -> Vec> { ), SubCommand::with_name("write") .about("Writes a new message") - .args(&tpl_args::tpl_args()) + .args(&tpl::args::tpl_args()) .arg(attachments_arg()) .arg(encrypt_arg()), SubCommand::with_name("send") @@ -462,12 +455,12 @@ pub fn subcmds<'a>() -> Vec> { .aliases(&["cp", "c"]) .about("Copies a message to the targetted mailbox") .arg(seq_arg()) - .arg(mbox_args::target_arg()), + .arg(folder::args::target_arg()), SubCommand::with_name("move") .aliases(&["mv"]) .about("Moves a message to the targetted mailbox") .arg(seq_arg()) - .arg(mbox_args::target_arg()), + .arg(folder::args::target_arg()), SubCommand::with_name("delete") .aliases(&["del", "d", "remove", "rm"]) .about("Deletes a message") diff --git a/cli/src/msg/msg_handlers.rs b/src/domain/email/handlers.rs similarity index 63% rename from cli/src/msg/msg_handlers.rs rename to src/domain/email/handlers.rs index 4041fdf..12f8599 100644 --- a/cli/src/msg/msg_handlers.rs +++ b/src/domain/email/handlers.rs @@ -5,9 +5,7 @@ use anyhow::{Context, Result}; use atty::Stream; use himalaya_lib::{ - account::{Account, DEFAULT_SENT_FOLDER}, - backend::Backend, - msg::{Msg, Part, Parts, TextPlainPart, TplOverride}, + AccountConfig, Backend, Email, Part, Parts, Sender, TextPlainPart, TplOverride, }; use log::{debug, info, trace}; use mailparse::addrparse; @@ -19,31 +17,28 @@ use std::{ use url::Url; use crate::{ - output::{PrintTableOpts, PrinterService}, - smtp::SmtpService, + printer::{PrintTableOpts, Printer}, ui::editor, }; /// Downloads all message attachments to the user account downloads directory. -pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - let attachments = backend.get_msg(mbox, seq)?.attachments(); + 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)); + return printer.print_struct(format!("No attachment found for message {}", seq)); } printer.print_str(format!( - "Found {:?} attachment{} for message {:?}", - attachments_len, - if attachments_len > 1 { "s" } else { "" }, - seq + "{} attachment(s) found for message {}", + attachments_len, seq ))?; for attachment in attachments { @@ -53,77 +48,80 @@ pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>( .context(format!("cannot download attachment {:?}", file_path))?; } - printer.print_struct(format!( - "Attachment{} successfully downloaded to {:?}", - if attachments_len > 1 { "s" } else { "" }, - config.downloads_dir - )) + printer.print_struct("Done!") } /// Copy a message from a mailbox to another. -pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox_src: &str, mbox_dst: &str, printer: &mut P, - backend: Box<&mut B>, + backend: &mut B, ) -> Result<()> { - backend.copy_msg(mbox_src, mbox_dst, seq)?; + backend.email_copy(mbox_src, mbox_dst, seq)?; printer.print_struct(format!( - r#"Message {} successfully copied to folder "{}""#, + "Message {} successfully copied to folder {}", seq, mbox_dst )) } /// Delete messages matching the given sequence range. -pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox: &str, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - backend.del_msg(mbox, seq)?; - printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq)) + backend.email_delete(mbox, seq)?; + printer.print_struct(format!("Message(s) {} successfully deleted", seq)) } /// Forward the given message UID from the selected mailbox. -pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( seq: &str, attachments_paths: Vec<&str>, encrypt: bool, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { let msg = backend - .get_msg(mbox, seq)? + .email_get(mbox, seq)? .into_forward(config)? .add_attachments(attachments_paths)? .encrypt(encrypt); - editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; + editor::edit_msg_with_editor( + msg, + TplOverride::default(), + config, + printer, + backend, + sender, + )?; Ok(()) } /// List paginated messages from the selected mailbox. -pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( max_width: Option, page_size: Option, page: usize, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - imap: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - let page_size = page_size.unwrap_or(config.default_page_size); + let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let msgs = imap.get_envelopes(mbox, page_size, page)?; + let msgs = backend.envelope_list(mbox, page_size, page)?; trace!("envelopes: {:?}", msgs); printer.print_table( Box::new(msgs), PrintTableOpts { - format: &config.format, + format: &config.email_reading_format, max_width, }, ) @@ -132,12 +130,12 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Parses and edits a message from a [mailto] URL string. /// /// [mailto]: https://en.wikipedia.org/wiki/Mailto -pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( url: &Url, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { info!("entering mailto command handler"); @@ -165,7 +163,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( } } - let msg = Msg { + let msg = Email { from: Some(vec![config.address()?].into()), to: if to.is_empty() { None } else { Some(to) }, cc: if cc.is_empty() { @@ -182,23 +180,30 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( parts: Parts(vec![Part::TextPlain(TextPlainPart { content: body.into(), })]), - ..Msg::default() + ..Email::default() }; trace!("message: {:?}", msg); - editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?; + editor::edit_msg_with_editor( + msg, + TplOverride::default(), + config, + printer, + backend, + sender, + )?; Ok(()) } /// Move a message from a mailbox to another. -pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, mbox_src: &str, mbox_dst: &str, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - backend.move_msg(mbox_src, mbox_dst, seq)?; + backend.email_move(mbox_src, mbox_dst, seq)?; printer.print_struct(format!( r#"Message {} successfully moved to folder "{}""#, seq, mbox_dst @@ -206,17 +211,17 @@ pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>( } /// Read a message by its sequence number. -pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, text_mime: &str, raw: bool, headers: Vec<&str>, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - let msg = backend.get_msg(mbox, seq)?; + 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. @@ -227,33 +232,40 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>( } /// Reply to the given message UID. -pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +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: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { let msg = backend - .get_msg(mbox, seq)? + .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, smtp)? - .add_flags(mbox, seq, "replied")?; + 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 mailbox. -pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( mbox: &str, raw_msg: &str, printer: &mut P, - backend: Box<&mut B>, + backend: &mut B, ) -> Result<()> { info!("entering save message handler"); @@ -274,66 +286,66 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( .collect::>() .join("\r\n") }; - backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?; + backend.email_add(mbox, raw_msg.as_bytes(), "seen")?; Ok(()) } /// Paginate messages from the selected mailbox matching the specified query. -pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>( query: String, max_width: Option, page_size: Option, page: usize, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - let page_size = page_size.unwrap_or(config.default_page_size); + let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?; + let msgs = backend.envelope_search(mbox, &query, "", page_size, page)?; trace!("messages: {:#?}", msgs); printer.print_table( Box::new(msgs), PrintTableOpts { - format: &config.format, + format: &config.email_reading_format, max_width, }, ) } /// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria. -pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>( sort: String, query: String, max_width: Option, page_size: Option, page: usize, mbox: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { - let page_size = page_size.unwrap_or(config.default_page_size); + let page_size = page_size.unwrap_or(config.email_listing_page_size()); debug!("page size: {}", page_size); - let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?; + let msgs = backend.envelope_search(mbox, &query, &sort, page_size, page)?; trace!("envelopes: {:#?}", msgs); printer.print_table( Box::new(msgs), PrintTableOpts { - format: &config.format, + format: &config.email_reading_format, max_width, }, ) } /// Send a raw message. -pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( raw_msg: &str, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { info!("entering send message handler"); @@ -342,11 +354,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( let is_json = printer.is_json(); debug!("is json: {}", is_json); - let sent_folder = config - .mailboxes - .get("sent") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_SENT_FOLDER); + let sent_folder = config.folder_alias("sent")?; debug!("sent folder: {:?}", sent_folder); let raw_msg = if is_tty || is_json { @@ -360,25 +368,25 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .join("\r\n") }; trace!("raw message: {:?}", raw_msg); - let msg = Msg::from_tpl(&raw_msg)?; - smtp.send(&config, &msg)?; - backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?; + 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: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( tpl: TplOverride, attachments_paths: Vec<&str>, encrypt: bool, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { - let msg = Msg::default() + let msg = Email::default() .add_attachments(attachments_paths)? .encrypt(encrypt); - editor::edit_msg_with_editor(msg, tpl, config, printer, backend, smtp)?; + editor::edit_msg_with_editor(msg, tpl, config, printer, backend, sender)?; Ok(()) } diff --git a/src/domain/email/mod.rs b/src/domain/email/mod.rs new file mode 100644 index 0000000..b0b957b --- /dev/null +++ b/src/domain/email/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handlers; diff --git a/cli/src/msg/envelope.rs b/src/domain/envelope/envelope.rs similarity index 96% rename from cli/src/msg/envelope.rs rename to src/domain/envelope/envelope.rs index 2f96dd9..bd694d8 100644 --- a/cli/src/msg/envelope.rs +++ b/src/domain/envelope/envelope.rs @@ -1,4 +1,4 @@ -use himalaya_lib::msg::{Envelope, Flag}; +use himalaya_lib::{Envelope, Flag}; use crate::ui::{Cell, Row, Table}; diff --git a/cli/src/msg/envelopes.rs b/src/domain/envelope/envelopes.rs similarity index 77% rename from cli/src/msg/envelopes.rs rename to src/domain/envelope/envelopes.rs index 0a524a9..1bbaa31 100644 --- a/cli/src/msg/envelopes.rs +++ b/src/domain/envelope/envelopes.rs @@ -1,8 +1,8 @@ use anyhow::Result; -use himalaya_lib::msg::Envelopes; +use himalaya_lib::Envelopes; use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, + printer::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, }; diff --git a/src/domain/envelope/mod.rs b/src/domain/envelope/mod.rs new file mode 100644 index 0000000..a893d38 --- /dev/null +++ b/src/domain/envelope/mod.rs @@ -0,0 +1,5 @@ +pub mod envelope; +pub mod envelopes; + +pub use envelope::*; +pub use envelopes::*; diff --git a/cli/src/msg/flag_args.rs b/src/domain/flag/args.rs similarity index 95% rename from cli/src/msg/flag_args.rs rename to src/domain/flag/args.rs index 74261db..78abaf8 100644 --- a/cli/src/msg/flag_args.rs +++ b/src/domain/flag/args.rs @@ -7,7 +7,7 @@ use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; use log::{debug, info}; -use crate::msg::msg_args; +use crate::email; type SeqRange<'a> = &'a str; type Flags = String; @@ -89,21 +89,21 @@ pub fn subcmds<'a>() -> Vec> { SubCommand::with_name("add") .aliases(&["a"]) .about("Adds flags to a message") - .arg(msg_args::seq_range_arg()) + .arg(email::args::seq_range_arg()) .arg(flags_arg()), ) .subcommand( SubCommand::with_name("set") .aliases(&["s", "change", "c"]) .about("Replaces all message flags") - .arg(msg_args::seq_range_arg()) + .arg(email::args::seq_range_arg()) .arg(flags_arg()), ) .subcommand( SubCommand::with_name("remove") .aliases(&["rem", "rm", "r", "delete", "del", "d"]) .about("Removes flags from a message") - .arg(msg_args::seq_range_arg()) + .arg(email::args::seq_range_arg()) .arg(flags_arg()), )] } diff --git a/cli/src/msg/flag_handlers.rs b/src/domain/flag/handlers.rs similarity index 59% rename from cli/src/msg/flag_handlers.rs rename to src/domain/flag/handlers.rs index 686912e..8a7675d 100644 --- a/cli/src/msg/flag_handlers.rs +++ b/src/domain/flag/handlers.rs @@ -5,18 +5,18 @@ use anyhow::Result; use himalaya_lib::backend::Backend; -use crate::output::PrinterService; +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: PrinterService, B: Backend<'a> + ?Sized>( - seq_range: &'a str, - flags: &'a str, - mbox: &'a str, - printer: &'a mut P, - backend: Box<&'a mut B>, +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.add_flags(mbox, seq_range, flags)?; + backend.flags_add(mbox, seq_range, flags)?; printer.print_struct(format!( "Flag(s) {:?} successfully added to message(s) {:?}", flags, seq_range @@ -25,14 +25,14 @@ pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Removes flags from all messages matching the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>( - seq_range: &'a str, - flags: &'a str, - mbox: &'a str, - printer: &'a mut P, - backend: Box<&'a mut B>, +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.del_flags(mbox, seq_range, flags)?; + backend.flags_delete(mbox, seq_range, flags)?; printer.print_struct(format!( "Flag(s) {:?} successfully removed from message(s) {:?}", flags, seq_range @@ -41,14 +41,14 @@ pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>( /// Replaces flags of all messages matching the given sequence range. /// Flags are case-insensitive, and they do not need to be prefixed with `\`. -pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>( - seq_range: &'a str, - flags: &'a str, - mbox: &'a str, - printer: &'a mut P, - backend: Box<&'a mut B>, +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.set_flags(mbox, seq_range, flags)?; + backend.flags_set(mbox, seq_range, flags)?; printer.print_struct(format!( "Flag(s) {:?} successfully set for message(s) {:?}", flags, seq_range diff --git a/src/domain/flag/mod.rs b/src/domain/flag/mod.rs new file mode 100644 index 0000000..b0b957b --- /dev/null +++ b/src/domain/flag/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handlers; diff --git a/cli/src/mbox/mbox_args.rs b/src/domain/folder/args.rs similarity index 83% rename from cli/src/mbox/mbox_args.rs rename to src/domain/folder/args.rs index 19f1456..cf4a898 100644 --- a/cli/src/mbox/mbox_args.rs +++ b/src/domain/folder/args.rs @@ -7,7 +7,7 @@ use anyhow::Result; use clap; use log::{debug, info}; -use crate::ui::table_arg; +use crate::ui::table; type MaxTableWidth = Option; @@ -37,24 +37,26 @@ pub fn matches(m: &clap::ArgMatches) -> Result> { /// Contains mailbox subcommands. pub fn subcmds<'a>() -> Vec> { vec![clap::SubCommand::with_name("mailboxes") - .aliases(&["mailbox", "mboxes", "mbox", "mb", "m"]) - .about("Lists mailboxes") - .arg(table_arg::max_width())] + .aliases(&[ + "mailbox", "mboxes", "mbox", "mb", "m", "folders", "fold", "fo", + ]) + .about("Lists folders") + .arg(table::args::max_width())] } /// Defines the source mailbox argument. pub fn source_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("mbox-source") - .short("m") - .long("mailbox") - .help("Specifies the source mailbox") + clap::Arg::with_name("folder-source") + .short("f") + .long("folder") + .help("Specifies the folder source") .value_name("SOURCE") } /// Defines the target mailbox argument. pub fn target_arg<'a>() -> clap::Arg<'a, 'a> { - clap::Arg::with_name("mbox-target") - .help("Specifies the targeted mailbox") + clap::Arg::with_name("folder-target") + .help("Specifies the folder target") .value_name("TARGET") .required(true) } @@ -105,13 +107,13 @@ mod tests { } let app = get_matches_from![]; - assert_eq!(None, app.value_of("mbox-source")); + assert_eq!(None, app.value_of("folder-source")); let app = get_matches_from!["-m", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("mbox-source")); + assert_eq!(Some("SOURCE"), app.value_of("folder-source")); let app = get_matches_from!["--mailbox", "SOURCE"]; - assert_eq!(Some("SOURCE"), app.value_of("mbox-source")); + assert_eq!(Some("SOURCE"), app.value_of("folder-source")); } #[test] @@ -131,6 +133,6 @@ mod tests { ); let app = get_matches_from!["TARGET"]; - assert_eq!(Some("TARGET"), app.unwrap().value_of("mbox-target")); + assert_eq!(Some("TARGET"), app.unwrap().value_of("folder-target")); } } diff --git a/cli/src/mbox/mbox.rs b/src/domain/folder/folder.rs similarity index 89% rename from cli/src/mbox/mbox.rs rename to src/domain/folder/folder.rs index e98e743..ba0d931 100644 --- a/cli/src/mbox/mbox.rs +++ b/src/domain/folder/folder.rs @@ -1,8 +1,8 @@ -use himalaya_lib::mbox::Mbox; +use himalaya_lib::folder::Folder; use crate::ui::{Cell, Row, Table}; -impl Table for Mbox { +impl Table for Folder { fn head() -> Row { Row::new() .cell(Cell::new("DELIM").bold().underline().white()) diff --git a/cli/src/mbox/mboxes.rs b/src/domain/folder/folders.rs similarity index 68% rename from cli/src/mbox/mboxes.rs rename to src/domain/folder/folders.rs index 9a032ff..db34dad 100644 --- a/cli/src/mbox/mboxes.rs +++ b/src/domain/folder/folders.rs @@ -1,12 +1,12 @@ use anyhow::Result; -use himalaya_lib::mbox::Mboxes; +use himalaya_lib::folder::Folders; use crate::{ - output::{PrintTable, PrintTableOpts, WriteColor}, + printer::{PrintTable, PrintTableOpts, WriteColor}, ui::Table, }; -impl PrintTable for Mboxes { +impl PrintTable for Folders { fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> { writeln!(writer)?; Table::print(writer, self, opts)?; diff --git a/cli/src/mbox/mbox_handlers.rs b/src/domain/folder/handlers.rs similarity index 69% rename from cli/src/mbox/mbox_handlers.rs rename to src/domain/folder/handlers.rs index b5a9aa9..125d4bd 100644 --- a/cli/src/mbox/mbox_handlers.rs +++ b/src/domain/folder/handlers.rs @@ -3,26 +3,26 @@ //! This module gathers all mailbox actions triggered by the CLI. use anyhow::Result; -use himalaya_lib::{account::Account, backend::Backend}; +use himalaya_lib::{AccountConfig, Backend}; use log::{info, trace}; -use crate::output::{PrintTableOpts, PrinterService}; +use crate::printer::{PrintTableOpts, Printer}; /// Lists all mailboxes. -pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>( max_width: Option, - config: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, + backend: &mut B, ) -> Result<()> { info!("entering list mailbox handler"); - let mboxes = backend.get_mboxes()?; + let mboxes = backend.folder_list()?; trace!("mailboxes: {:?}", mboxes); printer.print_table( // TODO: remove Box Box::new(mboxes), PrintTableOpts { - format: &config.format, + format: &config.email_reading_format, max_width, }, ) @@ -30,15 +30,11 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>( #[cfg(test)] mod tests { - use himalaya_lib::{ - backend::{backend, Backend}, - mbox::{Mbox, Mboxes}, - msg::{Envelopes, Msg}, - }; + use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders}; use std::{fmt::Debug, io}; use termcolor::ColorSpec; - use crate::output::{Print, PrintTable, WriteColor}; + use crate::printer::{Print, PrintTable, WriteColor}; use super::*; @@ -83,7 +79,7 @@ mod tests { pub writer: StringWriter, } - impl PrinterService for PrinterServiceTest { + impl Printer for PrinterServiceTest { fn print_table( &mut self, data: Box, @@ -109,18 +105,18 @@ mod tests { struct TestBackend; impl<'a> Backend<'a> for TestBackend { - fn add_mbox(&mut self, _: &str) -> backend::Result<()> { + fn folder_add(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_mboxes(&mut self) -> backend::Result { - Ok(Mboxes { - mboxes: vec![ - Mbox { + fn folder_list(&mut self) -> backend::Result { + Ok(Folders { + folders: vec![ + Folder { delim: "/".into(), name: "INBOX".into(), desc: "desc".into(), }, - Mbox { + Folder { delim: "/".into(), name: "Sent".into(), desc: "desc".into(), @@ -128,13 +124,13 @@ mod tests { ], }) } - fn del_mbox(&mut self, _: &str) -> backend::Result<()> { + fn folder_delete(&mut self, _: &str) -> backend::Result<()> { unimplemented!(); } - fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result { + fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result { unimplemented!() } - fn search_envelopes( + fn envelope_search( &mut self, _: &str, _: &str, @@ -144,38 +140,40 @@ mod tests { ) -> backend::Result { unimplemented!() } - fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { + fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result { unimplemented!() } - fn get_msg(&mut self, _: &str, _: &str) -> backend::Result { + fn email_list(&mut self, _: &str, _: &str) -> backend::Result { unimplemented!() } - fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn email_get(&mut self, _: &str, _: &str) -> backend::Result { unimplemented!() } - fn move_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_msg(&mut self, _: &str, _: &str) -> backend::Result<()> { + fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn add_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn set_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } - fn del_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { + unimplemented!() + } + fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> { unimplemented!() } } - let config = Account::default(); + let account_config = AccountConfig::default(); let mut printer = PrinterServiceTest::default(); let mut backend = TestBackend {}; - let backend = Box::new(&mut backend); - assert!(list(None, &config, &mut printer, backend).is_ok()); + assert!(list(None, &account_config, &mut printer, &mut backend).is_ok()); assert_eq!( concat![ "\n", diff --git a/src/domain/folder/mod.rs b/src/domain/folder/mod.rs new file mode 100644 index 0000000..45a1338 --- /dev/null +++ b/src/domain/folder/mod.rs @@ -0,0 +1,8 @@ +pub mod folder; +pub use folder::*; + +pub mod folders; +pub use folders::*; + +pub mod args; +pub mod handlers; diff --git a/cli/src/imap/imap_args.rs b/src/domain/imap/args.rs similarity index 100% rename from cli/src/imap/imap_args.rs rename to src/domain/imap/args.rs diff --git a/cli/src/imap/imap_handlers.rs b/src/domain/imap/handlers.rs similarity index 91% rename from cli/src/imap/imap_handlers.rs rename to src/domain/imap/handlers.rs index f5ab439..fe05c8f 100644 --- a/cli/src/imap/imap_handlers.rs +++ b/src/domain/imap/handlers.rs @@ -3,7 +3,7 @@ //! This module gathers all IMAP handlers triggered by the CLI. use anyhow::{Context, Result}; -use himalaya_lib::backend::ImapBackend; +use himalaya_lib::ImapBackend; pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> { imap.notify(keepalive, mbox).context("cannot imap notify") diff --git a/src/domain/imap/mod.rs b/src/domain/imap/mod.rs new file mode 100644 index 0000000..b0b957b --- /dev/null +++ b/src/domain/imap/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handlers; diff --git a/src/domain/mod.rs b/src/domain/mod.rs new file mode 100644 index 0000000..34862e7 --- /dev/null +++ b/src/domain/mod.rs @@ -0,0 +1,17 @@ +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::*; diff --git a/cli/src/msg/tpl_args.rs b/src/domain/tpl/args.rs similarity index 95% rename from cli/src/msg/tpl_args.rs rename to src/domain/tpl/args.rs index 5436ad0..081ca3a 100644 --- a/cli/src/msg/tpl_args.rs +++ b/src/domain/tpl/args.rs @@ -4,10 +4,10 @@ use anyhow::Result; use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand}; -use himalaya_lib::msg::TplOverride; +use himalaya_lib::email::TplOverride; use log::{debug, info, trace}; -use crate::msg::msg_args; +use crate::email; type Seq<'a> = &'a str; type ReplyAll = bool; @@ -156,27 +156,27 @@ pub fn subcmds<'a>() -> Vec> { SubCommand::with_name("reply") .aliases(&["rep", "re", "r"]) .about("Generates a reply message template") - .arg(msg_args::seq_arg()) - .arg(msg_args::reply_all_arg()) + .arg(email::args::seq_arg()) + .arg(email::args::reply_all_arg()) .args(&tpl_args()), ) .subcommand( SubCommand::with_name("forward") .aliases(&["fwd", "fw", "f"]) .about("Generates a forward message template") - .arg(msg_args::seq_arg()) + .arg(email::args::seq_arg()) .args(&tpl_args()), ) .subcommand( SubCommand::with_name("save") .about("Saves a message based on the given template") - .arg(&msg_args::attachments_arg()) + .arg(&email::args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), ) .subcommand( SubCommand::with_name("send") .about("Sends a message based on the given template") - .arg(&msg_args::attachments_arg()) + .arg(&email::args::attachments_arg()) .arg(Arg::with_name("template").raw(true)), )] } diff --git a/cli/src/msg/tpl_handlers.rs b/src/domain/tpl/handlers.rs similarity index 59% rename from cli/src/msg/tpl_handlers.rs rename to src/domain/tpl/handlers.rs index 558e76a..0fcb078 100644 --- a/cli/src/msg/tpl_handlers.rs +++ b/src/domain/tpl/handlers.rs @@ -4,66 +4,62 @@ use anyhow::Result; use atty::Stream; -use himalaya_lib::{ - account::Account, - backend::Backend, - msg::{Msg, TplOverride}, -}; +use himalaya_lib::{AccountConfig, Backend, Email, Sender, TplOverride}; use std::io::{self, BufRead}; -use crate::{output::PrinterService, smtp::SmtpService}; +use crate::printer::Printer; /// Generate a new message template. -pub fn new<'a, P: PrinterService>( +pub fn new<'a, P: Printer>( opts: TplOverride<'a>, - account: &'a Account, + config: &'a AccountConfig, printer: &'a mut P, ) -> Result<()> { - let tpl = Msg::default().to_tpl(opts, account)?; + let tpl = Email::default().to_tpl(opts, config)?; printer.print_struct(tpl) } /// Generate a reply message template. -pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized>( +pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, all: bool, - opts: TplOverride<'a>, + opts: TplOverride<'_>, mbox: &str, - config: &'a Account, - printer: &'a mut P, - backend: Box<&'a mut B>, + config: &AccountConfig, + printer: &mut P, + backend: &mut B, ) -> Result<()> { let tpl = backend - .get_msg(mbox, seq)? + .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: PrinterService, B: Backend<'a> + ?Sized>( +pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized>( seq: &str, - opts: TplOverride<'a>, + opts: TplOverride<'_>, mbox: &str, - config: &'a Account, - printer: &'a mut P, - backend: Box<&'a mut B>, + config: &AccountConfig, + printer: &mut P, + backend: &mut B, ) -> Result<()> { let tpl = backend - .get_msg(mbox, seq)? + .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: PrinterService, B: Backend<'a> + ?Sized>( +pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>( mbox: &str, - config: &Account, + config: &AccountConfig, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, - backend: Box<&mut B>, + backend: &mut B, ) -> Result<()> { let tpl = if atty::is(Stream::Stdin) || printer.is_json() { tpl.replace("\r", "") @@ -75,21 +71,21 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>( .collect::>() .join("\n") }; - let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; + let msg = Email::from_tpl(&tpl)?.add_attachments(attachments_paths)?; let raw_msg = msg.into_sendable_msg(config)?.formatted(); - backend.add_msg(mbox, &raw_msg, "seen")?; + 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: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( +pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( mbox: &str, - account: &Account, + account: &AccountConfig, attachments_paths: Vec<&str>, tpl: &str, printer: &mut P, - backend: Box<&mut B>, - smtp: &mut S, + backend: &mut B, + sender: &mut S, ) -> Result<()> { let tpl = if atty::is(Stream::Stdin) || printer.is_json() { tpl.replace("\r", "") @@ -101,8 +97,8 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( .collect::>() .join("\n") }; - let msg = Msg::from_tpl(&tpl)?.add_attachments(attachments_paths)?; - let sent_msg = smtp.send(account, &msg)?; - backend.add_msg(mbox, &sent_msg, "seen")?; + 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") } diff --git a/src/domain/tpl/mod.rs b/src/domain/tpl/mod.rs new file mode 100644 index 0000000..b0b957b --- /dev/null +++ b/src/domain/tpl/mod.rs @@ -0,0 +1,2 @@ +pub mod args; +pub mod handlers; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a840ea1 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,8 @@ +pub mod compl; +pub mod config; +pub mod domain; +pub mod output; +pub mod printer; +pub mod ui; + +pub use domain::*; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e94abef --- /dev/null +++ b/src/main.rs @@ -0,0 +1,320 @@ +use anyhow::{Context, Result}; +use std::env; +use url::Url; + +use himalaya::{ + account, compl, + config::{self, DeserializedConfig}, + email, flag, folder, + output::{self, OutputFmt}, + printer::StdoutPrinter, + tpl, +}; +use himalaya_lib::{BackendBuilder, BackendConfig, ImapBackend, SenderBuilder}; + +#[cfg(feature = "imap-backend")] +use himalaya::imap; + +fn create_app<'a>() -> clap::App<'a, 'a> { + let app = clap::App::new(env!("CARGO_PKG_NAME")) + .version(env!("CARGO_PKG_VERSION")) + .about(env!("CARGO_PKG_DESCRIPTION")) + .author(env!("CARGO_PKG_AUTHORS")) + .global_setting(clap::AppSettings::GlobalVersion) + .arg(&config::args::path_arg()) + .arg(&account::args::name_arg()) + .args(&output::args::args()) + .arg(folder::args::source_arg()) + .subcommands(compl::args::subcmds()) + .subcommands(account::args::subcmds()) + .subcommands(folder::args::subcmds()) + .subcommands(email::args::subcmds()); + + #[cfg(feature = "imap-backend")] + let app = app.subcommands(imap::args::subcmds()); + + app +} + +#[allow(clippy::single_match)] +fn main() -> Result<()> { + let default_env_filter = env_logger::DEFAULT_FILTER_ENV; + env_logger::init_from_env(env_logger::Env::default().filter_or(default_env_filter, "off")); + + // Check mailto command BEFORE app initialization. + let raw_args: Vec = env::args().collect(); + if raw_args.len() > 1 && raw_args[1].starts_with("mailto:") { + let url = Url::parse(&raw_args[1])?; + let config = DeserializedConfig::from_opt_path(None)?; + let (account_config, backend_config) = config.to_configs(None)?; + let mut backend = BackendBuilder::build(&account_config, &backend_config)?; + let mut sender = SenderBuilder::build(&account_config)?; + let mut printer = StdoutPrinter::from_fmt(OutputFmt::Plain); + + return email::handlers::mailto( + &url, + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + + let app = create_app(); + let m = app.get_matches(); + + // Check completion command BEFORE entities and services initialization. + // Related issue: https://github.com/soywod/himalaya/issues/115. + match compl::args::matches(&m)? { + Some(compl::args::Command::Generate(shell)) => { + return compl::handlers::generate(create_app(), shell); + } + _ => (), + } + + // Init entities and services. + let config = DeserializedConfig::from_opt_path(m.value_of("config"))?; + let (account_config, backend_config) = config.to_configs(m.value_of("account"))?; + let default_folder = account_config.folder_alias("inbox")?; + let folder = m.value_of("folder-source").unwrap_or(&default_folder); + + // Check IMAP commands. + #[cfg(feature = "imap-backend")] + if let BackendConfig::Imap(imap_config) = backend_config { + // FIXME: find a way to downcast `backend` instead. + let mut imap = ImapBackend::new(&account_config, imap_config); + match imap::args::matches(&m)? { + Some(imap::args::Command::Notify(keepalive)) => { + return imap::handlers::notify(keepalive, folder, &mut imap); + } + Some(imap::args::Command::Watch(keepalive)) => { + return imap::handlers::watch(keepalive, folder, &mut imap); + } + _ => (), + } + } + + let mut backend = BackendBuilder::build(&account_config, &backend_config)?; + let mut sender = SenderBuilder::build(&account_config)?; + let mut printer = StdoutPrinter::from_opt_str(m.value_of("output"))?; + + // Check account commands. + match account::args::matches(&m)? { + Some(account::args::Cmd::List(max_width)) => { + return account::handlers::list(max_width, &account_config, &config, &mut printer); + } + _ => (), + } + + // Check mailbox commands. + match folder::args::matches(&m)? { + Some(folder::args::Cmd::List(max_width)) => { + return folder::handlers::list( + max_width, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + _ => (), + } + + // Check message commands. + match email::args::matches(&m)? { + Some(email::args::Cmd::Attachments(seq)) => { + return email::handlers::attachments( + seq, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(email::args::Cmd::Copy(seq, mbox_dst)) => { + return email::handlers::copy(seq, folder, mbox_dst, &mut printer, backend.as_mut()); + } + Some(email::args::Cmd::Delete(seq)) => { + return email::handlers::delete(seq, folder, &mut printer, backend.as_mut()); + } + Some(email::args::Cmd::Forward(seq, attachment_paths, encrypt)) => { + return email::handlers::forward( + seq, + attachment_paths, + encrypt, + folder, + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + Some(email::args::Cmd::List(max_width, page_size, page)) => { + return email::handlers::list( + max_width, + page_size, + page, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(email::args::Cmd::Move(seq, mbox_dst)) => { + return email::handlers::move_(seq, folder, mbox_dst, &mut printer, backend.as_mut()); + } + Some(email::args::Cmd::Read(seq, text_mime, raw, headers)) => { + return email::handlers::read( + seq, + text_mime, + raw, + headers, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(email::args::Cmd::Reply(seq, all, attachment_paths, encrypt)) => { + return email::handlers::reply( + seq, + all, + attachment_paths, + encrypt, + folder, + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + Some(email::args::Cmd::Save(raw_msg)) => { + return email::handlers::save(folder, raw_msg, &mut printer, backend.as_mut()); + } + Some(email::args::Cmd::Search(query, max_width, page_size, page)) => { + return email::handlers::search( + query, + max_width, + page_size, + page, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(email::args::Cmd::Sort(criteria, query, max_width, page_size, page)) => { + return email::handlers::sort( + criteria, + query, + max_width, + page_size, + page, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(email::args::Cmd::Send(raw_msg)) => { + return email::handlers::send( + raw_msg, + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + Some(email::args::Cmd::Write(tpl, atts, encrypt)) => { + return email::handlers::write( + tpl, + atts, + encrypt, + &account_config, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + Some(email::args::Cmd::Flag(m)) => match m { + Some(flag::args::Cmd::Set(seq_range, ref flags)) => { + return flag::handlers::set( + seq_range, + flags, + folder, + &mut printer, + backend.as_mut(), + ); + } + Some(flag::args::Cmd::Add(seq_range, ref flags)) => { + return flag::handlers::add( + seq_range, + flags, + folder, + &mut printer, + backend.as_mut(), + ); + } + Some(flag::args::Cmd::Remove(seq_range, ref flags)) => { + return flag::handlers::remove( + seq_range, + flags, + folder, + &mut printer, + backend.as_mut(), + ); + } + _ => (), + }, + Some(email::args::Cmd::Tpl(m)) => match m { + Some(tpl::args::Cmd::New(tpl)) => { + return tpl::handlers::new(tpl, &account_config, &mut printer); + } + Some(tpl::args::Cmd::Reply(seq, all, tpl)) => { + return tpl::handlers::reply( + seq, + all, + tpl, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(tpl::args::Cmd::Forward(seq, tpl)) => { + return tpl::handlers::forward( + seq, + tpl, + folder, + &account_config, + &mut printer, + backend.as_mut(), + ); + } + Some(tpl::args::Cmd::Save(atts, tpl)) => { + return tpl::handlers::save( + folder, + &account_config, + atts, + tpl, + &mut printer, + backend.as_mut(), + ); + } + Some(tpl::args::Cmd::Send(atts, tpl)) => { + return tpl::handlers::send( + folder, + &account_config, + atts, + tpl, + &mut printer, + backend.as_mut(), + sender.as_mut(), + ); + } + _ => (), + }, + _ => (), + } + + backend.as_mut().disconnect().context("cannot disconnect") +} diff --git a/cli/src/output/output_args.rs b/src/output/args.rs similarity index 100% rename from cli/src/output/output_args.rs rename to src/output/args.rs diff --git a/src/output/mod.rs b/src/output/mod.rs new file mode 100644 index 0000000..7cc1c9a --- /dev/null +++ b/src/output/mod.rs @@ -0,0 +1,4 @@ +pub mod args; +pub mod output; + +pub use output::*; diff --git a/cli/src/output/output_entity.rs b/src/output/output.rs similarity index 100% rename from cli/src/output/output_entity.rs rename to src/output/output.rs diff --git a/src/printer/mod.rs b/src/printer/mod.rs new file mode 100644 index 0000000..3f35c35 --- /dev/null +++ b/src/printer/mod.rs @@ -0,0 +1,7 @@ +pub mod print; +pub mod print_table; +pub mod printer; + +pub use print::*; +pub use print_table::*; +pub use printer::*; diff --git a/cli/src/output/print.rs b/src/printer/print.rs similarity index 93% rename from cli/src/output/print.rs rename to src/printer/print.rs index 8faf0a4..54bbde7 100644 --- a/cli/src/output/print.rs +++ b/src/printer/print.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; -use crate::output::WriteColor; +use crate::printer::WriteColor; pub trait Print { fn print(&self, writer: &mut dyn WriteColor) -> Result<()>; diff --git a/cli/src/output/print_table.rs b/src/printer/print_table.rs similarity index 81% rename from cli/src/output/print_table.rs rename to src/printer/print_table.rs index a99e316..bf6d75f 100644 --- a/cli/src/output/print_table.rs +++ b/src/printer/print_table.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use himalaya_lib::account::TextPlainFormat; +use himalaya_lib::EmailTextPlainFormat; use std::io; use termcolor::{self, StandardStream}; @@ -12,6 +12,6 @@ pub trait PrintTable { } pub struct PrintTableOpts<'a> { - pub format: &'a TextPlainFormat, + pub format: &'a EmailTextPlainFormat, pub max_width: Option, } diff --git a/cli/src/output/printer_service.rs b/src/printer/printer.rs similarity index 82% rename from cli/src/output/printer_service.rs rename to src/printer/printer.rs index cc4685a..d52c575 100644 --- a/cli/src/output/printer_service.rs +++ b/src/printer/printer.rs @@ -1,14 +1,14 @@ -use anyhow::{Context, Error, Result}; +use anyhow::{Context, Result}; use atty::Stream; -use std::{ - convert::TryFrom, - fmt::{self, Debug}, -}; +use std::fmt::{self, Debug}; use termcolor::{ColorChoice, StandardStream}; -use crate::output::{OutputFmt, OutputJson, Print, PrintTable, PrintTableOpts, WriteColor}; +use crate::{ + output::{OutputFmt, OutputJson}, + printer::{Print, PrintTable, PrintTableOpts, WriteColor}, +}; -pub trait PrinterService { +pub trait Printer { fn print_str(&mut self, data: T) -> Result<()>; fn print_struct(&mut self, data: T) -> Result<()>; fn print_table( @@ -24,7 +24,33 @@ pub struct StdoutPrinter { pub fmt: OutputFmt, } -impl PrinterService for StdoutPrinter { +impl StdoutPrinter { + pub fn from_fmt(fmt: OutputFmt) -> Self { + let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { + // Colors should be deactivated if the terminal is not a tty. + ColorChoice::Never + } else { + // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]: + // - If `NO_COLOR` is set to any value, then colors will be suppressed. + // - If `TERM` is set to dumb, then colors will be suppressed. + // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed. + // + // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection + ColorChoice::Auto + }); + let writer = Box::new(writer); + Self { writer, fmt } + } + + pub fn from_opt_str(s: Option<&str>) -> Result { + Ok(Self { + fmt: OutputFmt::try_from(s)?, + ..Self::from_fmt(OutputFmt::Plain) + }) + } +} + +impl Printer for StdoutPrinter { fn print_str(&mut self, data: T) -> Result<()> { match self.fmt { OutputFmt::Plain => data.print(self.writer.as_mut()), @@ -60,33 +86,3 @@ impl PrinterService for StdoutPrinter { self.fmt == OutputFmt::Json } } - -impl From for StdoutPrinter { - fn from(fmt: OutputFmt) -> Self { - let writer = StandardStream::stdout(if atty::isnt(Stream::Stdin) { - // Colors should be deactivated if the terminal is not a tty. - ColorChoice::Never - } else { - // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]: - // - If `NO_COLOR` is set to any value, then colors will be suppressed. - // - If `TERM` is set to dumb, then colors will be suppressed. - // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed. - // - // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection - ColorChoice::Auto - }); - let writer = Box::new(writer); - Self { writer, fmt } - } -} - -impl TryFrom> for StdoutPrinter { - type Error = Error; - - fn try_from(fmt: Option<&str>) -> Result { - Ok(Self { - fmt: OutputFmt::try_from(fmt)?, - ..Self::from(OutputFmt::Plain) - }) - } -} diff --git a/cli/src/ui/choice.rs b/src/ui/choice.rs similarity index 100% rename from cli/src/ui/choice.rs rename to src/ui/choice.rs diff --git a/cli/src/ui/editor.rs b/src/ui/editor.rs similarity index 66% rename from cli/src/ui/editor.rs rename to src/ui/editor.rs index 613f6c5..0557028 100644 --- a/cli/src/ui/editor.rs +++ b/src/ui/editor.rs @@ -1,15 +1,13 @@ use anyhow::{Context, Result}; use himalaya_lib::{ - account::{Account, DEFAULT_DRAFT_FOLDER, DEFAULT_SENT_FOLDER}, - backend::Backend, - msg::{local_draft_path, remove_local_draft, Msg, TplOverride}, + email::{local_draft_path, remove_local_draft, Email, TplOverride}, + AccountConfig, Backend, Sender, }; use log::{debug, info}; use std::{env, fs, process::Command}; use crate::{ - output::PrinterService, - smtp::SmtpService, + printer::Printer, ui::choice::{self, PostEditChoice, PreEditChoice}, }; @@ -39,20 +37,20 @@ pub fn open_with_draft() -> Result { open_with_tpl(tpl) } -fn _edit_msg_with_editor(msg: &Msg, tpl: TplOverride, account: &Account) -> Result { - let tpl = msg.to_tpl(tpl, account)?; +fn _edit_msg_with_editor(msg: &Email, tpl: TplOverride, config: &AccountConfig) -> Result { + let tpl = msg.to_tpl(tpl, config)?; let tpl = open_with_tpl(tpl)?; - Msg::from_tpl(&tpl).context("cannot parse message from template") + Email::from_tpl(&tpl).context("cannot parse message from template") } -pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>( - mut msg: Msg, +pub fn edit_msg_with_editor<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>( + mut msg: Email, tpl: TplOverride, - account: &Account, + config: &AccountConfig, printer: &mut P, - backend: Box<&'a mut B>, - smtp: &mut S, -) -> Result> { + backend: &mut B, + sender: &mut S, +) -> Result<()> { info!("start editing with editor"); let draft = local_draft_path(); @@ -62,14 +60,14 @@ pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: S Ok(choice) => match choice { PreEditChoice::Edit => { let tpl = open_with_draft()?; - msg.merge_with(Msg::from_tpl(&tpl)?); + msg.merge_with(Email::from_tpl(&tpl)?); break; } PreEditChoice::Discard => { - msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), config)?); break; } - PreEditChoice::Quit => return Ok(backend), + PreEditChoice::Quit => return Ok(()), }, Err(err) => { println!("{}", err); @@ -78,27 +76,23 @@ pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: S } } } else { - msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), config)?); } loop { match choice::post_edit() { Ok(PostEditChoice::Send) => { printer.print_str("Sending message…")?; - let sent_msg = smtp.send(account, &msg)?; - let sent_folder = account - .mailboxes - .get("sent") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_SENT_FOLDER); + let sent_msg: Vec = sender.send(config, &msg)?; + let sent_folder = config.folder_alias("sent")?; printer.print_str(format!("Adding message to the {:?} folder…", sent_folder))?; - backend.add_msg(&sent_folder, &sent_msg, "seen")?; + backend.email_add(&sent_folder, &sent_msg, "seen")?; remove_local_draft()?; printer.print_struct("Done!")?; break; } Ok(PostEditChoice::Edit) => { - msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), account)?); + msg.merge_with(_edit_msg_with_editor(&msg, tpl.clone(), config)?); continue; } Ok(PostEditChoice::LocalDraft) => { @@ -106,13 +100,9 @@ pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: S break; } Ok(PostEditChoice::RemoteDraft) => { - let tpl = msg.to_tpl(TplOverride::default(), account)?; - let draft_folder = account - .mailboxes - .get("draft") - .map(|s| s.as_str()) - .unwrap_or(DEFAULT_DRAFT_FOLDER); - backend.add_msg(&draft_folder, tpl.as_bytes(), "seen draft")?; + let tpl = msg.to_tpl(TplOverride::default(), config)?; + let draft_folder = config.folder_alias("draft")?; + backend.email_add(&draft_folder, tpl.as_bytes(), "seen draft")?; remove_local_draft()?; printer.print_struct(format!("Message successfully saved to {}", draft_folder))?; break; @@ -128,5 +118,5 @@ pub fn edit_msg_with_editor<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: S } } - Ok(backend) + Ok(()) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..565c3f5 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,5 @@ +pub mod choice; +pub mod editor; +pub mod table; + +pub use self::table::*; diff --git a/cli/src/ui/table_arg.rs b/src/ui/table/args.rs similarity index 100% rename from cli/src/ui/table_arg.rs rename to src/ui/table/args.rs diff --git a/src/ui/table/mod.rs b/src/ui/table/mod.rs new file mode 100644 index 0000000..d9e4ee3 --- /dev/null +++ b/src/ui/table/mod.rs @@ -0,0 +1,4 @@ +pub mod args; +pub mod table; + +pub use table::*; diff --git a/cli/src/ui/table.rs b/src/ui/table/table.rs similarity index 96% rename from cli/src/ui/table.rs rename to src/ui/table/table.rs index 82fef6f..9090e92 100644 --- a/cli/src/ui/table.rs +++ b/src/ui/table/table.rs @@ -5,13 +5,13 @@ //! [builder design pattern]: https://refactoring.guru/design-patterns/builder use anyhow::{Context, Result}; -use himalaya_lib::account::TextPlainFormat; +use himalaya_lib::EmailTextPlainFormat; use log::trace; use termcolor::{Color, ColorSpec}; use terminal_size; use unicode_width::UnicodeWidthStr; -use crate::output::{Print, PrintTableOpts, WriteColor}; +use crate::printer::{Print, PrintTableOpts, WriteColor}; /// Defines the default terminal size. /// This is used when the size cannot be determined by the `terminal_size` crate. @@ -169,11 +169,11 @@ where /// Writes the table to the writer. fn print(writer: &mut dyn WriteColor, items: &[Self], opts: PrintTableOpts) -> Result<()> { - let is_format_flowed = matches!(opts.format, TextPlainFormat::Flowed); + let is_format_flowed = matches!(opts.format, EmailTextPlainFormat::Flowed); let max_width = match opts.format { - TextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width), - TextPlainFormat::Flowed => 0, - TextPlainFormat::Auto => opts + EmailTextPlainFormat::Fixed(width) => opts.max_width.unwrap_or(*width), + EmailTextPlainFormat::Flowed => 0, + EmailTextPlainFormat::Auto => opts .max_width .or_else(|| terminal_size::terminal_size().map(|(w, _)| w.0 as usize)) .unwrap_or(DEFAULT_TERM_WIDTH), @@ -339,7 +339,7 @@ mod tests { macro_rules! write_items { ($writer:expr, $($item:expr),*) => { - Table::print($writer, &[$($item,)*], PrintTableOpts { format: &Format::Auto, max_width: Some(20) }).unwrap(); + Table::print($writer, &[$($item,)*], PrintTableOpts { format: &EmailTextPlainFormat::Auto, max_width: Some(20) }).unwrap(); }; } diff --git a/vim/README.md b/vim/README.md deleted file mode 100644 index 9275a74..0000000 --- a/vim/README.md +++ /dev/null @@ -1,175 +0,0 @@ -# Vim plugin - -## Installation - -First you need to install and configure the [himalaya -CLI](https://github.com/soywod/himalaya#installation). Then you can -install this plugin with your favorite plugin manager. For example -with [vim-plug](https://github.com/junegunn/vim-plug), add to your -`.vimrc`: - -```viml -Plug 'soywod/himalaya', {'rtp': 'vim'} -``` - -Then: - -```viml -:PlugInstall -``` - -It is highly recommanded to have this option on: - -```viml -set hidden -``` - -## Configuration - -### Mailbox picker provider - -```vim -let g:himalaya_mailbox_picker = 'native' | 'fzf' | 'telescope' -``` - -Defines the provider used for picking mailboxes (default keybind: -`gm`): - -- `native`: a vim native input -- `fzf`: https://github.com/junegunn/fzf.vim -- `telescope`: https://github.com/nvim-telescope/telescope.nvim - -If no value given, the first loaded (and available) provider will be -used (fzf > telescope > native). - -### Telescope preview - -```vim -let g:himalaya_telescope_preview_enabled = 0 -``` - -Should enable telescope preview when picking a mailbox with the telescope -provider. - -### Contact completion - -```vim -let g:himalaya_complete_contact_cmd = '' -``` - -Define the command to use for contact completion. When this is set, -`completefunc` will be set when composing messages so that contacts can be -completed with ``. - -The command must print each possible result on its own line. Each line must -contain tab-separated fields; the first must be the email address, and the -second, if present, must be the name. `%s` in the command will be replaced -with the search query. - -For example, to complete contacts with khard, you could use -`khard email --remove-first-line --parsable '%s'` as the completion command. - -## Usage - -### List messages view - -```vim -:Himalaya -``` - -![gif](https://user-images.githubusercontent.com/10437171/110707014-f9ef1580-81f8-11eb-93ad-233010733ca3.gif) - -| Function | Default binding | -| --- | --- | -| Change the current mbox | `gm` | -| Show previous page | `gp` | -| Show next page | `gn` | -| Read focused msg | `` | -| Write a new msg | `gw` | -| Reply to the focused msg | `gr` | -| Reply all to the focused msg | `gR` | -| Forward the focused message | `gf` | -| Download attachments from focused message | `ga` | -| Copy the focused message | `gC` | -| Move the focused message | `gM` | -| Delete the focused message(s) | `gD` | - -They can be customized: - -```vim -nmap gm (himalaya-mbox-input) -nmap gp (himalaya-mbox-prev-page) -nmap gn (himalaya-mbox-next-page) -nmap (himalaya-msg-read) -nmap gw (himalaya-msg-write) -nmap gr (himalaya-msg-reply) -nmap gR (himalaya-msg-reply-all) -nmap gf (himalaya-msg-forward) -nmap ga (himalaya-msg-attachments) -nmap gC (himalaya-msg-copy) -nmap gM (himalaya-msg-move) -nmap gD (himalaya-msg-delete) -``` - -### List mailboxes - -Default behaviour (basic prompt): - -![screenshot](https://user-images.githubusercontent.com/10437171/113631817-51eb3180-966a-11eb-8b13-cd1f1f2539ab.jpeg) - -With [telescope](https://github.com/nvim-telescope/telescope.nvim) support: - -![screenshot](https://user-images.githubusercontent.com/10437171/113631294-86122280-9669-11eb-8074-1c43c36b65a9.jpeg) - -With [fzf](https://github.com/junegunn/fzf) support: - -![screenshot](https://user-images.githubusercontent.com/10437171/113631382-acd05900-9669-11eb-817d-c28fd5d9574c.jpeg) - -### Read message view - -![gif](https://user-images.githubusercontent.com/10437171/110708073-7b937300-81fa-11eb-9f4c-5472cea22e21.gif) - -| Function | Default binding | -| --- | --- | -| Write a new msg | `gw` | -| Reply to the msg | `gr` | -| Reply all to the msg | `gR` | -| Forward the message | `gf` | -| Download all msg attachments | `ga` | -| Copy the message | `gC` | -| Move the message | `gM` | -| Delete the message | `gD` | - -They can be customized: - -```vim -nmap gw (himalaya-msg-write) -nmap gr (himalaya-msg-reply) -nmap gR (himalaya-msg-reply-all) -nmap gf (himalaya-msg-forward) -nmap ga (himalaya-msg-attachments) -nmap gC (himalaya-msg-copy) -nmap gM (himalaya-msg-move) -nmap gD (himalaya-msg-delete) -``` - -### Write message view - -![gif](https://user-images.githubusercontent.com/10437171/110708795-84387900-81fb-11eb-8f8a-f7e7862e816d.gif) - -| Function | Default binding | -| --- | --- | -| Add attachment | `ga` | - -They can be customized: - -```vim -nmap ga (himalaya-msg-add-attachment) -``` - -When you exit this special buffer, you will be prompted 4 choices: - -- `Send`: sends the message -- `Draft`: saves the message into the `Drafts` mailbox -- `Quit`: quits the buffer without saving -- `Cancel`: goes back to the message edition diff --git a/vim/autoload/himalaya/account.vim b/vim/autoload/himalaya/account.vim deleted file mode 100644 index 27c8f9e..0000000 --- a/vim/autoload/himalaya/account.vim +++ /dev/null @@ -1,11 +0,0 @@ -" Account - -let s:curr_account = "" - -function! himalaya#account#curr() - return s:curr_account -endfunction - -function! himalaya#account#set(account) - let s:curr_account = a:account -endfunction diff --git a/vim/autoload/himalaya/mbox.vim b/vim/autoload/himalaya/mbox.vim deleted file mode 100644 index e1b76d2..0000000 --- a/vim/autoload/himalaya/mbox.vim +++ /dev/null @@ -1,84 +0,0 @@ -let s:dir = expand(":h") -let s:cli = function("himalaya#shared#cli#call") - -" Pickers - -function! s:telescope_picker(cb, mboxes) - call luaeval("require('himalaya.mbox').mbox_picker")(a:cb, a:mboxes) -endfunction - -function! s:fzf_picker(cb, mboxes) - call fzf#run({ - \"source": a:mboxes, - \"sink": function(a:cb), - \"down": "25%", - \}) -endfunction - -function! s:native_picker(cb, mboxes) - let choice = map(copy(a:mboxes), "printf('%s (%d)', v:val, v:key)") - let choice = input(join(choice, ", ") . ": ") - redraw | echo - call function(a:cb)(a:mboxes[choice]) -endfunction - -" Pagination - -let s:curr_page = 1 - -function! himalaya#mbox#curr_page() - return s:curr_page -endfunction - -function! himalaya#mbox#prev_page() - let s:curr_page = max([1, s:curr_page - 1]) - call himalaya#msg#list() -endfunction - -function! himalaya#mbox#next_page() - let s:curr_page = s:curr_page + 1 - call himalaya#msg#list() -endfunction - -" Mailbox - -let s:curr_mbox = "INBOX" - -function! himalaya#mbox#curr_mbox() - return s:curr_mbox -endfunction - -function! himalaya#mbox#pick(cb) - try - let account = himalaya#account#curr() - let mboxes = map(s:cli("--account %s mailboxes", [shellescape(account)], "Fetching mailboxes", 0), "v:val.name") - - if exists("g:himalaya_mailbox_picker") - let picker = g:himalaya_mailbox_picker - else - if &rtp =~ "telescope" - let picker = "telescope" - elseif &rtp =~ "fzf" - let picker = "fzf" - else - let picker = "native" - endif - endif - - execute printf("call s:%s_picker(a:cb, mboxes)", picker) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#mbox#change() - call himalaya#mbox#pick("himalaya#mbox#_change") -endfunction - -function! himalaya#mbox#_change(mbox) - let s:curr_mbox = a:mbox - let s:curr_page = 1 - call himalaya#msg#list() -endfunction diff --git a/vim/autoload/himalaya/msg.vim b/vim/autoload/himalaya/msg.vim deleted file mode 100644 index e9e03b8..0000000 --- a/vim/autoload/himalaya/msg.vim +++ /dev/null @@ -1,415 +0,0 @@ -let s:log = function("himalaya#shared#log#info") -let s:trim = function("himalaya#shared#utils#trim") -let s:cli = function("himalaya#shared#cli#call") -let s:plain_req = function("himalaya#request#plain") - -let s:msg_id = "" -let s:draft = "" -let s:attachment_paths = [] - -function! himalaya#msg#list_with(account, mbox, page, should_throw) - let pos = getpos(".") - let msgs = s:plain_req({ - \'cmd': '--account %s --mailbox %s list --max-width %d --page %d', - \'args': [shellescape(a:account), shellescape(a:mbox), s:bufwidth(), a:page], - \'msg': printf("Fetching %s messages", a:mbox), - \'should_throw': a:should_throw, - \}) - let buftype = stridx(bufname("%"), "Himalaya messages") == 0 ? "file" : "edit" - execute printf("silent! %s Himalaya messages [%s] [page %d]", buftype, a:mbox, a:page) - setlocal modifiable - silent execute "%d" - call append(0, split(msgs, '\n')) - silent execute "$d" - setlocal filetype=himalaya-msg-list - let &modified = 0 - execute 0 - call setpos('.', pos) -endfunction - -function! himalaya#msg#list(...) - try - if a:0 > 0 - call himalaya#account#set(a:1) - endif - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let page = himalaya#mbox#curr_page() - call himalaya#msg#list_with(account, mbox, page, 1) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#read() - try - let pos = getpos(".") - let s:msg_id = s:get_focused_msg_id() - if empty(s:msg_id) || s:msg_id == "HASH" | return | endif - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg = s:cli( - \"--account %s --mailbox %s read %s", - \[shellescape(account), shellescape(mbox), s:msg_id], - \printf("Fetching message %s", s:msg_id), - \1, - \) - call s:close_open_buffers('Himalaya read message') - execute printf("silent! botright new Himalaya read message [%s]", s:msg_id) - setlocal modifiable - silent execute "%d" - call append(0, split(substitute(msg, "\r", "", "g"), "\n")) - silent execute "$d" - setlocal filetype=himalaya-msg-read - let &modified = 0 - execute 0 - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#write() - try - let pos = getpos(".") - let account = himalaya#account#curr() - let msg = s:cli("--account %s template new", [shellescape(account)], "Fetching new template", 0) - silent! edit Himalaya write - call append(0, split(substitute(msg, "\r", "", "g"), "\n")) - silent execute "$d" - setlocal filetype=himalaya-msg-write - let &modified = 0 - execute 0 - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#reply() - try - let pos = getpos(".") - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let msg = s:cli( - \"--account %s --mailbox %s template reply %s", - \[shellescape(account), shellescape(mbox), msg_id], - \"Fetching reply template", - \0, - \) - execute printf("silent! edit Himalaya reply [%s]", msg_id) - call append(0, split(substitute(msg, "\r", "", "g"), "\n")) - silent execute "$d" - setlocal filetype=himalaya-msg-write - let &modified = 0 - execute 0 - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#reply_all() - try - let pos = getpos(".") - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let msg = s:cli( - \"--account %s --mailbox %s template reply %s --all", - \[shellescape(account), shellescape(mbox), msg_id], - \"Fetching reply all template", - \0 - \) - execute printf("silent! edit Himalaya reply all [%s]", msg_id) - call append(0, split(substitute(msg, "\r", "", "g"), "\n")) - silent execute "$d" - setlocal filetype=himalaya-msg-write - let &modified = 0 - execute 0 - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#forward() - try - let pos = getpos(".") - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let msg = s:cli( - \"--account %s --mailbox %s template forward %s", - \[shellescape(account), shellescape(mbox), msg_id], - \"Fetching forward template", - \0 - \) - execute printf("silent! edit Himalaya forward [%s]", msg_id) - call append(0, split(substitute(msg, "\r", "", "g"), "\n")) - silent execute "$d" - setlocal filetype=himalaya-msg-write - let &modified = 0 - execute 0 - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#copy() - call himalaya#mbox#pick("himalaya#msg#_copy") -endfunction - -function! himalaya#msg#_copy(target_mbox) - try - let pos = getpos(".") - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let account = himalaya#account#curr() - let source_mbox = himalaya#mbox#curr_mbox() - let msg = s:cli( - \"--account %s --mailbox %s copy %s %s", - \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], - \"Copying message", - \1, - \) - call himalaya#msg#list_with(account, source_mbox, himalaya#mbox#curr_page(), 1) - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#move() - call himalaya#mbox#pick("himalaya#msg#_move") -endfunction - -function! himalaya#msg#_move(target_mbox) - try - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let choice = input(printf("Are you sure you want to move the message %s? (y/N) ", msg_id)) - redraw | echo - if choice != "y" | return | endif - let pos = getpos(".") - let account = himalaya#account#curr() - let source_mbox = himalaya#mbox#curr_mbox() - let msg = s:cli( - \"--account %s --mailbox %s move %s %s", - \[shellescape(account), shellescape(source_mbox), msg_id, shellescape(a:target_mbox)], - \"Moving message", - \1, - \) - call himalaya#msg#list_with(account, source_mbox, himalaya#mbox#curr_page(), 1) - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#delete() range - try - let msg_ids = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_ids(a:firstline, a:lastline) : s:msg_id - let choice = input(printf("Are you sure you want to delete message(s) %s? (y/N) ", msg_ids)) - redraw | echo - if choice != "y" | return | endif - let pos = getpos(".") - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg = s:cli( - \"--account %s --mailbox %s delete %s", - \[shellescape(account), shellescape(mbox), msg_ids], - \"Deleting message(s)", - \1, - \) - call himalaya#msg#list_with(account, mbox, himalaya#mbox#curr_page(), 1) - call setpos('.', pos) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#draft_save() - let s:draft = join(getline(1, "$"), "\n") . "\n" - redraw | call s:log("Save draft [OK]") - let &modified = 0 -endfunction - -function! himalaya#msg#draft_handle() - try - let account = himalaya#account#curr() - let attachments = join(map(s:attachment_paths, "'--attachment '.v:val"), " ") - while 1 - let choice = input("(s)end, (d)raft, (q)uit or (c)ancel? ") - let choice = tolower(choice)[0] - redraw | echo - - if choice == "s" - return s:cli( - \"--account %s template send %s -- %s", - \[shellescape(account), attachments, shellescape(s:draft)], - \"Sending message", - \0, - \) - elseif choice == "d" - return s:cli( - \"--account %s --mailbox Drafts template save %s -- %s", - \[shellescape(account), attachments, shellescape(s:draft)], - \"Saving draft", - \0, - \) - elseif choice == "q" - return - elseif choice == "c" - throw "Action canceled" - endif - endwhile - catch - " TODO: find a better way to prevent the buffer to close (stop the BufUnload event) - call himalaya#shared#log#err(v:exception) - throw "" - endtry -endfunction - -function! himalaya#msg#attachments() - try - let account = himalaya#account#curr() - let mbox = himalaya#mbox#curr_mbox() - let msg_id = stridx(bufname("%"), "Himalaya messages") == 0 ? s:get_focused_msg_id() : s:msg_id - let msg = s:cli( - \"--account %s --mailbox %s attachments %s", - \[shellescape(account), shellescape(mbox), msg_id], - \"Downloading attachments", - \0 - \) - call himalaya#shared#log#info(msg) - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#complete_contact(findstart, base) - try - if a:findstart - if !exists("g:himalaya_complete_contact_cmd") - echoerr "You must set 'g:himalaya_complete_contact_cmd' to complete contacts" - return -3 - endif - - " search for everything up to the last colon or comma - let line_to_cursor = getline(".")[:col(".") - 1] - let start = match(line_to_cursor, '[^:,]*$') - - " don't include leading spaces - while start <= len(line_to_cursor) && line_to_cursor[start] == " " - let start += 1 - endwhile - - return start - else - let output = system(substitute(g:himalaya_complete_contact_cmd, "%s", a:base, "")) - let lines = split(output, "\n") - - return map(lines, "s:line_to_complete_item(v:val)") - endif - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -function! himalaya#msg#add_attachment() - try - let attachment_path = input("Attachment path: ", "", "file") - if empty(expand(glob(attachment_path))) - throw "The file does not exist" - endif - call add(s:attachment_paths, attachment_path) - redraw | call himalaya#shared#log#info("Attachment added!") - catch - if !empty(v:exception) - redraw | call himalaya#shared#log#err(v:exception) - endif - endtry -endfunction - -" Utils - -" https://newbedev.com/get-usable-window-width-in-vim-script -function! s:bufwidth() - let width = winwidth(0) - let numberwidth = max([&numberwidth, strlen(line('$'))+1]) - let numwidth = (&number || &relativenumber)? numberwidth : 0 - let foldwidth = &foldcolumn - - if &signcolumn == 'yes' - let signwidth = 2 - elseif &signcolumn == 'auto' - let signs = execute(printf('sign place buffer=%d', bufnr(''))) - let signs = split(signs, '\n') - let signwidth = len(signs)>2? 2: 0 - else - let signwidth = 0 - endif - return width - numwidth - foldwidth - signwidth -endfunction - -function! s:get_msg_id(line) - return matchstr(a:line, '[0-9a-zA-Z]*') -endfunction - -function! s:get_focused_msg_id() - try - return s:get_msg_id(getline(".")) - catch - throw "message not found" - endtry -endfunction - -function! s:get_focused_msg_ids(from, to) - try - return join(map(range(a:from, a:to), "s:get_msg_id(getline(v:val))"), ",") - catch - throw "messages not found" - endtry -endfunction - -function! s:close_open_buffers(name) - let open_buffers = filter(range(1, bufnr('$')), 'bufexists(v:val)') - let target_buffers = filter(open_buffers, 'buffer_name(v:val) =~ a:name') - for buffer_to_close in target_buffers - execute ":bwipeout " . buffer_to_close - endfor -endfunction - -function! s:line_to_complete_item(line) - let fields = split(a:line, "\t") - let email = fields[0] - let name = "" - if len(fields) > 1 - let name = '"' . fields[1] . '" ' - endif - - return name . "<" . email . ">" -endfunction diff --git a/vim/autoload/himalaya/request.vim b/vim/autoload/himalaya/request.vim deleted file mode 100644 index 57712dc..0000000 --- a/vim/autoload/himalaya/request.vim +++ /dev/null @@ -1,59 +0,0 @@ -function! himalaya#request#json(opts) - let msg = get(a:, 'opts.msg', '') - let cmd = get(a:, 'opts.cmd', '') - let args = get(a:, 'opts.args', []) - let should_throw = get(a:, 'opts.should_throw', v:false) - - call himalaya#shared#log#info(printf('%s…', msg)) - let cmd = call('printf', ['himalaya --output json ' . cmd] + args) - let res = system(cmd) - - if empty(res) - redraw | call himalaya#shared#log#info(printf('%s [OK]', msg)) - else - try - let res = substitute(res, ':null', ':v:null', 'g') - let res = substitute(res, ':true', ':v:true', 'g') - let res = substitute(res, ':false', ':v:false', 'g') - let res = eval(res) - redraw | call himalaya#shared#log#info(printf('%s [OK]', msg)) - return res.response - catch - redraw - for line in split(res, '\n') - call himalaya#shared#log#err(line) - endfor - if should_throw - throw '' - endif - endtry - endif -endfunction - -function! himalaya#request#plain(opts) - let msg = get(a:opts, 'msg', '') - let cmd = get(a:opts, 'cmd', '') - let args = get(a:opts, 'args', []) - let should_throw = get(a:, 'opts.should_throw', v:false) - - call himalaya#shared#log#info(printf('%s…', msg)) - let cmd = call('printf', ['himalaya --output plain ' . cmd] + args) - let res = system(cmd) - - if empty(res) - redraw | call himalaya#shared#log#info(printf('%s [OK]', msg)) - else - try - redraw | call himalaya#shared#log#info(printf('%s [OK]', msg)) - return trim(res) - catch - redraw - for line in split(res, '\n') - call himalaya#shared#log#err(line) - endfor - if should_throw - throw '' - endif - endtry - endif -endfunction diff --git a/vim/autoload/himalaya/shared/bindings.vim b/vim/autoload/himalaya/shared/bindings.vim deleted file mode 100644 index 0cac565..0000000 --- a/vim/autoload/himalaya/shared/bindings.vim +++ /dev/null @@ -1,11 +0,0 @@ -function! himalaya#shared#bindings#define(bindings) - for [mode, key, name] in a:bindings - let plug = substitute(name, "[#_]", "-", "g") - let plug = printf("(himalaya-%s)", plug) - execute printf("%snoremap %s :call himalaya#%s()", mode, plug, name) - - if !hasmapto(plug, mode) - execute printf("%smap %s %s", mode, key, plug) - endif - endfor -endfunction diff --git a/vim/autoload/himalaya/shared/cli.vim b/vim/autoload/himalaya/shared/cli.vim deleted file mode 100644 index 2aee9b3..0000000 --- a/vim/autoload/himalaya/shared/cli.vim +++ /dev/null @@ -1,26 +0,0 @@ -function! himalaya#shared#cli#call(cmd, args, log, should_throw) - call himalaya#shared#log#info(printf("%s…", a:log)) - let cmd = call("printf", ["himalaya --output json " . a:cmd] + a:args) - let res = system(cmd) - - if empty(res) - redraw | call himalaya#shared#log#info(printf("%s [OK]", a:log)) - else - try - let res = substitute(res, ":null", ":v:null", "g") - let res = substitute(res, ":true", ":v:true", "g") - let res = substitute(res, ":false", ":v:false", "g") - let res = eval(res) - redraw | call himalaya#shared#log#info(printf("%s [OK]", a:log)) - return res.response - catch - redraw - for line in split(res, "\n") - call himalaya#shared#log#err(line) - endfor - if a:should_throw - throw "" - endif - endtry - endif -endfunction diff --git a/vim/autoload/himalaya/shared/log.vim b/vim/autoload/himalaya/shared/log.vim deleted file mode 100644 index 0d36bb3..0000000 --- a/vim/autoload/himalaya/shared/log.vim +++ /dev/null @@ -1,10 +0,0 @@ -function! himalaya#shared#log#info(msg) - echohl None - echomsg a:msg -endfunction - -function! himalaya#shared#log#err(msg) - echohl ErrorMsg - echomsg a:msg - echohl None -endfunction diff --git a/vim/autoload/himalaya/shared/thread.vim b/vim/autoload/himalaya/shared/thread.vim deleted file mode 100644 index 583c485..0000000 --- a/vim/autoload/himalaya/shared/thread.vim +++ /dev/null @@ -1,3 +0,0 @@ -function! himalaya#shared#thread#fold(lnum) - return getline(a:lnum)[0] == ">" -endfunction diff --git a/vim/autoload/himalaya/shared/utils.vim b/vim/autoload/himalaya/shared/utils.vim deleted file mode 100644 index a942585..0000000 --- a/vim/autoload/himalaya/shared/utils.vim +++ /dev/null @@ -1,30 +0,0 @@ -" Compose - -function! himalaya#shared#utils#compose(...) - let funcs = map(reverse(copy(a:000)), 'function(v:val)') - return function('s:compose', [funcs]) -endfunction - -function! s:compose(funcs, arg) - let data = a:arg - - for Func in a:funcs - let data = Func(data) - endfor - - return data -endfunction - -" Trim - -function! himalaya#shared#utils#trim(str) - return himalaya#shared#utils#compose('s:trim_left', 's:trim_right')(a:str) -endfunction - -function! s:trim_left(str) - return substitute(a:str, '^\s*', '', 'g') -endfunction - -function! s:trim_right(str) - return substitute(a:str, '\s*$', '', 'g') -endfunction diff --git a/vim/doc/.gitignore b/vim/doc/.gitignore deleted file mode 100644 index 55cf735..0000000 --- a/vim/doc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -tags \ No newline at end of file diff --git a/vim/doc/himalaya.txt b/vim/doc/himalaya.txt deleted file mode 100644 index 128fc16..0000000 --- a/vim/doc/himalaya.txt +++ /dev/null @@ -1,126 +0,0 @@ -*himalaya.txt* - Command-line interface for email management - - _/ _/ _/_/_/ _/ _/ _/_/ _/ _/_/ _/ _/ _/_/ - _/ _/ _/ _/_/ _/_/ _/ _/ _/ _/ _/ _/ _/ _/ _/ - _/_/_/_/ _/ _/ _/ _/ _/_/_/_/ _/ _/_/_/_/ _/ _/_/_/_/ - _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ _/ -_/ _/ _/_/_/ _/ _/ _/ _/ _/_/_/ _/ _/ _/ _/ _/ - -============================================================================== -TABLE OF CONTENTS *himalaya* *himalaya-contents* - - Requirements ....................................... |himalaya-requirements| - Configuration ..................................... |himalaya-configuration| - Usage ..................................................... |himalaya-usage| - License ................................................. |himalaya-license| - -============================================================================== -REQUIREMENTS *himalaya-requirements* - - - Vim or Neovim - - Himalaya CLI {1} - https://github.com/soywod/himalaya#installation {1} - -============================================================================== -CONFIGURATION *himalaya-configuration* - ------------------------------------------------------------------------------- -MAILBOX PICKER PROVIDER -> - let g:himalaya_mailbox_picker = 'native' | 'fzf' | 'telescope' -< -Defines the provider used for picking mailboxes: - -- `native`: a vim native input -- `fzf`: https://github.com/junegunn/fzf.vim -- `telescope`: https://github.com/nvim-telescope/telescope.nvim - -If no value given, the first loaded (and available) provider will be used (fzf -> telescope > native). - ------------------------------------------------------------------------------- -TELESCOPE PREVIEW -> - let g:himalaya_telescope_preview_enabled = 0 -< -Should enable telescope preview when picking a mailbox with the telescope -provider. - ------------------------------------------------------------------------------- -CONTACT COMPLETION -> - let g:himalaya_complete_contact_cmd = '' -< -Define the command to use for contact completion. When this is set, -'completefunc' will be set when composing messages so that contacts can be -completed with |i_CTRL-X_CTRL-U|. - -The command must print each possible result on its own line. Each line must -contain tab-separated fields; the first must be the email address, and the -second, if present, must be the name. `%s` in the command will be replaced -with the search query. - -For example, to complete contacts with khard, you could use -`khard email --remove-first-line --parsable '%s'` as the completion command. - -============================================================================== -USAGE *himalaya-usage* - -It is recommanded to first read the Himalaya CLI documentation {1} to -understand the concept and to configure it properly. - -Then, to list your emails: -> - :Himalaya -< -Change the current mbox gm -Show previous page gp -Show next page gn -Read focused msg -Write a new msg gw -Reply to the focused msg gr -Reply all to the focused msg gR -Forward the focused message gf -Download attachments from focused message ga -Copy the focused message gC -Move the focused message gM -Delete the focused message(s) gD - - https://github.com/soywod/himalaya#readme {1} - -============================================================================== -LICENSE *himalaya-license* - -Copyright © 2020,2021 Clément DOUIN (AKA soywod) - -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - - * Neither the name of Author name here nor the names of other - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -============================================================================== -vim:tw=78:ts=4:ft=help:norl: diff --git a/vim/ftplugin/himalaya-msg-list.vim b/vim/ftplugin/himalaya-msg-list.vim deleted file mode 100644 index de5abc2..0000000 --- a/vim/ftplugin/himalaya-msg-list.vim +++ /dev/null @@ -1,20 +0,0 @@ -setlocal buftype=nofile -setlocal cursorline -setlocal nomodifiable -setlocal nowrap - -call himalaya#shared#bindings#define([ - \["n", "gm" , "mbox#change" ], - \["n", "gp" , "mbox#prev_page" ], - \["n", "gn" , "mbox#next_page" ], - \["n", "", "msg#read" ], - \["n", "gw" , "msg#write" ], - \["n", "gr" , "msg#reply" ], - \["n", "gR" , "msg#reply_all" ], - \["n", "gf" , "msg#forward" ], - \["n", "ga" , "msg#attachments"], - \["n", "gC" , "msg#copy" ], - \["n", "gM" , "msg#move" ], - \["n", "gD" , "msg#delete" ], - \["v", "gD" , "msg#delete" ], -\]) diff --git a/vim/ftplugin/himalaya-msg-read.vim b/vim/ftplugin/himalaya-msg-read.vim deleted file mode 100644 index 9a9a3f3..0000000 --- a/vim/ftplugin/himalaya-msg-read.vim +++ /dev/null @@ -1,17 +0,0 @@ -setlocal bufhidden=wipe -setlocal buftype=nofile -setlocal filetype=mail -setlocal foldexpr=himalaya#shared#thread#fold(v:lnum) -setlocal foldmethod=expr -setlocal nomodifiable - -call himalaya#shared#bindings#define([ - \["n", "gw", "msg#write" ], - \["n", "gr", "msg#reply" ], - \["n", "gR", "msg#reply_all" ], - \["n", "gf", "msg#forward" ], - \["n", "ga", "msg#attachments"], - \["n", "gC", "msg#copy" ], - \["n", "gM", "msg#move" ], - \["n", "gD", "msg#delete" ], -\]) diff --git a/vim/ftplugin/himalaya-msg-write.vim b/vim/ftplugin/himalaya-msg-write.vim deleted file mode 100644 index ccbfd2d..0000000 --- a/vim/ftplugin/himalaya-msg-write.vim +++ /dev/null @@ -1,18 +0,0 @@ -setlocal filetype=mail -setlocal foldexpr=himalaya#shared#thread#fold(v:lnum) -setlocal foldmethod=expr -setlocal startofline - -if exists("g:himalaya_complete_contact_cmd") - setlocal completefunc=himalaya#msg#complete_contact -endif - -call himalaya#shared#bindings#define([ - \["n", "ga", "msg#add_attachment"], -\]) - -augroup himalaya_write - autocmd! * - autocmd BufWriteCmd call himalaya#msg#draft_save() - autocmd BufLeave call himalaya#msg#draft_handle() -augroup end diff --git a/vim/lua/himalaya/mbox.lua b/vim/lua/himalaya/mbox.lua deleted file mode 100644 index bee0e3d..0000000 --- a/vim/lua/himalaya/mbox.lua +++ /dev/null @@ -1,58 +0,0 @@ -local M = {} - -local actions = require('telescope.actions') -local action_state = require('telescope.actions.state') -local finders = require('telescope.finders') -local pickers = require('telescope.pickers') -local sorters = require('telescope.sorters') -local previewers = require('telescope.previewers') - -local function preview_command(entry, bufnr) - vim.api.nvim_buf_call(bufnr, function() - local page = 0 -- page 0 for preview - local account = vim.fn['himalaya#account#curr']() - local success, output = pcall(vim.fn['himalaya#msg#list_with'], account, entry.value, page, true) - if not (success) then - vim.cmd('redraw') - vim.bo.modifiable = true - local errors = vim.fn.split(output, '\n') - errors[1] = "Errors: "..errors[1] - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, errors) - end - end) -end - -local function entry_maker(entry) - return { - value = entry, - display = entry, - ordinal = entry, - preview_command = preview_command, - } -end - -M.mbox_picker = function(cb, mboxes) - local finder_opts = {results = mboxes} - local previewer = nil - if vim.g.himalaya_telescope_preview_enabled then - finder_opts.entry_maker = entry_maker - previewer = previewers.display_content.new({}) - end - pickers.new { - results_title = 'Mailboxes', - finder = finders.new_table(finder_opts), - sorter = sorters.get_generic_fuzzy_sorter(), - attach_mappings = function(prompt_bufnr) - actions.select_default:replace(function() - local selection = action_state.get_selected_entry() - actions.close(prompt_bufnr) - vim.fn[cb](selection.display) - end) - - return true - end, - previewer = previewer, - }:find() -end - -return M diff --git a/vim/plugin/himalaya.vim b/vim/plugin/himalaya.vim deleted file mode 100644 index 5c0a85c..0000000 --- a/vim/plugin/himalaya.vim +++ /dev/null @@ -1,19 +0,0 @@ -if exists("g:loaded_himalaya") - finish -endif - -if !executable("himalaya") - throw "Himalaya CLI not found, see https://github.com/soywod/himalaya#installation" -endif - -" Backup cpo -let s:cpo_backup = &cpo -set cpo&vim - -command! -nargs=* Himalaya call himalaya#msg#list() - -" Restore cpo -let &cpo = s:cpo_backup -unlet s:cpo_backup - -let g:loaded_himalaya = 1 diff --git a/vim/syntax/himalaya-msg-list.vim b/vim/syntax/himalaya-msg-list.vim deleted file mode 100644 index 8e200f1..0000000 --- a/vim/syntax/himalaya-msg-list.vim +++ /dev/null @@ -1,26 +0,0 @@ -if exists("b:current_syntax") - finish -endif - -syntax match HimalayaSeparator /│/ -syntax match HimalayaHead /.*\%1l/ contains=HimalayaSeparator -syntax match HimalayaId /^.\{-}│/ contains=HimalayaSeparator -syntax match HimalayaFlags /^.\{-}│.\{-}│/ contains=HimalayaId,HimalayaSeparator -syntax match HimalayaSubject /^.\{-}│.\{-}│.\{-}│/ contains=HimalayaId,HimalayaFlags,HimalayaSeparator -syntax match HimalayaSender /^.\{-}│.\{-}│.\{-}│.\{-}│/ contains=HimalayaId,HimalayaFlags,HimalayaSubject,HimalayaSeparator -syntax match HimalayaDate /^.\{-}│.\{-}│.\{-}│.\{-}│.\{-}/ contains=HimalayaId,HimalayaFlags,HimalayaSubject,HimalayaSender,HimalayaSeparator - -" FIXME: Find a way to set the line bold AND to keep the style of each columns. -" syntax match HimalayaUnseen /^.\{-}│✷.*$/ contains=HimalayaSeparator -" highlight HimalayaUnseen term=bold cterm=bold gui=bold - -highlight HimalayaHead term=bold,underline cterm=bold,underline gui=bold,underline - -highlight default link HimalayaSeparator VertSplit -highlight default link HimalayaId Identifier -highlight default link HimalayaFlags Special -highlight default link HimalayaSubject String -highlight default link HimalayaSender Structure -highlight default link HimalayaDate Constant - -let b:current_syntax = "himalaya-msg-list"