mirror of
https://github.com/soywod/himalaya.git
synced 2024-11-21 18:40:19 +00:00
clean config refactor
This commit is contained in:
parent
82b7dfb97f
commit
a3686c1c44
137 changed files with 2772 additions and 7546 deletions
46
CHANGELOG.md
46
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
|
||||
|
|
661
COPYING
Normal file
661
COPYING
Normal file
|
@ -0,0 +1,661 @@
|
|||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
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.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
<https://www.gnu.org/licenses/>.
|
422
Cargo.lock
generated
422
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
63
Cargo.toml
63
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 <clement.douin@posteo.net>"]
|
||||
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 }
|
||||
|
|
32
LICENSE
32
LICENSE
|
@ -1,32 +0,0 @@
|
|||
Copyright (c) 2020-2021, soywod (Clément DOUIN) <clement.douin@posteo.net>
|
||||
|
||||
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.
|
|
@ -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
|
||||
|
|
1
cli/.gitignore
vendored
1
cli/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
Cargo.lock
|
|
@ -1,59 +0,0 @@
|
|||
[package]
|
||||
name = "himalaya"
|
||||
description = "Command-line interface for email management"
|
||||
version = "0.5.10"
|
||||
authors = ["soywod <clement.douin@posteo.net>"]
|
||||
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 }
|
|
@ -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;
|
|
@ -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<Account>);
|
||||
|
||||
impl Deref for Accounts {
|
||||
type Target = Vec<Account>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Accounts {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<Iter<'_, String, DeserializedAccountConfig>> 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)
|
||||
}
|
||||
}
|
|
@ -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<Vec<ImapFetch>>;
|
||||
|
||||
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for fetch in fetches.iter().rev() {
|
||||
envelopes.push(from_imap_fetch(fetch).context("cannot parse imap fetch")?);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
|
@ -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;
|
351
cli/src/main.rs
351
cli/src/main.rs
|
@ -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<String> = 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")
|
||||
}
|
|
@ -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::*;
|
|
@ -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<String> {
|
||||
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<Vec<u8>> {
|
||||
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)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
//! Module related to SMTP.
|
|
@ -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<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub struct LettreService<'a> {
|
||||
account: &'a Account,
|
||||
transport: Option<SmtpTransport>,
|
||||
}
|
||||
|
||||
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<Vec<u8>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
15
flake.nix
15
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
|
||||
|
|
|
@ -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 }
|
|
@ -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<env::VarError>),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[error("cannot expand notmuch path")]
|
||||
ExpandNotmuchDatabasePathError(#[source] shellexpand::LookupError<env::VarError>),
|
||||
#[error("cannot expand mailbox alias {1}")]
|
||||
ExpandMboxAliasError(#[source] shellexpand::LookupError<env::VarError>, 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<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: usize,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
pub notify_query: String,
|
||||
/// Represents the watch commands.
|
||||
pub watch_cmds: Vec<String>,
|
||||
/// 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<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// 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<String>,
|
||||
/// Represents the command used to decrypt a message.
|
||||
pub pgp_decrypt_cmd: Option<String>,
|
||||
}
|
||||
|
||||
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<MailAddr, AccountError> {
|
||||
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<SmtpCredentials, AccountError> {
|
||||
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<String, AccountError> {
|
||||
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<String, AccountError> {
|
||||
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<S: AsRef<str>>(
|
||||
&self,
|
||||
file_name: S,
|
||||
) -> Result<PathBuf, AccountError> {
|
||||
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<PathBuf, AccountError> {
|
||||
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<S: AsRef<str>>(&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<String, AccountError> {
|
||||
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<String, AccountError> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
#[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")
|
||||
));
|
||||
}
|
||||
}
|
|
@ -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<String>,
|
||||
/// Overrides the downloads directory (mostly for attachments).
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
/// Overrides the signature for this account.
|
||||
pub signature: Option<String>,
|
||||
/// Overrides the signature delimiter for this account.
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Overrides the default page size for this account.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Overrides the notify command for this account.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the IMAP query used to fetch new messages for this account.
|
||||
pub notify_query: Option<String>,
|
||||
/// Overrides the watch commands for this account.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
/// Represents the text/plain format.
|
||||
pub format: Option<TextPlainFormat>,
|
||||
/// Represents the default headers displayed at the top of
|
||||
/// the read message.
|
||||
#[serde(default)]
|
||||
pub read_headers: Vec<String>,
|
||||
|
||||
/// Makes this account the default one.
|
||||
pub default: Option<bool>,
|
||||
/// 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<bool>,
|
||||
/// Trusts any certificate.
|
||||
pub smtp_insecure: Option<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<String>,
|
||||
/// Represents the command used to decrypt a message.
|
||||
pub pgp_decrypt_cmd: Option<String>,
|
||||
|
||||
/// Represents mailbox aliases.
|
||||
#[serde(default)]
|
||||
pub mailboxes: HashMap<String, String>,
|
||||
|
||||
/// Represents hooks.
|
||||
pub hooks: Option<Hooks>,
|
||||
|
||||
$(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<bool>,
|
||||
imap_insecure: Option<bool>,
|
||||
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
|
||||
);
|
|
@ -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<PathBuf>,
|
||||
/// Represents the signature of the user.
|
||||
pub signature: Option<String>,
|
||||
/// Overrides the default signature delimiter "`-- \n`".
|
||||
pub signature_delimiter: Option<String>,
|
||||
/// Represents the default page size for listings.
|
||||
pub default_page_size: Option<usize>,
|
||||
/// Represents the notify command.
|
||||
pub notify_cmd: Option<String>,
|
||||
/// Overrides the default IMAP query "NEW" used to fetch new messages
|
||||
pub notify_query: Option<String>,
|
||||
/// Represents the watch commands.
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
|
||||
/// Represents all the user accounts.
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, DeserializedAccountConfig>,
|
||||
}
|
||||
|
||||
impl DeserializedConfig {
|
||||
/// Tries to create a config from an optional path.
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self, DeserializeConfigError> {
|
||||
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<PathBuf, DeserializeConfigError> {
|
||||
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<PathBuf, DeserializeConfigError> {
|
||||
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<PathBuf, DeserializeConfigError> {
|
||||
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<PathBuf, DeserializeConfigError> {
|
||||
Self::path_from_xdg()
|
||||
.or_else(|_| Self::path_from_xdg_alt())
|
||||
.or_else(|_| Self::path_from_home())
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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<T> = result::Result<T, Error>;
|
||||
|
||||
pub trait Backend<'a> {
|
||||
fn connect(&mut self) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn add_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_mboxes(&mut self) -> Result<Mboxes>;
|
||||
fn del_mbox(&mut self, mbox: &str) -> Result<()>;
|
||||
fn get_envelopes(&mut self, mbox: &str, page_size: usize, page: usize) -> Result<Envelopes>;
|
||||
fn search_envelopes(
|
||||
&mut self,
|
||||
mbox: &str,
|
||||
query: &str,
|
||||
sort: &str,
|
||||
page_size: usize,
|
||||
page: usize,
|
||||
) -> Result<Envelopes>;
|
||||
fn add_msg(&mut self, mbox: &str, msg: &[u8], flags: &str) -> Result<String>;
|
||||
fn get_msg(&mut self, mbox: &str, id: &str) -> Result<Msg>;
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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<T> = result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct IdMapper {
|
||||
path: path::PathBuf,
|
||||
map: collections::HashMap<String, String>,
|
||||
short_hash_len: usize,
|
||||
}
|
||||
|
||||
impl IdMapper {
|
||||
pub fn new(dir: &path::Path) -> Result<Self> {
|
||||
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<String> {
|
||||
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::<Vec<_>>()
|
||||
.join(", "),
|
||||
))
|
||||
} else {
|
||||
Ok(self.get(matching_hashes[0]).unwrap().to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append(&mut self, lines: Vec<(String, String)>) -> Result<usize> {
|
||||
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<String, String>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.map
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for IdMapper {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.map
|
||||
}
|
||||
}
|
|
@ -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<T> = result::Result<T, Error>;
|
|
@ -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<TlsStream<TcpStream>>;
|
||||
|
||||
pub struct ImapBackend<'a> {
|
||||
account_config: &'a Account,
|
||||
imap_config: &'a ImapBackendConfig,
|
||||
sess: Option<ImapSess>,
|
||||
}
|
||||
|
||||
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<Vec<u32>> {
|
||||
let uids: Vec<u32> = 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<u32> = self
|
||||
.search_new_msgs(&self.account_config.notify_query)?
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<HashSet<_>>();
|
||||
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<u32> = 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::<Vec<_>>()
|
||||
.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<Mboxes> {
|
||||
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::<Vec<_>>()
|
||||
.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<Envelopes> {
|
||||
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<Envelopes> {
|
||||
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<String> = 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<String> {
|
||||
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<Msg> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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<Envelope> {
|
||||
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,
|
||||
})
|
||||
}
|
|
@ -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<Vec<ImapFetch>>;
|
||||
|
||||
pub fn from_imap_fetches(fetches: ImapFetches) -> Result<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for fetch in fetches.iter().rev() {
|
||||
envelopes.push(from_imap_fetch(fetch)?);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
|
@ -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()),
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
use crate::{
|
||||
backend::from_imap_flag,
|
||||
msg::{Flag, Flags},
|
||||
};
|
||||
|
||||
pub fn into_imap_flags<'a>(flags: &'a Flags) -> Vec<imap::types::Flag<'a>> {
|
||||
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()
|
||||
}
|
|
@ -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<imap::extensions::sort::SortCriterion<'a>>);
|
||||
|
||||
impl<'a> Deref for SortCriteria<'a> {
|
||||
type Target = Vec<imap::extensions::sort::SortCriterion<'a>>;
|
||||
|
||||
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<Self, Self::Error> {
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
|
@ -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<PathBuf> {
|
||||
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<maildir::Maildir> {
|
||||
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<Mboxes> {
|
||||
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<Envelopes> {
|
||||
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<Envelopes> {
|
||||
info!(">> search maildir envelopes");
|
||||
info!("<< search maildir envelopes");
|
||||
Err(MaildirError::SearchEnvelopesUnimplementedError)?
|
||||
}
|
||||
|
||||
fn add_msg(&mut self, dir: &str, msg: &[u8], flags: &str) -> Result<String> {
|
||||
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<Msg> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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<Envelope> {
|
||||
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)
|
||||
}
|
|
@ -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<Envelopes> {
|
||||
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)
|
||||
}
|
|
@ -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<char> {
|
||||
match flag {
|
||||
Flag::Answered => Some('R'),
|
||||
Flag::Seen => Some('S'),
|
||||
Flag::Deleted => Some('T'),
|
||||
Flag::Draft => Some('D'),
|
||||
Flag::Flagged => Some('F'),
|
||||
_ => None,
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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::*;
|
|
@ -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),
|
||||
}
|
|
@ -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<NotmuchBackend<'a>> {
|
||||
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<Envelopes> {
|
||||
// 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<Mboxes> {
|
||||
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<Envelopes> {
|
||||
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<Envelopes> {
|
||||
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<String> {
|
||||
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<Msg> {
|
||||
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(())
|
||||
}
|
||||
}
|
|
@ -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<Envelope> {
|
||||
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)
|
||||
}
|
|
@ -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<Envelopes> {
|
||||
let mut envelopes = Envelopes::default();
|
||||
for msg in msgs {
|
||||
let envelope = notmuch_envelope::from_notmuch_msg(msg)?;
|
||||
envelopes.push(envelope);
|
||||
}
|
||||
Ok(envelopes)
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
mod process;
|
||||
|
||||
pub mod account;
|
||||
pub mod backend;
|
||||
pub mod mbox;
|
||||
pub mod msg;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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<Mbox>,
|
||||
}
|
||||
|
||||
impl ops::Deref for Mboxes {
|
||||
type Target = Vec<Mbox>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.mboxes
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Mboxes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.mboxes
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
//! Mailbox module.
|
||||
//!
|
||||
//! This module contains everything related to mailboxes.
|
||||
|
||||
mod mbox;
|
||||
pub use mbox::*;
|
||||
|
||||
mod mboxes;
|
||||
pub use mboxes::*;
|
|
@ -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<S: AsRef<str> + fmt::Debug>(
|
||||
addrs: S,
|
||||
) -> result::Result<Option<Addrs>, mailparse::MailParseError> {
|
||||
let addrs = mailparse::addrparse(addrs.as_ref())?;
|
||||
Ok(if addrs.is_empty() { None } else { Some(addrs) })
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::message::Mailbox`].
|
||||
pub fn from_addrs_to_sendable_mbox(addrs: &Addrs) -> Result<Vec<lettre::message::Mailbox>> {
|
||||
let mut sendable_addrs: Vec<lettre::message::Mailbox> = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
Addr::Single(mailparse::SingleInfo { display_name, addr }) => sendable_addrs.push(
|
||||
lettre::message::Mailbox::new(display_name.clone(), addr.parse()?),
|
||||
),
|
||||
Addr::Group(mailparse::GroupInfo { group_name, addrs }) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(lettre::message::Mailbox::new(
|
||||
addr.display_name.clone().or(Some(group_name.clone())),
|
||||
addr.to_string().parse()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
||||
|
||||
/// Converts a list of addresses into a list of [`lettre::Address`].
|
||||
pub fn from_addrs_to_sendable_addrs(addrs: &Addrs) -> Result<Vec<lettre::Address>> {
|
||||
let mut sendable_addrs = vec![];
|
||||
for addr in addrs.iter() {
|
||||
match addr {
|
||||
mailparse::MailAddr::Single(mailparse::SingleInfo {
|
||||
display_name: _,
|
||||
addr,
|
||||
}) => {
|
||||
sendable_addrs.push(addr.parse()?);
|
||||
}
|
||||
mailparse::MailAddr::Group(mailparse::GroupInfo {
|
||||
group_name: _,
|
||||
addrs,
|
||||
}) => {
|
||||
for addr in addrs {
|
||||
sendable_addrs.push(addr.addr.parse()?);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(sendable_addrs)
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
use super::Flags;
|
||||
|
||||
/// Represents the message envelope. The envelope is just a message
|
||||
/// subset, and is mostly used for listings.
|
||||
#[derive(Debug, Default, Clone, Serialize)]
|
||||
pub struct Envelope {
|
||||
/// Represents the message identifier.
|
||||
pub id: String,
|
||||
/// Represents the internal message identifier.
|
||||
pub internal_id: String,
|
||||
/// Represents the message flags.
|
||||
pub flags: Flags,
|
||||
/// Represents the subject of the message.
|
||||
pub subject: String,
|
||||
/// Represents the first sender of the message.
|
||||
pub sender: String,
|
||||
/// Represents the internal date of the message.
|
||||
pub date: Option<String>,
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
use serde::Serialize;
|
||||
use std::ops;
|
||||
|
||||
use super::Envelope;
|
||||
|
||||
/// Represents the list of envelopes.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Envelopes {
|
||||
#[serde(rename = "response")]
|
||||
pub envelopes: Vec<Envelope>,
|
||||
}
|
||||
|
||||
impl ops::Deref for Envelopes {
|
||||
type Target = Vec<Envelope>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.envelopes
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Envelopes {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.envelopes
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
use std::{
|
||||
env, io,
|
||||
path::{self, PathBuf},
|
||||
result,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::account;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("cannot expand attachment path {1}")]
|
||||
ExpandAttachmentPathError(#[source] shellexpand::LookupError<env::VarError>, String),
|
||||
#[error("cannot read attachment at {1}")]
|
||||
ReadAttachmentError(#[source] io::Error, PathBuf),
|
||||
#[error("cannot parse template")]
|
||||
ParseTplError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot parse content type of attachment {1}")]
|
||||
ParseAttachmentContentTypeError(#[source] lettre::message::header::ContentTypeErr, String),
|
||||
#[error("cannot write temporary multipart on the disk")]
|
||||
WriteTmpMultipartError(#[source] io::Error),
|
||||
#[error("cannot write temporary multipart on the disk")]
|
||||
BuildSendableMsgError(#[source] lettre::error::Error),
|
||||
#[error("cannot parse {1} value: {2}")]
|
||||
ParseHeaderError(#[source] mailparse::MailParseError, String, String),
|
||||
#[error("cannot build envelope")]
|
||||
BuildEnvelopeError(#[source] lettre::error::Error),
|
||||
#[error("cannot get file name of attachment {0}")]
|
||||
GetAttachmentFilenameError(PathBuf),
|
||||
#[error("cannot parse recipient")]
|
||||
ParseRecipientError,
|
||||
|
||||
#[error("cannot parse message or address")]
|
||||
ParseAddressError(#[from] lettre::address::AddressError),
|
||||
|
||||
#[error(transparent)]
|
||||
AccountError(#[from] account::AccountError),
|
||||
|
||||
#[error("cannot get content type of multipart")]
|
||||
GetMultipartContentTypeError,
|
||||
#[error("cannot find encrypted part of multipart")]
|
||||
GetEncryptedPartMultipartError,
|
||||
#[error("cannot parse encrypted part of multipart")]
|
||||
ParseEncryptedPartError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot get body from encrypted part")]
|
||||
GetEncryptedPartBodyError(#[source] mailparse::MailParseError),
|
||||
#[error("cannot write encrypted part to temporary file")]
|
||||
WriteEncryptedPartBodyError(#[source] io::Error),
|
||||
#[error("cannot write encrypted part to temporary file")]
|
||||
DecryptPartError(#[source] account::AccountError),
|
||||
|
||||
#[error("cannot delete local draft: {1}")]
|
||||
DeleteLocalDraftError(#[source] io::Error, path::PathBuf),
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, Error>;
|
|
@ -1,27 +0,0 @@
|
|||
use serde::Serialize;
|
||||
|
||||
/// Represents the flag variants.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum Flag {
|
||||
Seen,
|
||||
Answered,
|
||||
Flagged,
|
||||
Deleted,
|
||||
Draft,
|
||||
Recent,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
impl From<&str> for Flag {
|
||||
fn from(flag_str: &str) -> Self {
|
||||
match flag_str {
|
||||
"seen" => Flag::Seen,
|
||||
"answered" | "replied" => Flag::Answered,
|
||||
"flagged" => Flag::Flagged,
|
||||
"deleted" | "trashed" => Flag::Deleted,
|
||||
"draft" => Flag::Draft,
|
||||
"recent" => Flag::Recent,
|
||||
flag => Flag::Custom(flag.into()),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
use serde::Serialize;
|
||||
use std::{fmt, ops};
|
||||
|
||||
use super::Flag;
|
||||
|
||||
/// Represents the list of flags.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Flags(pub Vec<Flag>);
|
||||
|
||||
impl Flags {
|
||||
/// Builds a symbols string.
|
||||
pub fn to_symbols_string(&self) -> String {
|
||||
let mut flags = String::new();
|
||||
flags.push_str(if self.contains(&Flag::Seen) {
|
||||
" "
|
||||
} else {
|
||||
"✷"
|
||||
});
|
||||
flags.push_str(if self.contains(&Flag::Answered) {
|
||||
"↵"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags.push_str(if self.contains(&Flag::Flagged) {
|
||||
"⚑"
|
||||
} else {
|
||||
" "
|
||||
});
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::Deref for Flags {
|
||||
type Target = Vec<Flag>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ops::DerefMut for Flags {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Flags {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut glue = "";
|
||||
|
||||
for flag in &self.0 {
|
||||
write!(f, "{}", glue)?;
|
||||
match flag {
|
||||
Flag::Seen => write!(f, "\\Seen")?,
|
||||
Flag::Answered => write!(f, "\\Answered")?,
|
||||
Flag::Flagged => write!(f, "\\Flagged")?,
|
||||
Flag::Deleted => write!(f, "\\Deleted")?,
|
||||
Flag::Draft => write!(f, "\\Draft")?,
|
||||
Flag::Recent => write!(f, "\\Recent")?,
|
||||
Flag::Custom(flag) => write!(f, "{}", flag)?,
|
||||
}
|
||||
glue = " ";
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Flags {
|
||||
fn from(flags: &str) -> Self {
|
||||
Flags(
|
||||
flags
|
||||
.split_whitespace()
|
||||
.map(|flag| flag.trim().into())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Flag> for Flags {
|
||||
fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
|
||||
let mut flags = Flags::default();
|
||||
for flag in iter {
|
||||
flags.push(flag);
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
mod error;
|
||||
pub use error::*;
|
||||
|
||||
mod flag;
|
||||
pub use flag::*;
|
||||
|
||||
mod flags;
|
||||
pub use flags::*;
|
||||
|
||||
mod envelope;
|
||||
pub use envelope::*;
|
||||
|
||||
mod envelopes;
|
||||
pub use envelopes::*;
|
||||
|
||||
mod parts;
|
||||
pub use parts::*;
|
||||
|
||||
mod addr;
|
||||
pub use addr::*;
|
||||
|
||||
mod tpl;
|
||||
pub use tpl::*;
|
||||
|
||||
mod msg;
|
||||
pub use msg::*;
|
||||
|
||||
mod msg_utils;
|
||||
pub use msg_utils::*;
|
|
@ -1,971 +0,0 @@
|
|||
use ammonia;
|
||||
use chrono::{DateTime, Local, TimeZone, Utc};
|
||||
use convert_case::{Case, Casing};
|
||||
use html_escape;
|
||||
use lettre::message::{header::ContentType, Attachment, MultiPart, SinglePart};
|
||||
use log::{info, trace, warn};
|
||||
use regex::Regex;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
convert::TryInto,
|
||||
env::temp_dir,
|
||||
fmt::Debug,
|
||||
fs,
|
||||
path::PathBuf,
|
||||
};
|
||||
use tree_magic;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
account::{Account, DEFAULT_SIG_DELIM},
|
||||
msg::{
|
||||
from_addrs_to_sendable_addrs, from_addrs_to_sendable_mbox, from_slice_to_addrs, Addr,
|
||||
Addrs, BinaryPart, Error, Part, Parts, Result, TextPlainPart, TplOverride,
|
||||
},
|
||||
};
|
||||
|
||||
/// Representation of a message.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct Msg {
|
||||
/// The sequence number of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.1.2
|
||||
pub id: u32,
|
||||
|
||||
/// The subject of the message.
|
||||
pub subject: String,
|
||||
|
||||
pub from: Option<Addrs>,
|
||||
pub reply_to: Option<Addrs>,
|
||||
pub to: Option<Addrs>,
|
||||
pub cc: Option<Addrs>,
|
||||
pub bcc: Option<Addrs>,
|
||||
pub in_reply_to: Option<String>,
|
||||
pub message_id: Option<String>,
|
||||
pub headers: HashMap<String, String>,
|
||||
|
||||
/// The internal date of the message.
|
||||
///
|
||||
/// [RFC3501]: https://datatracker.ietf.org/doc/html/rfc3501#section-2.3.3
|
||||
pub date: Option<DateTime<Local>>,
|
||||
pub parts: Parts,
|
||||
|
||||
pub encrypt: bool,
|
||||
|
||||
pub raw: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Msg {
|
||||
pub fn attachments(&self) -> Vec<BinaryPart> {
|
||||
self.parts
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
Part::Binary(part) => Some(part.to_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Folds string body from all plain text parts into a single
|
||||
/// string body. If no plain text parts are found, HTML parts are
|
||||
/// used instead. The result is sanitized (all HTML markup is
|
||||
/// removed).
|
||||
pub fn fold_text_plain_parts(&self) -> String {
|
||||
let (plain, html) = self.parts.iter().fold(
|
||||
(String::default(), String::default()),
|
||||
|(mut plain, mut html), part| {
|
||||
match part {
|
||||
Part::TextPlain(part) => {
|
||||
let glue = if plain.is_empty() { "" } else { "\n\n" };
|
||||
plain.push_str(glue);
|
||||
plain.push_str(&part.content);
|
||||
}
|
||||
Part::TextHtml(part) => {
|
||||
let glue = if html.is_empty() { "" } else { "\n\n" };
|
||||
html.push_str(glue);
|
||||
html.push_str(&part.content);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
(plain, html)
|
||||
},
|
||||
);
|
||||
if plain.is_empty() {
|
||||
// Remove HTML markup
|
||||
let sanitized_html = ammonia::Builder::new()
|
||||
.tags(HashSet::default())
|
||||
.clean(&html)
|
||||
.to_string();
|
||||
// Merge new line chars
|
||||
let sanitized_html = Regex::new(r"(\r?\n\s*){2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, "\n\n")
|
||||
.to_string();
|
||||
// Replace tabulations and &npsp; by spaces
|
||||
let sanitized_html = Regex::new(r"(\t| )")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, " ")
|
||||
.to_string();
|
||||
// Merge spaces
|
||||
let sanitized_html = Regex::new(r" {2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_html, " ")
|
||||
.to_string();
|
||||
// Decode HTML entities
|
||||
let sanitized_html = html_escape::decode_html_entities(&sanitized_html).to_string();
|
||||
|
||||
sanitized_html
|
||||
} else {
|
||||
// Merge new line chars
|
||||
let sanitized_plain = Regex::new(r"(\r?\n\s*){2,}")
|
||||
.unwrap()
|
||||
.replace_all(&plain, "\n\n")
|
||||
.to_string();
|
||||
// Replace tabulations by spaces
|
||||
let sanitized_plain = Regex::new(r"\t")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_plain, " ")
|
||||
.to_string();
|
||||
// Merge spaces
|
||||
let sanitized_plain = Regex::new(r" {2,}")
|
||||
.unwrap()
|
||||
.replace_all(&sanitized_plain, " ")
|
||||
.to_string();
|
||||
|
||||
sanitized_plain
|
||||
}
|
||||
}
|
||||
|
||||
/// Fold string body from all HTML parts into a single string
|
||||
/// body.
|
||||
fn fold_text_html_parts(&self) -> String {
|
||||
let text_parts = self
|
||||
.parts
|
||||
.iter()
|
||||
.filter_map(|part| match part {
|
||||
Part::TextHtml(part) => Some(part.content.to_owned()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
let text_parts = Regex::new(r"(\r?\n){2,}")
|
||||
.unwrap()
|
||||
.replace_all(&text_parts, "\n\n")
|
||||
.to_string();
|
||||
text_parts
|
||||
}
|
||||
|
||||
/// Fold string body from all text parts into a single string
|
||||
/// body. The mime allows users to choose between plain text parts
|
||||
/// and html text parts.
|
||||
pub fn fold_text_parts(&self, text_mime: &str) -> String {
|
||||
if text_mime == "html" {
|
||||
self.fold_text_html_parts()
|
||||
} else {
|
||||
self.fold_text_plain_parts()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_reply(mut self, all: bool, account: &Account) -> Result<Self> {
|
||||
let account_addr = account.address()?;
|
||||
|
||||
// In-Reply-To
|
||||
self.in_reply_to = self.message_id.to_owned();
|
||||
|
||||
// Message-Id
|
||||
self.message_id = None;
|
||||
|
||||
// To
|
||||
let addrs = self
|
||||
.reply_to
|
||||
.as_deref()
|
||||
.or_else(|| self.from.as_deref())
|
||||
.map(|addrs| {
|
||||
addrs.iter().cloned().filter(|addr| match addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(a) => match &account_addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(b) => a.addr != b.addr,
|
||||
},
|
||||
})
|
||||
});
|
||||
if all {
|
||||
self.to = addrs.map(|addrs| addrs.collect::<Vec<_>>().into());
|
||||
} else {
|
||||
self.to = addrs
|
||||
.and_then(|mut addrs| addrs.next())
|
||||
.map(|addr| vec![addr].into());
|
||||
}
|
||||
|
||||
// Cc
|
||||
self.cc = if all {
|
||||
self.cc.as_deref().map(|addrs| {
|
||||
addrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|addr| match addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(a) => match &account_addr {
|
||||
Addr::Group(_) => false,
|
||||
Addr::Single(b) => a.addr != b.addr,
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Bcc
|
||||
self.bcc = None;
|
||||
|
||||
// Body
|
||||
let plain_content = {
|
||||
let date = self
|
||||
.date
|
||||
.as_ref()
|
||||
.map(|date| date.format("%d %b %Y, at %H:%M (%z)").to_string())
|
||||
.unwrap_or_else(|| "unknown date".into());
|
||||
let sender = self
|
||||
.reply_to
|
||||
.as_ref()
|
||||
.or_else(|| self.from.as_ref())
|
||||
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||
.map(|addr| addr.display_name.clone().unwrap_or_else(|| addr.addr))
|
||||
.unwrap_or_else(|| "unknown sender".into());
|
||||
let mut content = format!("\n\nOn {}, {} wrote:\n", date, sender);
|
||||
|
||||
let mut glue = "";
|
||||
for line in self.fold_text_parts("plain").trim().lines() {
|
||||
if line == DEFAULT_SIG_DELIM {
|
||||
break;
|
||||
}
|
||||
content.push_str(glue);
|
||||
content.push('>');
|
||||
content.push_str(if line.starts_with('>') { "" } else { " " });
|
||||
content.push_str(line);
|
||||
glue = "\n";
|
||||
}
|
||||
|
||||
content
|
||||
};
|
||||
|
||||
self.parts = Parts(vec![Part::new_text_plain(plain_content)]);
|
||||
|
||||
// Subject
|
||||
if !self.subject.starts_with("Re:") {
|
||||
self.subject = format!("Re: {}", self.subject);
|
||||
}
|
||||
|
||||
// From
|
||||
self.from = Some(vec![account_addr.clone()].into());
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn into_forward(mut self, account: &Account) -> Result<Self> {
|
||||
let account_addr = account.address()?;
|
||||
|
||||
let prev_subject = self.subject.to_owned();
|
||||
let prev_date = self.date.to_owned();
|
||||
let prev_from = self.reply_to.to_owned().or_else(|| self.from.to_owned());
|
||||
let prev_to = self.to.to_owned();
|
||||
|
||||
// Message-Id
|
||||
self.message_id = None;
|
||||
|
||||
// In-Reply-To
|
||||
self.in_reply_to = None;
|
||||
|
||||
// From
|
||||
self.from = Some(vec![account_addr].into());
|
||||
|
||||
// To
|
||||
self.to = Some(vec![].into());
|
||||
|
||||
// Cc
|
||||
self.cc = None;
|
||||
|
||||
// Bcc
|
||||
self.bcc = None;
|
||||
|
||||
// Subject
|
||||
if !self.subject.starts_with("Fwd:") {
|
||||
self.subject = format!("Fwd: {}", self.subject);
|
||||
}
|
||||
|
||||
// Body
|
||||
let mut content = String::default();
|
||||
content.push_str("\n\n-------- Forwarded Message --------\n");
|
||||
content.push_str(&format!("Subject: {}\n", prev_subject));
|
||||
if let Some(date) = prev_date {
|
||||
content.push_str(&format!("Date: {}\n", date.to_rfc2822()));
|
||||
}
|
||||
if let Some(addrs) = prev_from.as_ref() {
|
||||
content.push_str("From: ");
|
||||
content.push_str(&addrs.to_string());
|
||||
content.push('\n');
|
||||
}
|
||||
if let Some(addrs) = prev_to.as_ref() {
|
||||
content.push_str("To: ");
|
||||
content.push_str(&addrs.to_string());
|
||||
content.push('\n');
|
||||
}
|
||||
content.push('\n');
|
||||
content.push_str(&self.fold_text_parts("plain"));
|
||||
self.parts
|
||||
.replace_text_plain_parts_with(TextPlainPart { content });
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn encrypt(mut self, encrypt: bool) -> Self {
|
||||
self.encrypt = encrypt;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_attachments(mut self, attachments_paths: Vec<&str>) -> Result<Self> {
|
||||
for path in attachments_paths {
|
||||
let path = shellexpand::full(path)
|
||||
.map_err(|err| Error::ExpandAttachmentPathError(err, path.to_owned()))?;
|
||||
let path = PathBuf::from(path.to_string());
|
||||
let filename: String = path
|
||||
.file_name()
|
||||
.ok_or_else(|| Error::GetAttachmentFilenameError(path.to_owned()))?
|
||||
.to_string_lossy()
|
||||
.into();
|
||||
let content =
|
||||
fs::read(&path).map_err(|err| Error::ReadAttachmentError(err, path.to_owned()))?;
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
|
||||
self.parts.push(Part::Binary(BinaryPart {
|
||||
filename,
|
||||
mime,
|
||||
content,
|
||||
}))
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn merge_with(&mut self, msg: Msg) {
|
||||
self.from = msg.from;
|
||||
self.reply_to = msg.reply_to;
|
||||
self.to = msg.to;
|
||||
self.cc = msg.cc;
|
||||
self.bcc = msg.bcc;
|
||||
self.subject = msg.subject;
|
||||
|
||||
if msg.message_id.is_some() {
|
||||
self.message_id = msg.message_id;
|
||||
}
|
||||
|
||||
if msg.in_reply_to.is_some() {
|
||||
self.in_reply_to = msg.in_reply_to;
|
||||
}
|
||||
|
||||
for part in msg.parts.0.into_iter() {
|
||||
match part {
|
||||
Part::Binary(_) => self.parts.push(part),
|
||||
Part::TextPlain(_) => {
|
||||
self.parts.retain(|p| !matches!(p, Part::TextPlain(_)));
|
||||
self.parts.push(part);
|
||||
}
|
||||
Part::TextHtml(_) => {
|
||||
self.parts.retain(|p| !matches!(p, Part::TextHtml(_)));
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_tpl(&self, opts: TplOverride, account: &Account) -> Result<String> {
|
||||
let account_addr: Addrs = vec![account.address()?].into();
|
||||
let mut tpl = String::default();
|
||||
|
||||
tpl.push_str("Content-Type: text/plain; charset=utf-8\n");
|
||||
|
||||
if let Some(in_reply_to) = self.in_reply_to.as_ref() {
|
||||
tpl.push_str(&format!("In-Reply-To: {}\n", in_reply_to))
|
||||
}
|
||||
|
||||
// From
|
||||
tpl.push_str(&format!(
|
||||
"From: {}\n",
|
||||
opts.from
|
||||
.map(|addrs| addrs.join(", "))
|
||||
.unwrap_or_else(|| account_addr.to_string())
|
||||
));
|
||||
|
||||
// To
|
||||
tpl.push_str(&format!(
|
||||
"To: {}\n",
|
||||
opts.to
|
||||
.map(|addrs| addrs.join(", "))
|
||||
.or_else(|| self.to.clone().map(|addrs| addrs.to_string()))
|
||||
.unwrap_or_default()
|
||||
));
|
||||
|
||||
// Cc
|
||||
if let Some(addrs) = opts
|
||||
.cc
|
||||
.map(|addrs| addrs.join(", "))
|
||||
.or_else(|| self.cc.clone().map(|addrs| addrs.to_string()))
|
||||
{
|
||||
tpl.push_str(&format!("Cc: {}\n", addrs));
|
||||
}
|
||||
|
||||
// Bcc
|
||||
if let Some(addrs) = opts
|
||||
.bcc
|
||||
.map(|addrs| addrs.join(", "))
|
||||
.or_else(|| self.bcc.clone().map(|addrs| addrs.to_string()))
|
||||
{
|
||||
tpl.push_str(&format!("Bcc: {}\n", addrs));
|
||||
}
|
||||
|
||||
// Subject
|
||||
tpl.push_str(&format!(
|
||||
"Subject: {}\n",
|
||||
opts.subject.unwrap_or(&self.subject)
|
||||
));
|
||||
|
||||
// Headers <=> body separator
|
||||
tpl.push('\n');
|
||||
|
||||
// Body
|
||||
if let Some(body) = opts.body {
|
||||
tpl.push_str(body);
|
||||
} else {
|
||||
tpl.push_str(&self.fold_text_plain_parts())
|
||||
}
|
||||
|
||||
// Signature
|
||||
if let Some(sig) = opts.sig {
|
||||
tpl.push_str("\n\n");
|
||||
tpl.push_str(sig);
|
||||
} else if let Some(ref sig) = account.sig {
|
||||
tpl.push_str("\n\n");
|
||||
tpl.push_str(sig);
|
||||
}
|
||||
|
||||
tpl.push('\n');
|
||||
|
||||
trace!("template: {:?}", tpl);
|
||||
Ok(tpl)
|
||||
}
|
||||
|
||||
pub fn from_tpl(tpl: &str) -> Result<Self> {
|
||||
info!("begin: building message from template");
|
||||
trace!("template: {:?}", tpl);
|
||||
|
||||
let parsed_mail = mailparse::parse_mail(tpl.as_bytes()).map_err(Error::ParseTplError)?;
|
||||
|
||||
info!("end: building message from template");
|
||||
Self::from_parsed_mail(parsed_mail, &Account::default())
|
||||
}
|
||||
|
||||
pub fn into_sendable_msg(&self, account: &Account) -> Result<lettre::Message> {
|
||||
let mut msg_builder = lettre::Message::builder()
|
||||
.message_id(self.message_id.to_owned())
|
||||
.subject(self.subject.to_owned());
|
||||
|
||||
if let Some(id) = self.in_reply_to.as_ref() {
|
||||
msg_builder = msg_builder.in_reply_to(id.to_owned());
|
||||
};
|
||||
|
||||
if let Some(addrs) = self.from.as_ref() {
|
||||
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||
msg_builder = msg_builder.from(addr)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(addrs) = self.to.as_ref() {
|
||||
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||
msg_builder = msg_builder.to(addr)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(addrs) = self.reply_to.as_ref() {
|
||||
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||
msg_builder = msg_builder.reply_to(addr)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(addrs) = self.cc.as_ref() {
|
||||
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||
msg_builder = msg_builder.cc(addr)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(addrs) = self.bcc.as_ref() {
|
||||
for addr in from_addrs_to_sendable_mbox(addrs)? {
|
||||
msg_builder = msg_builder.bcc(addr)
|
||||
}
|
||||
};
|
||||
|
||||
let mut multipart = {
|
||||
let mut multipart =
|
||||
MultiPart::mixed().singlepart(SinglePart::plain(self.fold_text_plain_parts()));
|
||||
for part in self.attachments() {
|
||||
multipart = multipart.singlepart(Attachment::new(part.filename.clone()).body(
|
||||
part.content,
|
||||
part.mime.parse().map_err(|err| {
|
||||
Error::ParseAttachmentContentTypeError(err, part.filename)
|
||||
})?,
|
||||
))
|
||||
}
|
||||
multipart
|
||||
};
|
||||
|
||||
if self.encrypt {
|
||||
let multipart_buffer = temp_dir().join(Uuid::new_v4().to_string());
|
||||
fs::write(multipart_buffer.clone(), multipart.formatted())
|
||||
.map_err(Error::WriteTmpMultipartError)?;
|
||||
let addr = self
|
||||
.to
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||
.map(|addr| addr.addr)
|
||||
.ok_or_else(|| Error::ParseRecipientError)?;
|
||||
let encrypted_multipart = account.pgp_encrypt_file(&addr, multipart_buffer.clone())?;
|
||||
trace!("encrypted multipart: {:#?}", encrypted_multipart);
|
||||
multipart = MultiPart::encrypted(String::from("application/pgp-encrypted"))
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::parse("application/pgp-encrypted").unwrap())
|
||||
.body(String::from("Version: 1")),
|
||||
)
|
||||
.singlepart(
|
||||
SinglePart::builder()
|
||||
.header(ContentType::parse("application/octet-stream").unwrap())
|
||||
.body(encrypted_multipart),
|
||||
)
|
||||
}
|
||||
|
||||
msg_builder
|
||||
.multipart(multipart)
|
||||
.map_err(Error::BuildSendableMsgError)
|
||||
}
|
||||
|
||||
pub fn from_parsed_mail(
|
||||
parsed_mail: mailparse::ParsedMail<'_>,
|
||||
config: &Account,
|
||||
) -> Result<Self> {
|
||||
trace!(">> build message from parsed mail");
|
||||
trace!("parsed mail: {:?}", parsed_mail);
|
||||
|
||||
let mut msg = Msg::default();
|
||||
for header in parsed_mail.get_headers() {
|
||||
trace!(">> parse header {:?}", header);
|
||||
|
||||
let key = header.get_key();
|
||||
trace!("header key: {:?}", key);
|
||||
|
||||
let val = header.get_value();
|
||||
trace!("header value: {:?}", val);
|
||||
|
||||
match key.to_lowercase().as_str() {
|
||||
"message-id" => msg.message_id = Some(val),
|
||||
"in-reply-to" => msg.in_reply_to = Some(val),
|
||||
"subject" => {
|
||||
msg.subject = val;
|
||||
}
|
||||
"date" => match mailparse::dateparse(&val) {
|
||||
Ok(timestamp) => {
|
||||
msg.date = Some(Utc.timestamp(timestamp, 0).with_timezone(&Local))
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("cannot parse message date {:?}, skipping it", val);
|
||||
warn!("{}", err);
|
||||
}
|
||||
},
|
||||
"from" => {
|
||||
msg.from = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"to" => {
|
||||
msg.to = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"reply-to" => {
|
||||
msg.reply_to = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"cc" => {
|
||||
msg.cc = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
"bcc" => {
|
||||
msg.bcc = from_slice_to_addrs(&val)
|
||||
.map_err(|err| Error::ParseHeaderError(err, key, val.to_owned()))?
|
||||
}
|
||||
key => {
|
||||
msg.headers.insert(key.to_lowercase(), val);
|
||||
}
|
||||
}
|
||||
trace!("<< parse header");
|
||||
}
|
||||
|
||||
msg.parts = Parts::from_parsed_mail(config, &parsed_mail)?;
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
info!("<< build message from parsed mail");
|
||||
Ok(msg)
|
||||
}
|
||||
|
||||
/// Transforms a message into a readable string. A readable
|
||||
/// message is like a template, except that:
|
||||
/// - headers part is customizable (can be omitted if empty filter given in argument)
|
||||
/// - body type is customizable (plain or html)
|
||||
pub fn to_readable_string(
|
||||
&self,
|
||||
text_mime: &str,
|
||||
headers: Vec<&str>,
|
||||
config: &Account,
|
||||
) -> Result<String> {
|
||||
let mut all_headers = vec![];
|
||||
for h in config.read_headers.iter() {
|
||||
let h = h.to_lowercase();
|
||||
if !all_headers.contains(&h) {
|
||||
all_headers.push(h)
|
||||
}
|
||||
}
|
||||
for h in headers.iter() {
|
||||
let h = h.to_lowercase();
|
||||
if !all_headers.contains(&h) {
|
||||
all_headers.push(h)
|
||||
}
|
||||
}
|
||||
|
||||
let mut readable_msg = String::new();
|
||||
for h in all_headers {
|
||||
match h.as_str() {
|
||||
"message-id" => match self.message_id {
|
||||
Some(ref message_id) if !message_id.is_empty() => {
|
||||
readable_msg.push_str(&format!("Message-Id: {}\n", message_id));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"in-reply-to" => match self.in_reply_to {
|
||||
Some(ref in_reply_to) if !in_reply_to.is_empty() => {
|
||||
readable_msg.push_str(&format!("In-Reply-To: {}\n", in_reply_to));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"subject" => {
|
||||
readable_msg.push_str(&format!("Subject: {}\n", self.subject));
|
||||
}
|
||||
"date" => {
|
||||
if let Some(ref date) = self.date {
|
||||
readable_msg.push_str(&format!("Date: {}\n", date.to_rfc2822()));
|
||||
}
|
||||
}
|
||||
"from" => match self.from {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("From: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"to" => match self.to {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("To: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"reply-to" => match self.reply_to {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Reply-To: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"cc" => match self.cc {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Cc: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
"bcc" => match self.bcc {
|
||||
Some(ref addrs) if !addrs.is_empty() => {
|
||||
readable_msg.push_str(&format!("Bcc: {}\n", addrs));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
key => match self.headers.get(key) {
|
||||
Some(ref val) if !val.is_empty() => {
|
||||
readable_msg.push_str(&format!("{}: {}\n", key.to_case(Case::Train), val));
|
||||
}
|
||||
_ => (),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if !readable_msg.is_empty() {
|
||||
readable_msg.push_str("\n");
|
||||
}
|
||||
|
||||
readable_msg.push_str(&self.fold_text_parts(text_mime));
|
||||
Ok(readable_msg)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::address::Envelope> for Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
(&self).try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryInto<lettre::address::Envelope> for &Msg {
|
||||
type Error = Error;
|
||||
|
||||
fn try_into(self) -> Result<lettre::address::Envelope> {
|
||||
let from = match self
|
||||
.from
|
||||
.as_ref()
|
||||
.and_then(|addrs| addrs.clone().extract_single_info())
|
||||
{
|
||||
Some(addr) => addr.addr.parse().map(Some),
|
||||
None => Ok(None),
|
||||
}?;
|
||||
let to = self
|
||||
.to
|
||||
.as_ref()
|
||||
.map(from_addrs_to_sendable_addrs)
|
||||
.unwrap_or(Ok(vec![]))?;
|
||||
Ok(lettre::address::Envelope::new(from, to).map_err(Error::BuildEnvelopeError)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailparse::SingleInfo;
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use crate::msg::Addr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_into_reply() {
|
||||
let config = Account {
|
||||
display_name: "Test".into(),
|
||||
email: "test-account@local".into(),
|
||||
..Account::default()
|
||||
};
|
||||
|
||||
// Checks that:
|
||||
// - "message_id" moves to "in_reply_to"
|
||||
// - "subject" starts by "Re: "
|
||||
// - "to" is replaced by "from"
|
||||
// - "from" is replaced by the address from the account config
|
||||
|
||||
let msg = Msg {
|
||||
message_id: Some("msg-id".into()),
|
||||
subject: "subject".into(),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-sender@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(false, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(msg.message_id, None);
|
||||
assert_eq!(msg.in_reply_to.unwrap(), "msg-id");
|
||||
assert_eq!(msg.subject, "Re: subject");
|
||||
assert_eq!(
|
||||
msg.from.unwrap().to_string(),
|
||||
"\"Test\" <test-account@local>"
|
||||
);
|
||||
assert_eq!(msg.to.unwrap().to_string(), "test-sender@local");
|
||||
|
||||
// Checks that:
|
||||
// - "subject" does not contains additional "Re: "
|
||||
// - "to" is replaced by reply_to
|
||||
// - "to" contains one address when "all" is false
|
||||
// - "cc" are empty when "all" is false
|
||||
|
||||
let msg = Msg {
|
||||
subject: "Re: subject".into(),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-sender@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
reply_to: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-to-reply@local".into(),
|
||||
display_name: Some("Sender".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-to-reply-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test-cc@local".into(),
|
||||
display_name: None,
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(false, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(msg.subject, "Re: subject");
|
||||
assert_eq!(
|
||||
msg.to.unwrap().to_string(),
|
||||
"\"Sender\" <test-sender-to-reply@local>"
|
||||
);
|
||||
assert_eq!(msg.cc, None);
|
||||
|
||||
// Checks that:
|
||||
// - "to" contains all addresses except for the sender when "all" is true
|
||||
// - "cc" contains all addresses except for the sender when "all" is true
|
||||
|
||||
let msg = Msg {
|
||||
from: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-1@local".into(),
|
||||
display_name: Some("Sender 1".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-account@local".into(),
|
||||
display_name: Some("Test".into()),
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(
|
||||
vec![
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-1@local".into(),
|
||||
display_name: Some("Sender 1".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-sender-2@local".into(),
|
||||
display_name: Some("Sender 2".into()),
|
||||
}),
|
||||
Addr::Single(SingleInfo {
|
||||
addr: "test-account@local".into(),
|
||||
display_name: None,
|
||||
}),
|
||||
]
|
||||
.into(),
|
||||
),
|
||||
..Msg::default()
|
||||
}
|
||||
.into_reply(true, &config)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
msg.to.unwrap().to_string(),
|
||||
"\"Sender 1\" <test-sender-1@local>, \"Sender 2\" <test-sender-2@local>"
|
||||
);
|
||||
assert_eq!(
|
||||
msg.cc.unwrap().to_string(),
|
||||
"\"Sender 1\" <test-sender-1@local>, \"Sender 2\" <test-sender-2@local>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_readable() {
|
||||
let config = Account::default();
|
||||
let msg = Msg {
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: String::from("hello, world!"),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
|
||||
// empty msg headers, empty headers, empty config
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec![], &config).unwrap()
|
||||
);
|
||||
// empty msg headers, basic headers
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec!["From", "DATE", "custom-hEader"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// empty msg headers, multiple subject headers
|
||||
assert_eq!(
|
||||
"Subject: \n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["subject", "Subject", "SUBJECT"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let msg = Msg {
|
||||
headers: HashMap::from_iter([("custom-header".into(), "custom value".into())]),
|
||||
message_id: Some("<message-id>".into()),
|
||||
from: Some(
|
||||
vec![Addr::Single(SingleInfo {
|
||||
addr: "test@local".into(),
|
||||
display_name: Some("Test".into()),
|
||||
})]
|
||||
.into(),
|
||||
),
|
||||
cc: Some(vec![].into()),
|
||||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: String::from("hello, world!"),
|
||||
})]),
|
||||
..Msg::default()
|
||||
};
|
||||
|
||||
// header present in msg headers, empty config
|
||||
assert_eq!(
|
||||
"From: \"Test\" <test@local>\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["from"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// header present but empty in msg headers, empty config
|
||||
assert_eq!(
|
||||
"hello, world!",
|
||||
msg.to_readable_string("plain", vec!["cc"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
// multiple same custom headers present in msg headers, empty
|
||||
// config
|
||||
assert_eq!(
|
||||
"Custom-Header: custom value\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["custom-header", "cuSTom-HeaDer"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
let config = Account {
|
||||
read_headers: vec![
|
||||
"CusTOM-heaDER".into(),
|
||||
"Subject".into(),
|
||||
"from".into(),
|
||||
"cc".into(),
|
||||
],
|
||||
..Account::default()
|
||||
};
|
||||
// header present but empty in msg headers, empty config
|
||||
assert_eq!(
|
||||
"Custom-Header: custom value\nSubject: \nFrom: \"Test\" <test@local>\nMessage-Id: <message-id>\n\nhello, world!",
|
||||
msg.to_readable_string("plain", vec!["cc", "message-ID"], &config)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
use log::{debug, trace};
|
||||
use std::{env, fs, path};
|
||||
|
||||
use crate::msg::{Error, Result};
|
||||
|
||||
pub fn local_draft_path() -> path::PathBuf {
|
||||
trace!(">> get local draft path");
|
||||
|
||||
let path = env::temp_dir().join("himalaya-draft.eml");
|
||||
debug!("local draft path: {:?}", path);
|
||||
|
||||
trace!("<< get local draft path");
|
||||
path
|
||||
}
|
||||
|
||||
pub fn remove_local_draft() -> Result<()> {
|
||||
trace!(">> remove local draft");
|
||||
|
||||
let path = local_draft_path();
|
||||
fs::remove_file(&path).map_err(|err| Error::DeleteLocalDraftError(err, path))?;
|
||||
|
||||
trace!("<< remove local draft");
|
||||
Ok(())
|
||||
}
|
|
@ -1,150 +0,0 @@
|
|||
use mailparse::MailHeaderMap;
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
env, fs,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{account::Account, msg};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextPlainPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct TextHtmlPart {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
pub struct BinaryPart {
|
||||
pub filename: String,
|
||||
pub mime: String,
|
||||
pub content: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Part {
|
||||
TextPlain(TextPlainPart),
|
||||
TextHtml(TextHtmlPart),
|
||||
Binary(BinaryPart),
|
||||
}
|
||||
|
||||
impl Part {
|
||||
pub fn new_text_plain(content: String) -> Self {
|
||||
Self::TextPlain(TextPlainPart { content })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Parts(pub Vec<Part>);
|
||||
|
||||
impl Parts {
|
||||
pub fn replace_text_plain_parts_with(&mut self, part: TextPlainPart) {
|
||||
self.retain(|part| !matches!(part, Part::TextPlain(_)));
|
||||
self.push(Part::TextPlain(part));
|
||||
}
|
||||
|
||||
pub fn from_parsed_mail<'a>(
|
||||
account: &'a Account,
|
||||
part: &'a mailparse::ParsedMail<'a>,
|
||||
) -> msg::Result<Self> {
|
||||
let mut parts = vec![];
|
||||
if part.subparts.is_empty() && part.get_headers().get_first_value("content-type").is_none()
|
||||
{
|
||||
let content = part.get_body().unwrap_or_default();
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else {
|
||||
build_parts_map_rec(account, part, &mut parts)?;
|
||||
}
|
||||
Ok(Self(parts))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Parts {
|
||||
type Target = Vec<Part>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Parts {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
fn build_parts_map_rec(
|
||||
account: &Account,
|
||||
parsed_mail: &mailparse::ParsedMail,
|
||||
parts: &mut Vec<Part>,
|
||||
) -> msg::Result<()> {
|
||||
if parsed_mail.subparts.is_empty() {
|
||||
let cdisp = parsed_mail.get_content_disposition();
|
||||
match cdisp.disposition {
|
||||
mailparse::DispositionType::Attachment => {
|
||||
let filename = cdisp
|
||||
.params
|
||||
.get("filename")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| String::from("noname"));
|
||||
let content = parsed_mail.get_body_raw().unwrap_or_default();
|
||||
let mime = tree_magic::from_u8(&content);
|
||||
parts.push(Part::Binary(BinaryPart {
|
||||
filename,
|
||||
mime,
|
||||
content,
|
||||
}));
|
||||
}
|
||||
// TODO: manage other use cases
|
||||
_ => {
|
||||
if let Some(ctype) = parsed_mail.get_headers().get_first_value("content-type") {
|
||||
let content = parsed_mail.get_body().unwrap_or_default();
|
||||
if ctype.starts_with("text/plain") {
|
||||
parts.push(Part::TextPlain(TextPlainPart { content }))
|
||||
} else if ctype.starts_with("text/html") {
|
||||
parts.push(Part::TextHtml(TextHtmlPart { content }))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
let ctype = parsed_mail
|
||||
.get_headers()
|
||||
.get_first_value("content-type")
|
||||
.ok_or_else(|| msg::Error::GetMultipartContentTypeError)?;
|
||||
if ctype.starts_with("multipart/encrypted") {
|
||||
let decrypted_part = parsed_mail
|
||||
.subparts
|
||||
.get(1)
|
||||
.ok_or_else(|| msg::Error::GetEncryptedPartMultipartError)
|
||||
.and_then(|part| decrypt_part(account, part))?;
|
||||
let parsed_mail = mailparse::parse_mail(decrypted_part.as_bytes())
|
||||
.map_err(msg::Error::ParseEncryptedPartError)?;
|
||||
build_parts_map_rec(account, &parsed_mail, parts)?;
|
||||
} else {
|
||||
for part in parsed_mail.subparts.iter() {
|
||||
build_parts_map_rec(account, part, parts)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decrypt_part(account: &Account, msg: &mailparse::ParsedMail) -> msg::Result<String> {
|
||||
let msg_path = env::temp_dir().join(Uuid::new_v4().to_string());
|
||||
let msg_body = msg
|
||||
.get_body()
|
||||
.map_err(msg::Error::GetEncryptedPartBodyError)?;
|
||||
fs::write(msg_path.clone(), &msg_body).map_err(msg::Error::WriteEncryptedPartBodyError)?;
|
||||
let content = account
|
||||
.pgp_decrypt_file(msg_path.clone())
|
||||
.map_err(msg::Error::DecryptPartError)?;
|
||||
Ok(content)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//! Module related to message template CLI.
|
||||
//!
|
||||
//! This module provides subcommands, arguments and a command matcher related to message template.
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
||||
pub struct TplOverride<'a> {
|
||||
pub subject: Option<&'a str>,
|
||||
pub from: Option<Vec<&'a str>>,
|
||||
pub to: Option<Vec<&'a str>>,
|
||||
pub cc: Option<Vec<&'a str>>,
|
||||
pub bcc: Option<Vec<&'a str>>,
|
||||
pub headers: Option<Vec<&'a str>>,
|
||||
pub body: Option<&'a str>,
|
||||
pub sig: Option<&'a str>,
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
//! Process module.
|
||||
//!
|
||||
//! This module contains cross platform helpers around the
|
||||
//! `std::process` crate.
|
||||
|
||||
use log::{debug, trace};
|
||||
use std::{io, process::Command, string};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ProcessError {
|
||||
#[error("cannot run command {1:?}")]
|
||||
RunCmdError(#[source] io::Error, String),
|
||||
|
||||
#[error("cannot parse command output")]
|
||||
ParseCmdOutputError(#[source] string::FromUtf8Error),
|
||||
}
|
||||
|
||||
pub fn run(cmd: &str) -> Result<String, ProcessError> {
|
||||
debug!(">> run command");
|
||||
debug!("command: {}", cmd);
|
||||
|
||||
let output = if cfg!(target_os = "windows") {
|
||||
Command::new("cmd").args(&["/C", cmd]).output()
|
||||
} else {
|
||||
Command::new("sh").arg("-c").arg(cmd).output()
|
||||
};
|
||||
let output = output.map_err(|err| ProcessError::RunCmdError(err, cmd.to_string()))?;
|
||||
let output = String::from_utf8(output.stdout).map_err(ProcessError::ParseCmdOutputError)?;
|
||||
|
||||
trace!("command output: {}", output);
|
||||
debug!("<< run command");
|
||||
Ok(output)
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
From: alice@localhost
|
||||
To: patrick@localhost
|
||||
Subject: Encrypted message
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted"; boundary="boundary"
|
||||
|
||||
--boundary
|
||||
Content-Type: application/pgp-encrypted
|
||||
|
||||
Version: 1
|
||||
|
||||
--boundary
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
--boundary
|
|
@ -1,7 +0,0 @@
|
|||
From: alice@localhost
|
||||
To: patrick@localhost
|
||||
Subject: Plain message
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Date: Tue, 1 Mar 2022 12:00:00 +0000
|
||||
|
||||
Ceci est un message.
|
|
@ -1,81 +0,0 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=XrAt
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
|
@ -1,41 +0,0 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
qtuSHJqRM01nuXfcSw==
|
||||
=JGp0
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
@ -1,81 +0,0 @@
|
|||
-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
|
||||
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
|
||||
=OpBu
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
|
@ -1,41 +0,0 @@
|
|||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
dnvixO4Xra6y6ZQzUoduR4I=
|
||||
=CQBw
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
@ -1,77 +0,0 @@
|
|||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::{
|
||||
account::{Account, ImapBackendConfig},
|
||||
backend::{Backend, ImapBackend},
|
||||
};
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[test]
|
||||
fn test_imap_backend() {
|
||||
// configure accounts
|
||||
let account_config = Account {
|
||||
smtp_host: "localhost".into(),
|
||||
smtp_port: 3465,
|
||||
smtp_starttls: false,
|
||||
smtp_insecure: true,
|
||||
smtp_login: "inbox@localhost".into(),
|
||||
smtp_passwd_cmd: "echo 'password'".into(),
|
||||
..Account::default()
|
||||
};
|
||||
let imap_config = ImapBackendConfig {
|
||||
imap_host: "localhost".into(),
|
||||
imap_port: 3993,
|
||||
imap_starttls: false,
|
||||
imap_insecure: true,
|
||||
imap_login: "inbox@localhost".into(),
|
||||
imap_passwd_cmd: "echo 'password'".into(),
|
||||
};
|
||||
let mut imap = ImapBackend::new(&account_config, &imap_config);
|
||||
imap.connect().unwrap();
|
||||
|
||||
// set up mailboxes
|
||||
if let Err(_) = imap.add_mbox("Mailbox1") {};
|
||||
if let Err(_) = imap.add_mbox("Mailbox2") {};
|
||||
imap.del_msg("Mailbox1", "1:*").unwrap();
|
||||
imap.del_msg("Mailbox2", "1:*").unwrap();
|
||||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let id = imap.add_msg("Mailbox1", msg, "seen").unwrap().to_string();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = imap.get_msg("Mailbox1", &id).unwrap();
|
||||
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
|
||||
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
|
||||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
assert_eq!("Plain message", envelope.subject);
|
||||
|
||||
// check that the message can be copied
|
||||
imap.copy_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
|
||||
.unwrap();
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
|
||||
// check that the message can be moved
|
||||
imap.move_msg("Mailbox1", "Mailbox2", &envelope.id.to_string())
|
||||
.unwrap();
|
||||
let envelopes = imap.get_envelopes("Mailbox1", 10, 0).unwrap();
|
||||
assert_eq!(0, envelopes.len());
|
||||
let envelopes = imap.get_envelopes("Mailbox2", 10, 0).unwrap();
|
||||
assert_eq!(2, envelopes.len());
|
||||
let id = envelopes.first().unwrap().id.to_string();
|
||||
|
||||
// check that the message can be deleted
|
||||
imap.del_msg("Mailbox2", &id).unwrap();
|
||||
assert!(imap.get_msg("Mailbox2", &id).is_err());
|
||||
|
||||
// check that disconnection works
|
||||
imap.disconnect().unwrap();
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
use maildir::Maildir;
|
||||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
use himalaya_lib::{
|
||||
account::{Account, MaildirBackendConfig},
|
||||
backend::{Backend, MaildirBackend},
|
||||
msg::Flag,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_maildir_backend() {
|
||||
// set up maildir folders
|
||||
let mdir: Maildir = env::temp_dir().join("himalaya-test-mdir").into();
|
||||
if let Err(_) = fs::remove_dir_all(mdir.path()) {}
|
||||
mdir.create_dirs().unwrap();
|
||||
|
||||
let mdir_sub: Maildir = mdir.path().join(".Subdir").into();
|
||||
if let Err(_) = fs::remove_dir_all(mdir_sub.path()) {}
|
||||
mdir_sub.create_dirs().unwrap();
|
||||
|
||||
// configure accounts
|
||||
let account_config = Account {
|
||||
mailboxes: HashMap::from_iter([("subdir".into(), "Subdir".into())]),
|
||||
..Account::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
maildir_dir: mdir.path().to_owned(),
|
||||
};
|
||||
let mut mdir = MaildirBackend::new(&account_config, &mdir_config);
|
||||
let mdir_sub_config = MaildirBackendConfig {
|
||||
maildir_dir: mdir_sub.path().to_owned(),
|
||||
};
|
||||
let mut mdir_subdir = MaildirBackend::new(&account_config, &mdir_sub_config);
|
||||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let hash = mdir.add_msg("inbox", msg, "seen").unwrap();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = mdir.get_msg("inbox", &hash).unwrap();
|
||||
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
|
||||
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
|
||||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = mdir.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
assert_eq!("Plain message", envelope.subject);
|
||||
|
||||
// check that a flag can be added to the message
|
||||
mdir.add_flags("inbox", &envelope.id, "flagged").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&Flag::Seen));
|
||||
assert!(envelope.flags.contains(&Flag::Flagged));
|
||||
|
||||
// check that the message flags can be changed
|
||||
mdir.set_flags("inbox", &envelope.id, "answered").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(!envelope.flags.contains(&Flag::Seen));
|
||||
assert!(!envelope.flags.contains(&Flag::Flagged));
|
||||
assert!(envelope.flags.contains(&Flag::Answered));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
mdir.del_flags("inbox", &envelope.id, "answered").unwrap();
|
||||
let envelopes = mdir.get_envelopes("inbox", 1, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(!envelope.flags.contains(&Flag::Seen));
|
||||
assert!(!envelope.flags.contains(&Flag::Flagged));
|
||||
assert!(!envelope.flags.contains(&Flag::Answered));
|
||||
|
||||
// check that the message can be copied
|
||||
mdir.copy_msg("inbox", "subdir", &envelope.id).unwrap();
|
||||
assert!(mdir.get_msg("inbox", &hash).is_ok());
|
||||
assert!(mdir.get_msg("subdir", &hash).is_ok());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_ok());
|
||||
|
||||
// check that the message can be moved
|
||||
mdir.move_msg("inbox", "subdir", &envelope.id).unwrap();
|
||||
assert!(mdir.get_msg("inbox", &hash).is_err());
|
||||
assert!(mdir.get_msg("subdir", &hash).is_ok());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_ok());
|
||||
|
||||
// check that the message can be deleted
|
||||
mdir.del_msg("subdir", &hash).unwrap();
|
||||
assert!(mdir.get_msg("subdir", &hash).is_err());
|
||||
assert!(mdir_subdir.get_msg("inbox", &hash).is_err());
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
#[cfg(feature = "notmuch-backend")]
|
||||
use std::{collections::HashMap, env, fs, iter::FromIterator};
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::{
|
||||
account::{Account, MaildirBackendConfig, NotmuchBackendConfig},
|
||||
backend::{Backend, MaildirBackend, NotmuchBackend},
|
||||
};
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[test]
|
||||
fn test_notmuch_backend() {
|
||||
use himalaya_lib::msg::Flag;
|
||||
|
||||
// set up maildir folders and notmuch database
|
||||
let mdir: maildir::Maildir = env::temp_dir().join("himalaya-test-notmuch").into();
|
||||
if let Err(_) = fs::remove_dir_all(mdir.path()) {}
|
||||
mdir.create_dirs().unwrap();
|
||||
notmuch::Database::create(mdir.path()).unwrap();
|
||||
|
||||
// configure accounts
|
||||
let account_config = AccountConfig {
|
||||
mailboxes: HashMap::from_iter([("inbox".into(), "*".into())]),
|
||||
..AccountConfig::default()
|
||||
};
|
||||
let mdir_config = MaildirBackendConfig {
|
||||
maildir_dir: mdir.path().to_owned(),
|
||||
};
|
||||
let notmuch_config = NotmuchBackendConfig {
|
||||
notmuch_database_dir: mdir.path().to_owned(),
|
||||
};
|
||||
let mut mdir = MaildirBackend::new(&account_config, &mdir_config);
|
||||
let mut notmuch = NotmuchBackend::new(&account_config, ¬much_config, &mut mdir).unwrap();
|
||||
|
||||
// check that a message can be added
|
||||
let msg = include_bytes!("./emails/alice-to-patrick.eml");
|
||||
let hash = notmuch.add_msg("", msg, "inbox seen").unwrap().to_string();
|
||||
|
||||
// check that the added message exists
|
||||
let msg = notmuch.get_msg("", &hash).unwrap();
|
||||
assert_eq!("alice@localhost", msg.from.clone().unwrap().to_string());
|
||||
assert_eq!("patrick@localhost", msg.to.clone().unwrap().to_string());
|
||||
assert_eq!("Ceci est un message.", msg.fold_text_plain_parts());
|
||||
|
||||
// check that the envelope of the added message exists
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert_eq!(1, envelopes.len());
|
||||
assert_eq!("alice@localhost", envelope.sender);
|
||||
assert_eq!("Plain message", envelope.subject);
|
||||
|
||||
// check that a flag can be added to the message
|
||||
notmuch
|
||||
.add_flags("", &envelope.id, "flagged answered")
|
||||
.unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that the message flags can be changed
|
||||
notmuch
|
||||
.set_flags("", &envelope.id, "inbox answered")
|
||||
.unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that a flag can be removed from the message
|
||||
notmuch.del_flags("", &envelope.id, "answered").unwrap();
|
||||
let envelopes = notmuch.get_envelopes("inbox", 10, 0).unwrap();
|
||||
let envelope = envelopes.first().unwrap();
|
||||
assert!(envelope.flags.contains(&Flag::Custom("inbox".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("seen".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("flagged".into())));
|
||||
assert!(!envelope.flags.contains(&Flag::Custom("answered".into())));
|
||||
|
||||
// check that the message can be deleted
|
||||
notmuch.del_msg("", &hash).unwrap();
|
||||
assert!(notmuch.get_msg("inbox", &hash).is_err());
|
||||
}
|
74
rustfmt.toml
74
rustfmt.toml
|
@ -1,74 +0,0 @@
|
|||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
newline_style = "Auto"
|
||||
indent_style = "Block"
|
||||
use_small_heuristics = "Default"
|
||||
fn_call_width = 60
|
||||
attr_fn_like_width = 70
|
||||
struct_lit_width = 18
|
||||
struct_variant_width = 35
|
||||
array_width = 60
|
||||
chain_width = 60
|
||||
single_line_if_else_max_width = 50
|
||||
wrap_comments = false
|
||||
format_code_in_doc_comments = false
|
||||
comment_width = 80
|
||||
normalize_comments = false
|
||||
normalize_doc_attributes = false
|
||||
license_template_path = ""
|
||||
format_strings = false
|
||||
format_macro_matchers = false
|
||||
format_macro_bodies = true
|
||||
empty_item_single_line = true
|
||||
struct_lit_single_line = true
|
||||
fn_single_line = false
|
||||
where_single_line = false
|
||||
imports_indent = "Block"
|
||||
imports_layout = "Mixed"
|
||||
imports_granularity = "Preserve"
|
||||
group_imports = "Preserve"
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
reorder_impl_items = false
|
||||
type_punctuation_density = "Wide"
|
||||
space_before_colon = false
|
||||
space_after_colon = true
|
||||
spaces_around_ranges = false
|
||||
binop_separator = "Front"
|
||||
remove_nested_parens = true
|
||||
combine_control_expr = true
|
||||
overflow_delimited_expr = false
|
||||
struct_field_align_threshold = 0
|
||||
enum_discrim_align_threshold = 0
|
||||
match_arm_blocks = true
|
||||
match_arm_leading_pipes = "Never"
|
||||
force_multiline_blocks = false
|
||||
fn_args_layout = "Tall"
|
||||
brace_style = "SameLineWhere"
|
||||
control_brace_style = "AlwaysSameLine"
|
||||
trailing_semicolon = true
|
||||
trailing_comma = "Vertical"
|
||||
match_block_trailing_comma = false
|
||||
blank_lines_upper_bound = 1
|
||||
blank_lines_lower_bound = 0
|
||||
edition = "2015"
|
||||
version = "One"
|
||||
inline_attribute_width = 0
|
||||
merge_derives = true
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
force_explicit_abi = true
|
||||
condense_wildcard_suffixes = false
|
||||
color = "Auto"
|
||||
unstable_features = false
|
||||
disable_all_formatting = false
|
||||
skip_children = false
|
||||
hide_parse_errors = false
|
||||
error_on_line_overflow = false
|
||||
error_on_unformatted = false
|
||||
report_todo = "Never"
|
||||
report_fixme = "Never"
|
||||
ignore = []
|
||||
emit_mode = "Files"
|
||||
make_backup = false
|
8
src/compl/mod.rs
Normal file
8
src/compl/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
//! Module related to shell completion.
|
||||
//!
|
||||
//! This module allows users to generate autocompletion scripts for
|
||||
//! their shells. You can see the list of available shells directly on
|
||||
//! the clap's [docs.rs](https://docs.rs/clap/2.33.3/clap/enum.Shell.html).
|
||||
|
||||
pub mod args;
|
||||
pub mod handlers;
|
576
src/config/config.rs
Normal file
576
src/config/config.rs
Normal file
|
@ -0,0 +1,576 @@
|
|||
// himalaya-lib, a Rust library for email management.
|
||||
// Copyright (C) 2022 soywod <clement.douin@posteo.net>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Deserialized config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of the
|
||||
//! user configuration file.
|
||||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use himalaya_lib::{AccountConfig, BackendConfig, EmailHooks, EmailTextPlainFormat};
|
||||
use log::{debug, trace};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
use toml;
|
||||
|
||||
use crate::{account::DeserializedAccountConfig, config::prelude::*};
|
||||
|
||||
/// Represents the user config file.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedConfig {
|
||||
#[serde(alias = "name")]
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub accounts: HashMap<String, DeserializedAccountConfig>,
|
||||
}
|
||||
|
||||
impl DeserializedConfig {
|
||||
/// Tries to create a config from an optional path.
|
||||
pub fn from_opt_path(path: Option<&str>) -> Result<Self> {
|
||||
trace!(">> parse config from path");
|
||||
debug!("path: {:?}", path);
|
||||
|
||||
let path = path.map(|s| s.into()).unwrap_or(Self::path()?);
|
||||
let content = fs::read_to_string(path).context("cannot read config file")?;
|
||||
let config: Self = toml::from_str(&content).context("cannot parse config file")?;
|
||||
|
||||
if config.accounts.is_empty() {
|
||||
return Err(anyhow!("config file must contain at least one account"));
|
||||
}
|
||||
|
||||
trace!("config: {:?}", config);
|
||||
trace!("<< parse config from path");
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from XDG_CONFIG_HOME
|
||||
/// environment variable.
|
||||
fn path_from_xdg() -> Result<PathBuf> {
|
||||
let path = env::var("XDG_CONFIG_HOME").context("cannot read env var XDG_CONFIG_HOME")?;
|
||||
let path = PathBuf::from(path).join("himalaya").join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the XDG config file path from HOME environment
|
||||
/// variable.
|
||||
fn path_from_xdg_alt() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path)
|
||||
.join(".config")
|
||||
.join("himalaya")
|
||||
.join("config.toml");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the .himalayarc config file path from HOME
|
||||
/// environment variable.
|
||||
fn path_from_home() -> Result<PathBuf> {
|
||||
let home_var = if cfg!(target_family = "windows") {
|
||||
"USERPROFILE"
|
||||
} else {
|
||||
"HOME"
|
||||
};
|
||||
let path = env::var(home_var).context(format!("cannot read env var {}", &home_var))?;
|
||||
let path = PathBuf::from(path).join(".himalayarc");
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Tries to get the config file path.
|
||||
pub fn path() -> Result<PathBuf> {
|
||||
Self::path_from_xdg()
|
||||
.or_else(|_| Self::path_from_xdg_alt())
|
||||
.or_else(|_| Self::path_from_home())
|
||||
}
|
||||
|
||||
pub fn to_configs(&self, account_name: Option<&str>) -> Result<(AccountConfig, BackendConfig)> {
|
||||
let (account_config, backend_config) = match account_name {
|
||||
Some("default") | Some("") | None => self
|
||||
.accounts
|
||||
.iter()
|
||||
.find_map(|(_, account)| {
|
||||
if account.is_default() {
|
||||
Some(account)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.ok_or_else(|| anyhow!("cannot find default account")),
|
||||
Some(name) => self
|
||||
.accounts
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow!(format!("cannot find account {}", name))),
|
||||
}?
|
||||
.to_configs(self);
|
||||
|
||||
Ok((account_config, backend_config))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{
|
||||
EmailSendCmd, EmailSender, ImapConfig, MaildirConfig, NotmuchConfig, SmtpConfig,
|
||||
};
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
use crate::account::{
|
||||
DeserializedBaseAccountConfig, DeserializedImapAccountConfig,
|
||||
DeserializedMaildirAccountConfig, DeserializedNotmuchAccountConfig,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_config(config: &str) -> Result<DeserializedConfig> {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
write!(file, "{}", config).unwrap();
|
||||
DeserializedConfig::from_opt_path(file.into_temp_path().to_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_config() {
|
||||
let config = make_config("");
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"config file must contain at least one account"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_backend_field() {
|
||||
let config = make_config("[account]");
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `backend` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_backend_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
backend = \"bad\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"unknown variant `bad`, expected one of `none`, `imap`, `maildir`, `notmuch` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_email_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
backend = \"none\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `email` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imap_account_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-host` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-port` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-login` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `imap-passwd-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_maildir_missing_root_dir_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `maildir-root-dir` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_notmuch_missing_db_path_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `notmuch-db-path` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_missing_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `sender` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_invalid_sender_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"bad\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"unknown variant `bad`, expected one of `none`, `internal`, `external` at line 1 column 1",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_internal_sender_missing_host_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"internal\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-host` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_internal_sender_missing_port_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"internal\"
|
||||
smtp-host = \"localhost\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-port` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_internal_sender_missing_login_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"internal\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-login` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_internal_sender_missing_passwd_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"internal\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `smtp-passwd-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_external_sender_missing_cmd_field() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"external\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap_err().root_cause().to_string(),
|
||||
"missing field `send-cmd` at line 1 column 1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_internal_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"internal\"
|
||||
smtp-host = \"localhost\"
|
||||
smtp-port = 25
|
||||
smtp-login = \"login\"
|
||||
smtp-passwd-cmd = \"echo password\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::None(DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
email_sender: EmailSender::Internal(SmtpConfig {
|
||||
host: "localhost".into(),
|
||||
port: 25,
|
||||
login: "login".into(),
|
||||
passwd_cmd: "echo password".into(),
|
||||
..SmtpConfig::default()
|
||||
}),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_external_sender_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
backend = \"none\"
|
||||
sender = \"external\"
|
||||
send-cmd = \"echo send\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::None(DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
email_sender: EmailSender::External(EmailSendCmd {
|
||||
cmd: "echo send".into(),
|
||||
}),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_imap_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"imap\"
|
||||
imap-host = \"localhost\"
|
||||
imap-port = 993
|
||||
imap-login = \"login\"
|
||||
imap-passwd-cmd = \"echo password\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: ImapConfig {
|
||||
host: "localhost".into(),
|
||||
port: 993,
|
||||
login: "login".into(),
|
||||
passwd_cmd: "echo password".into(),
|
||||
..ImapConfig::default()
|
||||
}
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_maildir_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"maildir\"
|
||||
maildir-root-dir = \"/tmp/maildir\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::Maildir(DeserializedMaildirAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: MaildirConfig {
|
||||
root_dir: "/tmp/maildir".into(),
|
||||
}
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_backend_notmuch_minimum_config() {
|
||||
let config = make_config(
|
||||
"[account]
|
||||
email = \"test@localhost\"
|
||||
sender = \"none\"
|
||||
backend = \"notmuch\"
|
||||
notmuch-db-path = \"/tmp/notmuch.db\"",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
config.unwrap(),
|
||||
DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account".into(),
|
||||
DeserializedAccountConfig::Notmuch(DeserializedNotmuchAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
email: "test@localhost".into(),
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: NotmuchConfig {
|
||||
db_path: "/tmp/notmuch.db".into(),
|
||||
}
|
||||
})
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
5
src/config/mod.rs
Normal file
5
src/config/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod prelude;
|
||||
|
||||
pub use config::*;
|
139
src/config/prelude.rs
Normal file
139
src/config/prelude.rs
Normal file
|
@ -0,0 +1,139 @@
|
|||
use himalaya_lib::{EmailHooks, EmailSendCmd, EmailSender, EmailTextPlainFormat, SmtpConfig};
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
use himalaya_lib::ImapConfig;
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
use himalaya_lib::MaildirConfig;
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
use himalaya_lib::NotmuchConfig;
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "SmtpConfig")]
|
||||
struct SmtpConfigDef {
|
||||
#[serde(rename = "smtp-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "smtp-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "smtp-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "smtp-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "smtp-login")]
|
||||
pub login: String,
|
||||
#[serde(rename = "smtp-passwd-cmd")]
|
||||
pub passwd_cmd: String,
|
||||
}
|
||||
|
||||
#[cfg(feature = "imap-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "ImapConfig")]
|
||||
pub struct ImapConfigDef {
|
||||
#[serde(rename = "imap-host")]
|
||||
pub host: String,
|
||||
#[serde(rename = "imap-port")]
|
||||
pub port: u16,
|
||||
#[serde(rename = "imap-starttls")]
|
||||
pub starttls: Option<bool>,
|
||||
#[serde(rename = "imap-insecure")]
|
||||
pub insecure: Option<bool>,
|
||||
#[serde(rename = "imap-login")]
|
||||
pub login: String,
|
||||
#[serde(rename = "imap-passwd-cmd")]
|
||||
pub passwd_cmd: String,
|
||||
#[serde(rename = "imap-notify-cmd")]
|
||||
pub notify_cmd: Option<String>,
|
||||
#[serde(rename = "imap-notify-query")]
|
||||
pub notify_query: Option<String>,
|
||||
#[serde(rename = "imap-watch-cmds")]
|
||||
pub watch_cmds: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "MaildirConfig")]
|
||||
pub struct MaildirConfigDef {
|
||||
#[serde(rename = "maildir-root-dir")]
|
||||
pub root_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "NotmuchConfig")]
|
||||
pub struct NotmuchConfigDef {
|
||||
#[serde(rename = "notmuch-db-path")]
|
||||
pub db_path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailTextPlainFormat", rename_all = "snake_case")]
|
||||
enum EmailTextPlainFormatDef {
|
||||
Auto,
|
||||
Flowed,
|
||||
Fixed(usize),
|
||||
}
|
||||
|
||||
pub mod email_text_plain_format {
|
||||
use himalaya_lib::EmailTextPlainFormat;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailTextPlainFormatDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailTextPlainFormat>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailTextPlainFormatDef")] EmailTextPlainFormat);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailSender", tag = "sender", rename_all = "snake_case")]
|
||||
pub enum EmailSenderDef {
|
||||
None,
|
||||
#[serde(with = "SmtpConfigDef")]
|
||||
Internal(SmtpConfig),
|
||||
#[serde(with = "EmailSendCmdDef")]
|
||||
External(EmailSendCmd),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailSendCmd")]
|
||||
pub struct EmailSendCmdDef {
|
||||
#[serde(rename = "send-cmd")]
|
||||
cmd: String,
|
||||
}
|
||||
|
||||
/// Represents the email hooks. Useful for doing extra email
|
||||
/// processing before or after sending it.
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(remote = "EmailHooks")]
|
||||
struct EmailHooksDef {
|
||||
/// Represents the hook called just before sending an email.
|
||||
pub pre_send: Option<String>,
|
||||
}
|
||||
|
||||
pub mod email_hooks {
|
||||
use himalaya_lib::EmailHooks;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
use super::EmailHooksDef;
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<EmailHooks>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
struct Helper(#[serde(with = "EmailHooksDef")] EmailHooks);
|
||||
|
||||
let helper = Option::deserialize(deserializer)?;
|
||||
Ok(helper.map(|Helper(external)| external))
|
||||
}
|
||||
}
|
54
src/domain/account/account.rs
Normal file
54
src/domain/account/account.rs
Normal file
|
@ -0,0 +1,54 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
|
||||
use serde::Serialize;
|
||||
use std::fmt;
|
||||
|
||||
use crate::ui::table::{Cell, Row, Table};
|
||||
|
||||
/// Represents the printable account.
|
||||
#[derive(Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct Account {
|
||||
/// Represents the account name.
|
||||
pub name: String,
|
||||
/// Represents the backend name of the account.
|
||||
pub backend: String,
|
||||
/// Represents the default state of the account.
|
||||
pub default: bool,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
pub fn new(name: &str, backend: &str, default: bool) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
backend: backend.into(),
|
||||
default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Table for Account {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("NAME").shrinkable().bold().underline().white())
|
||||
.cell(Cell::new("BACKEND").bold().underline().white())
|
||||
.cell(Cell::new("DEFAULT").bold().underline().white())
|
||||
}
|
||||
|
||||
fn row(&self) -> Row {
|
||||
let default = if self.default { "yes" } else { "" };
|
||||
Row::new()
|
||||
.cell(Cell::new(&self.name).shrinkable().green())
|
||||
.cell(Cell::new(&self.backend).blue())
|
||||
.cell(Cell::new(default).white())
|
||||
}
|
||||
}
|
61
src/domain/account/accounts.rs
Normal file
61
src/domain/account/accounts.rs
Normal file
|
@ -0,0 +1,61 @@
|
|||
//! Account module.
|
||||
//!
|
||||
//! This module contains the definition of the printable account,
|
||||
//! which is only used by the "accounts" command to list all available
|
||||
//! accounts from the config file.
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::Serialize;
|
||||
use std::{collections::hash_map::Iter, ops::Deref};
|
||||
|
||||
use crate::{
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
use super::{Account, DeserializedAccountConfig};
|
||||
|
||||
/// Represents the list of printable accounts.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct Accounts(pub Vec<Account>);
|
||||
|
||||
impl Deref for Accounts {
|
||||
type Target = Vec<Account>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PrintTable for Accounts {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
||||
writeln!(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Iter<'_, String, DeserializedAccountConfig>> for Accounts {
|
||||
fn from(map: Iter<'_, String, DeserializedAccountConfig>) -> Self {
|
||||
let mut accounts: Vec<_> = map
|
||||
.map(|(name, account)| match account {
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => {
|
||||
Account::new(name, "imap", config.base.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => {
|
||||
Account::new(name, "maildir", config.base.default.unwrap_or_default())
|
||||
}
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => {
|
||||
Account::new(name, "notmuch", config.base.default.unwrap_or_default())
|
||||
}
|
||||
DeserializedAccountConfig::None(..) => Account::new(name, "none", false),
|
||||
})
|
||||
.collect();
|
||||
accounts.sort_by(|a, b| b.name.partial_cmp(&a.name).unwrap());
|
||||
Self(accounts)
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ use anyhow::Result;
|
|||
use clap::{App, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table_arg;
|
||||
use crate::ui::table;
|
||||
|
||||
type MaxTableWidth = Option<usize>;
|
||||
|
||||
|
@ -41,7 +41,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
vec![SubCommand::with_name("accounts")
|
||||
.aliases(&["account", "acc", "a"])
|
||||
.about("Lists accounts")
|
||||
.arg(table_arg::max_width())]
|
||||
.arg(table::args::max_width())]
|
||||
}
|
||||
|
||||
/// Represents the user account name argument.
|
224
src/domain/account/config.rs
Normal file
224
src/domain/account/config.rs
Normal file
|
@ -0,0 +1,224 @@
|
|||
// himalaya-lib, a Rust library for email management.
|
||||
// Copyright (C) 2022 soywod <clement.douin@posteo.net>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Deserialized account config module.
|
||||
//!
|
||||
//! This module contains the raw deserialized representation of an
|
||||
//! account in the accounts section of the user configuration file.
|
||||
|
||||
use himalaya_lib::{
|
||||
AccountConfig, BackendConfig, EmailHooks, EmailSender, EmailTextPlainFormat, ImapConfig,
|
||||
MaildirConfig, NotmuchConfig,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::config::{prelude::*, DeserializedConfig};
|
||||
|
||||
/// Represents all existing kind of account config.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(tag = "backend", rename_all = "snake_case")]
|
||||
pub enum DeserializedAccountConfig {
|
||||
None(DeserializedBaseAccountConfig),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
Imap(DeserializedImapAccountConfig),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
Maildir(DeserializedMaildirAccountConfig),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
Notmuch(DeserializedNotmuchAccountConfig),
|
||||
}
|
||||
|
||||
impl DeserializedAccountConfig {
|
||||
pub fn to_configs(&self, global_config: &DeserializedConfig) -> (AccountConfig, BackendConfig) {
|
||||
match self {
|
||||
DeserializedAccountConfig::None(config) => {
|
||||
(config.to_account_config(global_config), BackendConfig::None)
|
||||
}
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Imap(&config.backend),
|
||||
),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Maildir(&config.backend),
|
||||
),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => (
|
||||
config.base.to_account_config(global_config),
|
||||
BackendConfig::Notmuch(&config.backend),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_default(&self) -> bool {
|
||||
match self {
|
||||
DeserializedAccountConfig::None(config) => config.default.unwrap_or_default(),
|
||||
#[cfg(feature = "imap-backend")]
|
||||
DeserializedAccountConfig::Imap(config) => config.base.default.unwrap_or_default(),
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
DeserializedAccountConfig::Maildir(config) => config.base.default.unwrap_or_default(),
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
DeserializedAccountConfig::Notmuch(config) => config.base.default.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct DeserializedBaseAccountConfig {
|
||||
pub email: String,
|
||||
pub default: Option<bool>,
|
||||
pub display_name: Option<String>,
|
||||
pub signature_delim: Option<String>,
|
||||
pub signature: Option<String>,
|
||||
pub downloads_dir: Option<PathBuf>,
|
||||
|
||||
pub folder_listing_page_size: Option<usize>,
|
||||
pub folder_aliases: Option<HashMap<String, String>>,
|
||||
|
||||
pub email_listing_page_size: Option<usize>,
|
||||
pub email_reading_headers: Option<Vec<String>>,
|
||||
#[serde(default, with = "email_text_plain_format")]
|
||||
pub email_reading_format: Option<EmailTextPlainFormat>,
|
||||
pub email_reading_decrypt_cmd: Option<String>,
|
||||
pub email_writing_encrypt_cmd: Option<String>,
|
||||
#[serde(flatten, with = "EmailSenderDef")]
|
||||
pub email_sender: EmailSender,
|
||||
#[serde(default, with = "email_hooks")]
|
||||
pub email_hooks: Option<EmailHooks>,
|
||||
}
|
||||
|
||||
impl DeserializedBaseAccountConfig {
|
||||
pub fn to_account_config(&self, config: &DeserializedConfig) -> AccountConfig {
|
||||
let mut folder_aliases = config
|
||||
.folder_aliases
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default();
|
||||
folder_aliases.extend(
|
||||
self.folder_aliases
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
AccountConfig {
|
||||
email: self.email.to_owned(),
|
||||
display_name: self
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.display_name.as_ref().map(ToOwned::to_owned)),
|
||||
signature_delim: self
|
||||
.signature_delim
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.signature_delim.as_ref().map(ToOwned::to_owned)),
|
||||
signature: self
|
||||
.signature
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.signature.as_ref().map(ToOwned::to_owned)),
|
||||
downloads_dir: self
|
||||
.downloads_dir
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.downloads_dir.as_ref().map(ToOwned::to_owned)),
|
||||
folder_listing_page_size: self
|
||||
.folder_listing_page_size
|
||||
.or_else(|| config.folder_listing_page_size),
|
||||
folder_aliases,
|
||||
email_listing_page_size: self
|
||||
.email_listing_page_size
|
||||
.or_else(|| config.email_listing_page_size),
|
||||
email_reading_headers: self
|
||||
.email_reading_headers
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_reading_headers.as_ref().map(ToOwned::to_owned)),
|
||||
email_reading_format: self
|
||||
.email_reading_format
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| config.email_reading_format.as_ref().map(ToOwned::to_owned))
|
||||
.unwrap_or_default(),
|
||||
email_reading_decrypt_cmd: self
|
||||
.email_reading_decrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_reading_decrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_writing_encrypt_cmd: self
|
||||
.email_writing_encrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_writing_encrypt_cmd
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
}),
|
||||
email_sender: self.email_sender.to_owned(),
|
||||
email_hooks: EmailHooks {
|
||||
pre_send: self
|
||||
.email_hooks
|
||||
.as_ref()
|
||||
.map(ToOwned::to_owned)
|
||||
.map(|hook| hook.pre_send)
|
||||
.or_else(|| {
|
||||
config
|
||||
.email_hooks
|
||||
.as_ref()
|
||||
.map(|hook| hook.pre_send.as_ref().map(ToOwned::to_owned))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "imap-backend")]
|
||||
pub struct DeserializedImapAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "ImapConfigDef")]
|
||||
pub backend: ImapConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "maildir-backend")]
|
||||
pub struct DeserializedMaildirAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "MaildirConfigDef")]
|
||||
pub backend: MaildirConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
|
||||
#[cfg(feature = "notmuch-backend")]
|
||||
pub struct DeserializedNotmuchAccountConfig {
|
||||
#[serde(flatten)]
|
||||
pub base: DeserializedBaseAccountConfig,
|
||||
#[serde(flatten, with = "NotmuchConfigDef")]
|
||||
pub backend: NotmuchConfig,
|
||||
}
|
|
@ -3,30 +3,31 @@
|
|||
//! This module gathers all account actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::account::{Account, DeserializedConfig};
|
||||
use himalaya_lib::AccountConfig;
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::{
|
||||
config::Accounts,
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
config::DeserializedConfig,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
Accounts,
|
||||
};
|
||||
|
||||
/// Lists all accounts.
|
||||
pub fn list<'a, P: PrinterService>(
|
||||
pub fn list<'a, P: Printer>(
|
||||
max_width: Option<usize>,
|
||||
config: &DeserializedConfig,
|
||||
account_config: &Account,
|
||||
config: &AccountConfig,
|
||||
deserialized_config: &DeserializedConfig,
|
||||
printer: &mut P,
|
||||
) -> Result<()> {
|
||||
info!(">> account list handler");
|
||||
|
||||
let accounts: Accounts = config.accounts.iter().into();
|
||||
let accounts: Accounts = deserialized_config.accounts.iter().into();
|
||||
trace!("accounts: {:?}", accounts);
|
||||
|
||||
printer.print_table(
|
||||
Box::new(accounts),
|
||||
PrintTableOpts {
|
||||
format: &account_config.format,
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)?;
|
||||
|
@ -37,13 +38,16 @@ pub fn list<'a, P: PrinterService>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::account::{
|
||||
Account, DeserializedAccountConfig, DeserializedConfig, DeserializedImapAccountConfig,
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Debug, io, iter::FromIterator};
|
||||
use himalaya_lib::{AccountConfig, ImapConfig};
|
||||
use std::{collections::HashMap, fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::output::{Print, PrintTable, WriteColor};
|
||||
use crate::{
|
||||
account::{
|
||||
DeserializedAccountConfig, DeserializedBaseAccountConfig, DeserializedImapAccountConfig,
|
||||
},
|
||||
printer::{Print, PrintTable, WriteColor},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -88,7 +92,7 @@ mod tests {
|
|||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl PrinterService for PrinterServiceTest {
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
|
@ -111,21 +115,23 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
let config = DeserializedConfig {
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let config = AccountConfig::default();
|
||||
let deserialized_config = DeserializedConfig {
|
||||
accounts: HashMap::from_iter([(
|
||||
"account-1".into(),
|
||||
DeserializedAccountConfig::Imap(DeserializedImapAccountConfig {
|
||||
base: DeserializedBaseAccountConfig {
|
||||
default: Some(true),
|
||||
..DeserializedImapAccountConfig::default()
|
||||
..DeserializedBaseAccountConfig::default()
|
||||
},
|
||||
backend: ImapConfig::default(),
|
||||
}),
|
||||
)]),
|
||||
..DeserializedConfig::default()
|
||||
};
|
||||
|
||||
let account_config = Account::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
|
||||
assert!(list(None, &config, &account_config, &mut printer).is_ok());
|
||||
assert!(list(None, &config, &deserialized_config, &mut printer).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
9
src/domain/account/mod.rs
Normal file
9
src/domain/account/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
pub mod account;
|
||||
pub mod accounts;
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod handlers;
|
||||
|
||||
pub use account::*;
|
||||
pub use accounts::*;
|
||||
pub use config::*;
|
|
@ -4,17 +4,10 @@
|
|||
|
||||
use anyhow::Result;
|
||||
use clap::{self, App, Arg, ArgMatches, SubCommand};
|
||||
use himalaya_lib::msg::TplOverride;
|
||||
use himalaya_lib::email::TplOverride;
|
||||
use log::{debug, info, trace};
|
||||
|
||||
use crate::{
|
||||
mbox::mbox_args,
|
||||
msg::{
|
||||
flag_args, msg_args,
|
||||
tpl_args::{self, from_args},
|
||||
},
|
||||
ui::table_arg,
|
||||
};
|
||||
use crate::{email, flag, folder, tpl, ui::table};
|
||||
|
||||
type Seq<'a> = &'a str;
|
||||
type PageSize = usize;
|
||||
|
@ -48,8 +41,8 @@ pub enum Cmd<'a> {
|
|||
Send(RawMsg<'a>),
|
||||
Write(TplOverride<'a>, AttachmentPaths<'a>, Encrypt),
|
||||
|
||||
Flag(Option<flag_args::Cmd<'a>>),
|
||||
Tpl(Option<tpl_args::Cmd<'a>>),
|
||||
Flag(Option<flag::args::Cmd<'a>>),
|
||||
Tpl(Option<tpl::args::Cmd<'a>>),
|
||||
}
|
||||
|
||||
/// Message command matcher.
|
||||
|
@ -67,7 +60,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
info!("copy command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
let mbox = m.value_of("folder-target").unwrap();
|
||||
debug!(r#"target mailbox: "{:?}""#, mbox);
|
||||
return Ok(Some(Cmd::Copy(seq, mbox)));
|
||||
}
|
||||
|
@ -113,7 +106,7 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
info!("move command matched");
|
||||
let seq = m.value_of("seq").unwrap();
|
||||
debug!("seq: {}", seq);
|
||||
let mbox = m.value_of("mbox-target").unwrap();
|
||||
let mbox = m.value_of("folder-target").unwrap();
|
||||
debug!("target mailbox: {:?}", mbox);
|
||||
return Ok(Some(Cmd::Move(seq, mbox)));
|
||||
}
|
||||
|
@ -265,16 +258,16 @@ pub fn matches<'a>(m: &'a ArgMatches) -> Result<Option<Cmd<'a>>> {
|
|||
debug!("attachments paths: {:?}", attachment_paths);
|
||||
let encrypt = m.is_present("encrypt");
|
||||
debug!("encrypt: {}", encrypt);
|
||||
let tpl = from_args(m);
|
||||
let tpl = tpl::args::from_args(m);
|
||||
return Ok(Some(Cmd::Write(tpl, attachment_paths, encrypt)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("template") {
|
||||
return Ok(Some(Cmd::Tpl(tpl_args::matches(m)?)));
|
||||
return Ok(Some(Cmd::Tpl(tpl::args::matches(m)?)));
|
||||
}
|
||||
|
||||
if let Some(m) = m.subcommand_matches("flag") {
|
||||
return Ok(Some(Cmd::Flag(flag_args::matches(m)?)));
|
||||
return Ok(Some(Cmd::Flag(flag::args::matches(m)?)));
|
||||
}
|
||||
|
||||
info!("default list command matched");
|
||||
|
@ -356,25 +349,25 @@ pub fn encrypt_arg<'a>() -> Arg<'a, 'a> {
|
|||
/// Message subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
||||
vec![
|
||||
flag_args::subcmds(),
|
||||
tpl_args::subcmds(),
|
||||
flag::args::subcmds(),
|
||||
tpl::args::subcmds(),
|
||||
vec![
|
||||
SubCommand::with_name("attachments")
|
||||
.aliases(&["attachment", "att", "a"])
|
||||
.about("Downloads all message attachments")
|
||||
.arg(msg_args::seq_arg()),
|
||||
.arg(email::args::seq_arg()),
|
||||
SubCommand::with_name("list")
|
||||
.aliases(&["lst", "l"])
|
||||
.about("Lists all messages")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width()),
|
||||
.arg(table::args::max_width()),
|
||||
SubCommand::with_name("search")
|
||||
.aliases(&["s", "query", "q"])
|
||||
.about("Lists messages matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(table::args::max_width())
|
||||
.arg(
|
||||
Arg::with_name("query")
|
||||
.help("IMAP query")
|
||||
|
@ -387,7 +380,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.about("Sorts messages by the given criteria and matching the given IMAP query")
|
||||
.arg(page_size_arg())
|
||||
.arg(page_arg())
|
||||
.arg(table_arg::max_width())
|
||||
.arg(table::args::max_width())
|
||||
.arg(
|
||||
Arg::with_name("criterion")
|
||||
.long("criterion")
|
||||
|
@ -417,7 +410,7 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
),
|
||||
SubCommand::with_name("write")
|
||||
.about("Writes a new message")
|
||||
.args(&tpl_args::tpl_args())
|
||||
.args(&tpl::args::tpl_args())
|
||||
.arg(attachments_arg())
|
||||
.arg(encrypt_arg()),
|
||||
SubCommand::with_name("send")
|
||||
|
@ -462,12 +455,12 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
.aliases(&["cp", "c"])
|
||||
.about("Copies a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name("move")
|
||||
.aliases(&["mv"])
|
||||
.about("Moves a message to the targetted mailbox")
|
||||
.arg(seq_arg())
|
||||
.arg(mbox_args::target_arg()),
|
||||
.arg(folder::args::target_arg()),
|
||||
SubCommand::with_name("delete")
|
||||
.aliases(&["del", "d", "remove", "rm"])
|
||||
.about("Deletes a message")
|
|
@ -5,9 +5,7 @@
|
|||
use anyhow::{Context, Result};
|
||||
use atty::Stream;
|
||||
use himalaya_lib::{
|
||||
account::{Account, DEFAULT_SENT_FOLDER},
|
||||
backend::Backend,
|
||||
msg::{Msg, Part, Parts, TextPlainPart, TplOverride},
|
||||
AccountConfig, Backend, Email, Part, Parts, Sender, TextPlainPart, TplOverride,
|
||||
};
|
||||
use log::{debug, info, trace};
|
||||
use mailparse::addrparse;
|
||||
|
@ -19,31 +17,28 @@ use std::{
|
|||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
output::{PrintTableOpts, PrinterService},
|
||||
smtp::SmtpService,
|
||||
printer::{PrintTableOpts, Printer},
|
||||
ui::editor,
|
||||
};
|
||||
|
||||
/// Downloads all message attachments to the user account downloads directory.
|
||||
pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn attachments<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let attachments = backend.get_msg(mbox, seq)?.attachments();
|
||||
let attachments = backend.email_get(mbox, seq)?.attachments();
|
||||
let attachments_len = attachments.len();
|
||||
|
||||
if attachments_len == 0 {
|
||||
return printer.print_struct(format!("No attachment found for message {:?}", seq));
|
||||
return printer.print_struct(format!("No attachment found for message {}", seq));
|
||||
}
|
||||
|
||||
printer.print_str(format!(
|
||||
"Found {:?} attachment{} for message {:?}",
|
||||
attachments_len,
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
seq
|
||||
"{} attachment(s) found for message {}",
|
||||
attachments_len, seq
|
||||
))?;
|
||||
|
||||
for attachment in attachments {
|
||||
|
@ -53,77 +48,80 @@ pub fn attachments<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
.context(format!("cannot download attachment {:?}", file_path))?;
|
||||
}
|
||||
|
||||
printer.print_struct(format!(
|
||||
"Attachment{} successfully downloaded to {:?}",
|
||||
if attachments_len > 1 { "s" } else { "" },
|
||||
config.downloads_dir
|
||||
))
|
||||
printer.print_struct("Done!")
|
||||
}
|
||||
|
||||
/// Copy a message from a mailbox to another.
|
||||
pub fn copy<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn copy<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.copy_msg(mbox_src, mbox_dst, seq)?;
|
||||
backend.email_copy(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully copied to folder "{}""#,
|
||||
"Message {} successfully copied to folder {}",
|
||||
seq, mbox_dst
|
||||
))
|
||||
}
|
||||
|
||||
/// Delete messages matching the given sequence range.
|
||||
pub fn delete<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn delete<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.del_msg(mbox, seq)?;
|
||||
printer.print_struct(format!(r#"Message(s) {} successfully deleted"#, seq))
|
||||
backend.email_delete(mbox, seq)?;
|
||||
printer.print_struct(format!("Message(s) {} successfully deleted", seq))
|
||||
}
|
||||
|
||||
/// Forward the given message UID from the selected mailbox.
|
||||
pub fn forward<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
pub fn forward<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.email_get(mbox, seq)?
|
||||
.into_forward(config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?;
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List paginated messages from the selected mailbox.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
imap: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = imap.get_envelopes(mbox, page_size, page)?;
|
||||
let msgs = backend.envelope_list(mbox, page_size, page)?;
|
||||
trace!("envelopes: {:?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
|
@ -132,12 +130,12 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
/// Parses and edits a message from a [mailto] URL string.
|
||||
///
|
||||
/// [mailto]: https://en.wikipedia.org/wiki/Mailto
|
||||
pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
pub fn mailto<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
url: &Url,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering mailto command handler");
|
||||
|
||||
|
@ -165,7 +163,7 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
}
|
||||
}
|
||||
|
||||
let msg = Msg {
|
||||
let msg = Email {
|
||||
from: Some(vec![config.address()?].into()),
|
||||
to: if to.is_empty() { None } else { Some(to) },
|
||||
cc: if cc.is_empty() {
|
||||
|
@ -182,23 +180,30 @@ pub fn mailto<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
parts: Parts(vec![Part::TextPlain(TextPlainPart {
|
||||
content: body.into(),
|
||||
})]),
|
||||
..Msg::default()
|
||||
..Email::default()
|
||||
};
|
||||
trace!("message: {:?}", msg);
|
||||
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?;
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Move a message from a mailbox to another.
|
||||
pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn move_<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
mbox_src: &str,
|
||||
mbox_dst: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.move_msg(mbox_src, mbox_dst, seq)?;
|
||||
backend.email_move(mbox_src, mbox_dst, seq)?;
|
||||
printer.print_struct(format!(
|
||||
r#"Message {} successfully moved to folder "{}""#,
|
||||
seq, mbox_dst
|
||||
|
@ -206,17 +211,17 @@ pub fn move_<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
}
|
||||
|
||||
/// Read a message by its sequence number.
|
||||
pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn read<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq: &str,
|
||||
text_mime: &str,
|
||||
raw: bool,
|
||||
headers: Vec<&str>,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let msg = backend.get_msg(mbox, seq)?;
|
||||
let msg = backend.email_get(mbox, seq)?;
|
||||
|
||||
printer.print_struct(if raw {
|
||||
// Emails don't always have valid utf8. Using "lossy" to display what we can.
|
||||
|
@ -227,33 +232,40 @@ pub fn read<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
}
|
||||
|
||||
/// Reply to the given message UID.
|
||||
pub fn reply<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
pub fn reply<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
seq: &str,
|
||||
all: bool,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = backend
|
||||
.get_msg(mbox, seq)?
|
||||
.email_get(mbox, seq)?
|
||||
.into_reply(all, config)?
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, TplOverride::default(), config, printer, backend, smtp)?
|
||||
.add_flags(mbox, seq, "replied")?;
|
||||
editor::edit_msg_with_editor(
|
||||
msg,
|
||||
TplOverride::default(),
|
||||
config,
|
||||
printer,
|
||||
backend,
|
||||
sender,
|
||||
)?;
|
||||
backend.flags_add(mbox, seq, "replied")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Saves a raw message to the targetted mailbox.
|
||||
pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn save<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
mbox: &str,
|
||||
raw_msg: &str,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
info!("entering save message handler");
|
||||
|
||||
|
@ -274,66 +286,66 @@ pub fn save<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
.collect::<Vec<String>>()
|
||||
.join("\r\n")
|
||||
};
|
||||
backend.add_msg(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
backend.email_add(mbox, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Paginate messages from the selected mailbox matching the specified query.
|
||||
pub fn search<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn search<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, "", page_size, page)?;
|
||||
let msgs = backend.envelope_search(mbox, &query, "", page_size, page)?;
|
||||
trace!("messages: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Paginates messages from the selected mailbox matching the specified query, sorted by the given criteria.
|
||||
pub fn sort<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn sort<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
sort: String,
|
||||
query: String,
|
||||
max_width: Option<usize>,
|
||||
page_size: Option<usize>,
|
||||
page: usize,
|
||||
mbox: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
let page_size = page_size.unwrap_or(config.default_page_size);
|
||||
let page_size = page_size.unwrap_or(config.email_listing_page_size());
|
||||
debug!("page size: {}", page_size);
|
||||
let msgs = backend.search_envelopes(mbox, &query, &sort, page_size, page)?;
|
||||
let msgs = backend.envelope_search(mbox, &query, &sort, page_size, page)?;
|
||||
trace!("envelopes: {:#?}", msgs);
|
||||
printer.print_table(
|
||||
Box::new(msgs),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Send a raw message.
|
||||
pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
pub fn send<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
raw_msg: &str,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&mut B>,
|
||||
smtp: &mut S,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
info!("entering send message handler");
|
||||
|
||||
|
@ -342,11 +354,7 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
let is_json = printer.is_json();
|
||||
debug!("is json: {}", is_json);
|
||||
|
||||
let sent_folder = config
|
||||
.mailboxes
|
||||
.get("sent")
|
||||
.map(|s| s.as_str())
|
||||
.unwrap_or(DEFAULT_SENT_FOLDER);
|
||||
let sent_folder = config.folder_alias("sent")?;
|
||||
debug!("sent folder: {:?}", sent_folder);
|
||||
|
||||
let raw_msg = if is_tty || is_json {
|
||||
|
@ -360,25 +368,25 @@ pub fn send<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
|||
.join("\r\n")
|
||||
};
|
||||
trace!("raw message: {:?}", raw_msg);
|
||||
let msg = Msg::from_tpl(&raw_msg)?;
|
||||
smtp.send(&config, &msg)?;
|
||||
backend.add_msg(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
let msg = Email::from_tpl(&raw_msg)?;
|
||||
sender.send(&config, &msg)?;
|
||||
backend.email_add(&sent_folder, raw_msg.as_bytes(), "seen")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Compose a new message.
|
||||
pub fn write<'a, P: PrinterService, B: Backend<'a> + ?Sized, S: SmtpService>(
|
||||
pub fn write<'a, P: Printer, B: Backend<'a> + ?Sized, S: Sender + ?Sized>(
|
||||
tpl: TplOverride,
|
||||
attachments_paths: Vec<&str>,
|
||||
encrypt: bool,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
smtp: &mut S,
|
||||
backend: &mut B,
|
||||
sender: &mut S,
|
||||
) -> Result<()> {
|
||||
let msg = Msg::default()
|
||||
let msg = Email::default()
|
||||
.add_attachments(attachments_paths)?
|
||||
.encrypt(encrypt);
|
||||
editor::edit_msg_with_editor(msg, tpl, config, printer, backend, smtp)?;
|
||||
editor::edit_msg_with_editor(msg, tpl, config, printer, backend, sender)?;
|
||||
Ok(())
|
||||
}
|
2
src/domain/email/mod.rs
Normal file
2
src/domain/email/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -1,4 +1,4 @@
|
|||
use himalaya_lib::msg::{Envelope, Flag};
|
||||
use himalaya_lib::{Envelope, Flag};
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::msg::Envelopes;
|
||||
use himalaya_lib::Envelopes;
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
5
src/domain/envelope/mod.rs
Normal file
5
src/domain/envelope/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub mod envelope;
|
||||
pub mod envelopes;
|
||||
|
||||
pub use envelope::*;
|
||||
pub use envelopes::*;
|
|
@ -7,7 +7,7 @@ use anyhow::Result;
|
|||
use clap::{self, App, AppSettings, Arg, ArgMatches, SubCommand};
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::msg::msg_args;
|
||||
use crate::email;
|
||||
|
||||
type SeqRange<'a> = &'a str;
|
||||
type Flags = String;
|
||||
|
@ -89,21 +89,21 @@ pub fn subcmds<'a>() -> Vec<App<'a, 'a>> {
|
|||
SubCommand::with_name("add")
|
||||
.aliases(&["a"])
|
||||
.about("Adds flags to a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(email::args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("set")
|
||||
.aliases(&["s", "change", "c"])
|
||||
.about("Replaces all message flags")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(email::args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)
|
||||
.subcommand(
|
||||
SubCommand::with_name("remove")
|
||||
.aliases(&["rem", "rm", "r", "delete", "del", "d"])
|
||||
.about("Removes flags from a message")
|
||||
.arg(msg_args::seq_range_arg())
|
||||
.arg(email::args::seq_range_arg())
|
||||
.arg(flags_arg()),
|
||||
)]
|
||||
}
|
|
@ -5,18 +5,18 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::backend::Backend;
|
||||
|
||||
use crate::output::PrinterService;
|
||||
use crate::printer::Printer;
|
||||
|
||||
/// Adds flags to all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
pub fn add<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.add_flags(mbox, seq_range, flags)?;
|
||||
backend.flags_add(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully added to message(s) {:?}",
|
||||
flags, seq_range
|
||||
|
@ -25,14 +25,14 @@ pub fn add<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
|
||||
/// Removes flags from all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
pub fn remove<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.del_flags(mbox, seq_range, flags)?;
|
||||
backend.flags_delete(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully removed from message(s) {:?}",
|
||||
flags, seq_range
|
||||
|
@ -41,14 +41,14 @@ pub fn remove<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
|
||||
/// Replaces flags of all messages matching the given sequence range.
|
||||
/// Flags are case-insensitive, and they do not need to be prefixed with `\`.
|
||||
pub fn set<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &'a str,
|
||||
flags: &'a str,
|
||||
mbox: &'a str,
|
||||
printer: &'a mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
pub fn set<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
seq_range: &str,
|
||||
flags: &str,
|
||||
mbox: &str,
|
||||
printer: &mut P,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
backend.set_flags(mbox, seq_range, flags)?;
|
||||
backend.flags_set(mbox, seq_range, flags)?;
|
||||
printer.print_struct(format!(
|
||||
"Flag(s) {:?} successfully set for message(s) {:?}",
|
||||
flags, seq_range
|
2
src/domain/flag/mod.rs
Normal file
2
src/domain/flag/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -7,7 +7,7 @@ use anyhow::Result;
|
|||
use clap;
|
||||
use log::{debug, info};
|
||||
|
||||
use crate::ui::table_arg;
|
||||
use crate::ui::table;
|
||||
|
||||
type MaxTableWidth = Option<usize>;
|
||||
|
||||
|
@ -37,24 +37,26 @@ pub fn matches(m: &clap::ArgMatches) -> Result<Option<Cmd>> {
|
|||
/// Contains mailbox subcommands.
|
||||
pub fn subcmds<'a>() -> Vec<clap::App<'a, 'a>> {
|
||||
vec![clap::SubCommand::with_name("mailboxes")
|
||||
.aliases(&["mailbox", "mboxes", "mbox", "mb", "m"])
|
||||
.about("Lists mailboxes")
|
||||
.arg(table_arg::max_width())]
|
||||
.aliases(&[
|
||||
"mailbox", "mboxes", "mbox", "mb", "m", "folders", "fold", "fo",
|
||||
])
|
||||
.about("Lists folders")
|
||||
.arg(table::args::max_width())]
|
||||
}
|
||||
|
||||
/// Defines the source mailbox argument.
|
||||
pub fn source_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mbox-source")
|
||||
.short("m")
|
||||
.long("mailbox")
|
||||
.help("Specifies the source mailbox")
|
||||
clap::Arg::with_name("folder-source")
|
||||
.short("f")
|
||||
.long("folder")
|
||||
.help("Specifies the folder source")
|
||||
.value_name("SOURCE")
|
||||
}
|
||||
|
||||
/// Defines the target mailbox argument.
|
||||
pub fn target_arg<'a>() -> clap::Arg<'a, 'a> {
|
||||
clap::Arg::with_name("mbox-target")
|
||||
.help("Specifies the targeted mailbox")
|
||||
clap::Arg::with_name("folder-target")
|
||||
.help("Specifies the folder target")
|
||||
.value_name("TARGET")
|
||||
.required(true)
|
||||
}
|
||||
|
@ -105,13 +107,13 @@ mod tests {
|
|||
}
|
||||
|
||||
let app = get_matches_from![];
|
||||
assert_eq!(None, app.value_of("mbox-source"));
|
||||
assert_eq!(None, app.value_of("folder-source"));
|
||||
|
||||
let app = get_matches_from!["-m", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
|
||||
assert_eq!(Some("SOURCE"), app.value_of("folder-source"));
|
||||
|
||||
let app = get_matches_from!["--mailbox", "SOURCE"];
|
||||
assert_eq!(Some("SOURCE"), app.value_of("mbox-source"));
|
||||
assert_eq!(Some("SOURCE"), app.value_of("folder-source"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -131,6 +133,6 @@ mod tests {
|
|||
);
|
||||
|
||||
let app = get_matches_from!["TARGET"];
|
||||
assert_eq!(Some("TARGET"), app.unwrap().value_of("mbox-target"));
|
||||
assert_eq!(Some("TARGET"), app.unwrap().value_of("folder-target"));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
use himalaya_lib::mbox::Mbox;
|
||||
use himalaya_lib::folder::Folder;
|
||||
|
||||
use crate::ui::{Cell, Row, Table};
|
||||
|
||||
impl Table for Mbox {
|
||||
impl Table for Folder {
|
||||
fn head() -> Row {
|
||||
Row::new()
|
||||
.cell(Cell::new("DELIM").bold().underline().white())
|
|
@ -1,12 +1,12 @@
|
|||
use anyhow::Result;
|
||||
use himalaya_lib::mbox::Mboxes;
|
||||
use himalaya_lib::folder::Folders;
|
||||
|
||||
use crate::{
|
||||
output::{PrintTable, PrintTableOpts, WriteColor},
|
||||
printer::{PrintTable, PrintTableOpts, WriteColor},
|
||||
ui::Table,
|
||||
};
|
||||
|
||||
impl PrintTable for Mboxes {
|
||||
impl PrintTable for Folders {
|
||||
fn print_table(&self, writer: &mut dyn WriteColor, opts: PrintTableOpts) -> Result<()> {
|
||||
writeln!(writer)?;
|
||||
Table::print(writer, self, opts)?;
|
|
@ -3,26 +3,26 @@
|
|||
//! This module gathers all mailbox actions triggered by the CLI.
|
||||
|
||||
use anyhow::Result;
|
||||
use himalaya_lib::{account::Account, backend::Backend};
|
||||
use himalaya_lib::{AccountConfig, Backend};
|
||||
use log::{info, trace};
|
||||
|
||||
use crate::output::{PrintTableOpts, PrinterService};
|
||||
use crate::printer::{PrintTableOpts, Printer};
|
||||
|
||||
/// Lists all mailboxes.
|
||||
pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
||||
pub fn list<'a, P: Printer, B: Backend<'a> + ?Sized>(
|
||||
max_width: Option<usize>,
|
||||
config: &Account,
|
||||
config: &AccountConfig,
|
||||
printer: &mut P,
|
||||
backend: Box<&'a mut B>,
|
||||
backend: &mut B,
|
||||
) -> Result<()> {
|
||||
info!("entering list mailbox handler");
|
||||
let mboxes = backend.get_mboxes()?;
|
||||
let mboxes = backend.folder_list()?;
|
||||
trace!("mailboxes: {:?}", mboxes);
|
||||
printer.print_table(
|
||||
// TODO: remove Box
|
||||
Box::new(mboxes),
|
||||
PrintTableOpts {
|
||||
format: &config.format,
|
||||
format: &config.email_reading_format,
|
||||
max_width,
|
||||
},
|
||||
)
|
||||
|
@ -30,15 +30,11 @@ pub fn list<'a, P: PrinterService, B: Backend<'a> + ?Sized>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use himalaya_lib::{
|
||||
backend::{backend, Backend},
|
||||
mbox::{Mbox, Mboxes},
|
||||
msg::{Envelopes, Msg},
|
||||
};
|
||||
use himalaya_lib::{backend, AccountConfig, Backend, Email, Envelopes, Folder, Folders};
|
||||
use std::{fmt::Debug, io};
|
||||
use termcolor::ColorSpec;
|
||||
|
||||
use crate::output::{Print, PrintTable, WriteColor};
|
||||
use crate::printer::{Print, PrintTable, WriteColor};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -83,7 +79,7 @@ mod tests {
|
|||
pub writer: StringWriter,
|
||||
}
|
||||
|
||||
impl PrinterService for PrinterServiceTest {
|
||||
impl Printer for PrinterServiceTest {
|
||||
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
|
||||
&mut self,
|
||||
data: Box<T>,
|
||||
|
@ -109,18 +105,18 @@ mod tests {
|
|||
struct TestBackend;
|
||||
|
||||
impl<'a> Backend<'a> for TestBackend {
|
||||
fn add_mbox(&mut self, _: &str) -> backend::Result<()> {
|
||||
fn folder_add(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_mboxes(&mut self) -> backend::Result<Mboxes> {
|
||||
Ok(Mboxes {
|
||||
mboxes: vec![
|
||||
Mbox {
|
||||
fn folder_list(&mut self) -> backend::Result<Folders> {
|
||||
Ok(Folders {
|
||||
folders: vec![
|
||||
Folder {
|
||||
delim: "/".into(),
|
||||
name: "INBOX".into(),
|
||||
desc: "desc".into(),
|
||||
},
|
||||
Mbox {
|
||||
Folder {
|
||||
delim: "/".into(),
|
||||
name: "Sent".into(),
|
||||
desc: "desc".into(),
|
||||
|
@ -128,13 +124,13 @@ mod tests {
|
|||
],
|
||||
})
|
||||
}
|
||||
fn del_mbox(&mut self, _: &str) -> backend::Result<()> {
|
||||
fn folder_delete(&mut self, _: &str) -> backend::Result<()> {
|
||||
unimplemented!();
|
||||
}
|
||||
fn get_envelopes(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
fn envelope_list(&mut self, _: &str, _: usize, _: usize) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn search_envelopes(
|
||||
fn envelope_search(
|
||||
&mut self,
|
||||
_: &str,
|
||||
_: &str,
|
||||
|
@ -144,38 +140,40 @@ mod tests {
|
|||
) -> backend::Result<Envelopes> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_msg(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
|
||||
fn email_add(&mut self, _: &str, _: &[u8], _: &str) -> backend::Result<String> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn get_msg(&mut self, _: &str, _: &str) -> backend::Result<Msg> {
|
||||
fn email_list(&mut self, _: &str, _: &str) -> backend::Result<Email> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn copy_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn email_get(&mut self, _: &str, _: &str) -> backend::Result<Email> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn move_msg(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn email_copy(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_msg(&mut self, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn email_move(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn add_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn email_delete(&mut self, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn set_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn flags_add(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn del_flags(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
fn flags_set(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn flags_delete(&mut self, _: &str, _: &str, _: &str) -> backend::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
let config = Account::default();
|
||||
let account_config = AccountConfig::default();
|
||||
let mut printer = PrinterServiceTest::default();
|
||||
let mut backend = TestBackend {};
|
||||
let backend = Box::new(&mut backend);
|
||||
|
||||
assert!(list(None, &config, &mut printer, backend).is_ok());
|
||||
assert!(list(None, &account_config, &mut printer, &mut backend).is_ok());
|
||||
assert_eq!(
|
||||
concat![
|
||||
"\n",
|
8
src/domain/folder/mod.rs
Normal file
8
src/domain/folder/mod.rs
Normal file
|
@ -0,0 +1,8 @@
|
|||
pub mod folder;
|
||||
pub use folder::*;
|
||||
|
||||
pub mod folders;
|
||||
pub use folders::*;
|
||||
|
||||
pub mod args;
|
||||
pub mod handlers;
|
|
@ -3,7 +3,7 @@
|
|||
//! This module gathers all IMAP handlers triggered by the CLI.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use himalaya_lib::backend::ImapBackend;
|
||||
use himalaya_lib::ImapBackend;
|
||||
|
||||
pub fn notify(keepalive: u64, mbox: &str, imap: &mut ImapBackend) -> Result<()> {
|
||||
imap.notify(keepalive, mbox).context("cannot imap notify")
|
2
src/domain/imap/mod.rs
Normal file
2
src/domain/imap/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod args;
|
||||
pub mod handlers;
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue