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