aboutsummaryrefslogtreecommitdiffstats
path: root/notmuch
diff options
context:
space:
mode:
authorGustav Sörnäs <gustav@sornas.net>2021-04-25 16:58:17 +0200
committerGustav Sörnäs <gustav@sornas.net>2021-04-25 16:58:17 +0200
commit72c2b83748a601d02c82d5bcb4220d3b238281cc (patch)
tree38acb17db4a48fc9f340680ea5b86c6cd49d8f2a /notmuch
parent51fa75397dda2c280f29760e7b525caefc03642e (diff)
parent2231a5cf6cdeb90c1cdb75d161a0063d4a385576 (diff)
downloadmail-72c2b83748a601d02c82d5bcb4220d3b238281cc.tar.gz
Add 'notmuch/' from commit '2231a5cf6cdeb90c1cdb75d161a0063d4a385576'
git-subtree-dir: notmuch git-subtree-mainline: 51fa75397dda2c280f29760e7b525caefc03642e git-subtree-split: 2231a5cf6cdeb90c1cdb75d161a0063d4a385576
Diffstat (limited to 'notmuch')
-rw-r--r--notmuch/.gitignore2
-rw-r--r--notmuch/.travis.yml29
-rw-r--r--notmuch/Cargo.toml37
-rw-r--r--notmuch/LICENSE.md675
-rw-r--r--notmuch/README.md100
-rw-r--r--notmuch/src/config_list.rs58
-rw-r--r--notmuch/src/database.rs470
-rw-r--r--notmuch/src/directory.rs57
-rw-r--r--notmuch/src/error.rs55
-rw-r--r--notmuch/src/ffi.rs1852
-rw-r--r--notmuch/src/filenames.rs68
-rw-r--r--notmuch/src/index.rs43
-rw-r--r--notmuch/src/lib.rs43
-rw-r--r--notmuch/src/macros.rs35
-rw-r--r--notmuch/src/message.rs329
-rw-r--r--notmuch/src/message_properties.rs73
-rw-r--r--notmuch/src/messages.rs136
-rw-r--r--notmuch/src/query.rs132
-rw-r--r--notmuch/src/tags.rs85
-rw-r--r--notmuch/src/thread.rs148
-rw-r--r--notmuch/src/threads.rs76
-rw-r--r--notmuch/src/utils.rs61
-rw-r--r--notmuch/tests/fixtures.rs198
-rw-r--r--notmuch/tests/lib.rs15
-rw-r--r--notmuch/tests/main.rs62
-rw-r--r--notmuch/tests/test_database.rs339
-rw-r--r--notmuch/tests/test_message.rs291
-rw-r--r--notmuch/tests/test_query.rs97
-rw-r--r--notmuch/tests/test_tags.rs142
-rw-r--r--notmuch/tests/test_thread.rs102
30 files changed, 5810 insertions, 0 deletions
diff --git a/notmuch/.gitignore b/notmuch/.gitignore
new file mode 100644
index 0000000..80faede
--- /dev/null
+++ b/notmuch/.gitignore
@@ -0,0 +1,2 @@
+Cargo.lock
+/target/
diff --git a/notmuch/.travis.yml b/notmuch/.travis.yml
new file mode 100644
index 0000000..26d147c
--- /dev/null
+++ b/notmuch/.travis.yml
@@ -0,0 +1,29 @@
+dist: bionic
+language: rust
+sudo: required
+rust:
+ - stable
+ - beta
+ - nightly
+
+# Cache cargo symbols for faster build
+cache: cargo
+
+addons:
+ apt:
+ packages:
+ - libnotmuch-dev
+ - notmuch
+ - git
+
+before_script:
+ - export PATH=$HOME/.cargo/bin:$PATH
+ - cargo install cargo-update || echo "cargo-update already installed"
+ - cargo install cargo-travis || echo "cargo-travis already installed"
+ - cargo install-update -a # update outdated cached binaries
+
+script:
+ - cargo build --no-default-features --verbose --all
+ # clone notmuch to have mail corpora
+ - git clone git://git.notmuchmail.org/git/notmuch /tmp/notmuch
+ - cargo test --no-default-features --verbose --all
diff --git a/notmuch/Cargo.toml b/notmuch/Cargo.toml
new file mode 100644
index 0000000..48e7415
--- /dev/null
+++ b/notmuch/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "notmuch"
+version = "0.6.0"
+authors = ["Dirk Van Haerenborgh <vhdirk@gmail.com>"]
+homepage = "https://github.com/vhdirk/notmuch-rs"
+repository = "https://github.com/vhdirk/notmuch-rs"
+description = "Rust interface and bindings for notmuch"
+license = "GPL-3.0+"
+readme = "README.md"
+keywords = ["email", "notmuch"]
+autotests = false
+
+[badges]
+travis-ci = { repository = "vhdirk/notmuch-rs" }
+
+[dependencies]
+libc = "0.2"
+# clippy = { version = "0.0.211", optional = true }
+supercow = "0.1.0"
+
+[dev-dependencies]
+dirs = "1.0"
+tempfile = "3"
+gethostname = "0.2.0"
+maildir = "0.3.2"
+lettre = "0.9.2"
+lettre_email = "0.9.2"
+
+[features]
+v0_21 = []
+v0_26 = ["v0_21"]
+default = ["v0_26"]
+
+[[test]]
+name = "tests"
+path = "tests/lib.rs"
+harness = true \ No newline at end of file
diff --git a/notmuch/LICENSE.md b/notmuch/LICENSE.md
new file mode 100644
index 0000000..2fb2e74
--- /dev/null
+++ b/notmuch/LICENSE.md
@@ -0,0 +1,675 @@
+### GNU GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 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 General Public License is a free, copyleft license for
+software and other kinds of works.
+
+The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is 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. We, the Free Software Foundation, use
+the GNU General Public License for most of our software; it applies
+also to any other work released this way by its authors. You can apply
+it to your programs, too.
+
+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.
+
+To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you
+have certain responsibilities if you distribute copies of the
+software, or if you modify it: responsibilities to respect the freedom
+of others.
+
+For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the
+manufacturer can do so. This is fundamentally incompatible with the
+aim of protecting users' freedom to change the software. The
+systematic pattern of such abuse occurs in the area of products for
+individuals to use, which is precisely where it is most unacceptable.
+Therefore, we have designed this version of the GPL to prohibit the
+practice for those products. If such problems arise substantially in
+other domains, we stand ready to extend this provision to those
+domains in future versions of the GPL, as needed to protect the
+freedom of users.
+
+Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish
+to avoid the special danger that patents applied to a free program
+could make it effectively proprietary. To prevent this, the GPL
+assures that patents cannot be used to render the program non-free.
+
+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 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. Use with the GNU Affero General Public License.
+
+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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+#### 14. Revised Versions of this License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU 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 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 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 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 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 General Public License for more details.
+
+ You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands \`show w' and \`show c' should show the
+appropriate parts of the General Public License. Of course, your
+program's commands might be different; for a GUI interface, you would
+use an "about box".
+
+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 GPL, see <https://www.gnu.org/licenses/>.
+
+The GNU General Public License does not permit incorporating your
+program into proprietary programs. If your program is a subroutine
+library, you may consider it more useful to permit linking proprietary
+applications with the library. If this is what you want to do, use the
+GNU Lesser General Public License instead of this License. But first,
+please read <https://www.gnu.org/licenses/why-not-lgpl.html>.
diff --git a/notmuch/README.md b/notmuch/README.md
new file mode 100644
index 0000000..1fcbddf
--- /dev/null
+++ b/notmuch/README.md
@@ -0,0 +1,100 @@
+# notmuch-rs
+
+This is not much more than a wrapper for the [notmuch](https://notmuchmail.org/) C api.
+
+[![Build Status](https://travis-ci.org/vhdirk/notmuch-rs.svg?branch=master)](https://travis-ci.org/vhdirk/notmuch-rs)
+[![Crate version](https://img.shields.io/crates/v/notmuch.svg)](https://crates.io/crates/notmuch)
+[![Download statistics](https://img.shields.io/crates/d/notmuch.svg)](https://crates.io/crates/notmuch)
+[![License](https://img.shields.io/crates/l/notmuch.svg)](https://crates.io/crates/notmuch) [![Join the chat at https://gitter.im/notmuch-rs/Lobby](https://badges.gitter.im/notmuch-rs/Lobby.svg)](https://gitter.im/notmuch-rs/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+## Building
+
+**notmuch-rs** expects libnotmuch development files to be installed on your system.
+
+## Using
+
+Add this to your `Cargo.toml`:
+
+```toml
+[dependencies]
+notmuch = "*"
+```
+
+and this to your crate root:
+
+```rust
+extern crate notmuch;
+```
+
+## Example
+
+```rust
+extern crate notmuch;
+
+
+fn main() {
+
+ let mut mail_path = std::env::home_dir().unwrap();
+ mail_path.push(".mail");
+
+ let db = notmuch::Database::open(&mail_path, notmuch::DatabaseMode::ReadOnly).unwrap();
+ let query = db.create_query("").unwrap();
+ let mut threads = query.search_threads().unwrap();
+
+ for thread in threads {
+ println!("thread {:?} {:?}", thread.subject(), thread.authors());
+ }
+}
+
+```
+
+## Concurrency
+
+Notmuch makes no claims regarding thread safety. It does not seem to use any
+thread locals, but I did not spot any locks. So, as far as I am concerned, it is
+not thread safe.
+So why do all structs implement ```Send``` and ```Sync```? Well, it _is_ safe to
+access pointers from different threads (as long as you know what you are doing :) ).
+Up till now I haven't done a lot of multithreaded stuff with notmuch-rs. If you
+feel this is too permissive, let me know.
+
+## Lifetime
+
+All structs are strictly linked together with their lifetime. The root of the
+tree is ```Database```, which has a lifetime that must outlive any child
+objects, for instance ```Query```. The ```Threads``` iterator that you can get
+from a ```Query``` is always outlived by the parent query. The ```Threads```
+does not own any individual ```Thread```. These are bound to the owner of
+the ```Threads``` iterator itself. Each structure keeps a ```PhantomCow```
+marker for its owner.
+
+Typically, using a lifetimes structure like this in an application poses
+significant difficulties in satisfying these lifetime requirements. While other
+libraries force the application developers towards crates like ```owningref```
+or ```rental``` to get around this, ```notmuch-rs``` makes use of the
+excellent [Supercow](https://crates.io/crates/supercow), to alleviate this.
+
+This way, you get to choose your own container type, and even keep the parent
+object alive so you don't have to juggle lifetimes. To use this, most types
+are accompagnied with an ```*Ext``` trait, that accepts ```Rc```, ```Arc``` or
+comparable.
+
+```rust
+ use std::sync::Arc;
+ use notmuch::{DatabaseExt};
+
+ let query = {
+ let dbr = Arc::new(db);
+
+ <Database as DatabaseExt>::create_query(dbr.clone(), &"".to_string()).unwrap()
+ };
+
+```
+
+## Acknowledgements
+
+notmuch-rs started out from the following projects:
+ - https://github.com/Stebalien/notmuch-sys/blob/master/src/lib.rs
+ - https://github.com/cmhamill/rust-notmuch
+
+Any contributions are welcome!
diff --git a/notmuch/src/config_list.rs b/notmuch/src/config_list.rs
new file mode 100644
index 0000000..2c1a6a4
--- /dev/null
+++ b/notmuch/src/config_list.rs
@@ -0,0 +1,58 @@
+use std::ops::Drop;
+
+use ffi;
+use Database;
+use utils::{ToStr, ScopedPhantomcow};
+
+
+#[derive(Debug)]
+pub struct ConfigList<'d> {
+ ptr: *mut ffi::notmuch_config_list_t,
+ marker: ScopedPhantomcow<'d, Database>,
+}
+
+impl<'d> Drop for ConfigList<'d> {
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_config_list_destroy(self.ptr) };
+ }
+}
+
+impl<'d> ConfigList<'d> {
+ pub(crate) fn from_ptr<O>(ptr: *mut ffi::notmuch_config_list_t, owner: O) -> ConfigList<'d>
+ where
+ O: Into<ScopedPhantomcow<'d, Database>>,
+ {
+ ConfigList {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+
+impl<'d> Iterator for ConfigList<'d>
+{
+ type Item = (String, String);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_config_list_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let (k, v) = unsafe {
+ let key = ffi::notmuch_config_list_key(self.ptr);
+ let value = ffi::notmuch_config_list_value(self.ptr);
+
+ ffi::notmuch_config_list_move_to_next(self.ptr);
+
+ (key, value)
+ };
+
+ Some((k.to_string_lossy().to_string(), v.to_string_lossy().to_string()))
+ }
+}
+
+unsafe impl<'d> Send for ConfigList<'d> {}
+unsafe impl<'d> Sync for ConfigList<'d> {}
diff --git a/notmuch/src/database.rs b/notmuch/src/database.rs
new file mode 100644
index 0000000..44ad040
--- /dev/null
+++ b/notmuch/src/database.rs
@@ -0,0 +1,470 @@
+use std::ffi::{CStr, CString};
+use std::ops::Drop;
+use std::path::Path;
+use std::ptr;
+
+use supercow::Supercow;
+
+use libc;
+use std::cmp::{PartialEq, PartialOrd, Ordering};
+
+use error::{Error, Result};
+use ffi;
+use ffi::Status;
+use utils::ToStr;
+use Directory;
+use Query;
+use Tags;
+use TagsOwner;
+use Message;
+use MessageOwner;
+use IndexOpts;
+use ConfigList;
+use utils::ScopedSupercow;
+
+
+// Re-exported under database module for pretty namespacin'.
+pub use ffi::DatabaseMode;
+
+
+#[derive(Clone, Debug)]
+pub struct Revision {
+ pub revision: libc::c_ulong,
+ pub uuid: String,
+}
+
+impl PartialEq for Revision {
+ fn eq(&self, other: &Revision) -> bool{
+ self.uuid == other.uuid && self.revision == other.revision
+ }
+}
+
+impl PartialOrd for Revision {
+ fn partial_cmp(&self, other: &Revision) -> Option<Ordering>{
+ if self.uuid != other.uuid {
+ return None;
+ }
+ self.revision.partial_cmp(&other.revision)
+ }
+}
+
+
+#[derive(Debug)]
+pub struct Database {
+ pub(crate) ptr: *mut ffi::notmuch_database_t,
+}
+
+impl Drop for Database {
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_database_destroy(self.ptr) };
+ }
+}
+
+impl TagsOwner for Database {}
+impl MessageOwner for Database {}
+
+impl Database {
+ pub fn create<P>(path: &P) -> Result<Self>
+ where
+ P: AsRef<Path>,
+ {
+ let path_str = CString::new(path.as_ref().to_str().unwrap()).unwrap();
+
+ let mut db = ptr::null_mut();
+ unsafe { ffi::notmuch_database_create(path_str.as_ptr(), &mut db) }.as_result()?;
+
+ Ok(Database {
+ ptr: db,
+ })
+ }
+
+ pub fn open<P>(path: &P, mode: DatabaseMode) -> Result<Self>
+ where
+ P: AsRef<Path>,
+ {
+ let path_str = CString::new(path.as_ref().to_str().unwrap()).unwrap();
+
+ let mut db = ptr::null_mut();
+ unsafe { ffi::notmuch_database_open(path_str.as_ptr(), mode.into(), &mut db) }
+ .as_result()?;
+
+ Ok(Database {
+ ptr: db,
+ })
+ }
+
+ pub fn close(&self) -> Result<()> {
+ unsafe { ffi::notmuch_database_close(self.ptr) }.as_result()?;
+
+ Ok(())
+ }
+
+ pub fn compact<P, F>(path: &P, backup_path: Option<&P>) -> Result<()>
+ where
+ P: AsRef<Path>,
+ F: FnMut(&str),
+ {
+ let status: Option<F> = None;
+ Database::_compact(path, backup_path, status)
+ }
+
+ pub fn compact_with_status<P, F>(path: &P, backup_path: Option<&P>, status: F) -> Result<()>
+ where
+ P: AsRef<Path>,
+ F: FnMut(&str),
+ {
+ Database::_compact(path, backup_path, Some(status))
+ }
+
+ fn _compact<P, F>(path: &P, backup_path: Option<&P>, status: Option<F>) -> Result<()>
+ where
+ P: AsRef<Path>,
+ F: FnMut(&str),
+ {
+ extern "C" fn wrapper<F: FnMut(&str)>(
+ message: *const libc::c_char,
+ closure: *mut libc::c_void,
+ ) {
+ let closure = closure as *mut F;
+ unsafe { (*closure)(message.to_str().unwrap()) }
+ }
+
+ let path_str = CString::new(path.as_ref().to_str().unwrap()).unwrap();
+
+ let backup_path = backup_path.map(|p| CString::new(p.as_ref().to_str().unwrap()).unwrap());
+
+ unsafe {
+ ffi::notmuch_database_compact(
+ path_str.as_ptr(),
+ backup_path.map_or(ptr::null(), |p| p.as_ptr()),
+ if status.is_some() {
+ Some(wrapper::<F>)
+ } else {
+ None
+ },
+ status.map_or(ptr::null_mut(), |f| &f as *const _ as *mut libc::c_void),
+ )
+ }.as_result()?;
+
+ Ok(())
+ }
+
+ pub fn path(&self) -> &Path {
+ Path::new(
+ unsafe { ffi::notmuch_database_get_path(self.ptr) }
+ .to_str()
+ .unwrap(),
+ )
+ }
+
+ pub fn version(&self) -> u32 {
+ unsafe { ffi::notmuch_database_get_version(self.ptr) }
+ }
+
+ #[cfg(feature = "v0_21")]
+ pub fn revision(&self) -> Revision {
+ let uuid_p: *const libc::c_char = ptr::null();
+ let revision = unsafe {
+ ffi::notmuch_database_get_revision(
+ self.ptr,
+ (&uuid_p) as *const _ as *mut *const libc::c_char,
+ )
+ };
+
+ let uuid = unsafe { CStr::from_ptr(uuid_p) };
+
+ Revision {
+ revision,
+ uuid: uuid.to_string_lossy().into_owned(),
+ }
+ }
+
+ pub fn needs_upgrade(&self) -> bool {
+ unsafe { ffi::notmuch_database_needs_upgrade(self.ptr) == 1 }
+ }
+
+ pub fn upgrade<F>(&mut self) -> Result<()>
+ where
+ F: FnMut(f64),
+ {
+ let status: Option<F> = None;
+ self._upgrade(status)
+ }
+
+ pub fn upgrade_with_status<F>(&mut self, status: F) -> Result<()>
+ where
+ F: FnMut(f64),
+ {
+ self._upgrade(Some(status))
+ }
+
+ fn _upgrade<F>(&mut self, status: Option<F>) -> Result<()>
+ where
+ F: FnMut(f64),
+ {
+ #[allow(trivial_numeric_casts)]
+ extern "C" fn wrapper<F>(closure: *mut libc::c_void, progress: libc::c_double)
+ where
+ F: FnMut(f64),
+ {
+ let closure = closure as *mut F;
+ unsafe { (*closure)(progress as f64) }
+ }
+
+ unsafe {
+ ffi::notmuch_database_upgrade(
+ self.ptr,
+ if status.is_some() {
+ Some(wrapper::<F>)
+ } else {
+ None
+ },
+ status.map_or(ptr::null_mut(), |f| &f as *const _ as *mut libc::c_void),
+ )
+ }.as_result()?;
+
+ Ok(())
+ }
+
+ pub fn directory<'d, P>(&'d self, path: &P) -> Result<Option<Directory<'d>>>
+ where
+ P: AsRef<Path>,
+ {
+ <Self as DatabaseExt>::directory(self, path)
+ }
+
+ pub fn config_list<'d>(&'d self, prefix: &str) -> Result<ConfigList<'d>>
+ {
+ <Self as DatabaseExt>::config_list(self, prefix)
+ }
+
+ pub fn create_query<'d>(&'d self, query_string: &str) -> Result<Query<'d>> {
+ <Self as DatabaseExt>::create_query(self, query_string)
+ }
+
+ pub fn all_tags<'d>(&'d self) -> Result<Tags<'d, Self>> {
+ <Self as DatabaseExt>::all_tags(self)
+ }
+
+ pub fn find_message<'d>(&'d self, message_id: &str) -> Result<Option<Message<'d, Self>>> {
+ <Self as DatabaseExt>::find_message(self, message_id)
+ }
+
+ pub fn find_message_by_filename<'d, P>(&'d self, filename: &P) -> Result<Option<Message<'d, Self>>>
+ where
+ P: AsRef<Path>,
+ {
+ <Self as DatabaseExt>::find_message_by_filename(self, filename)
+ }
+
+ pub fn remove_message<'d, P>(&'d self, path: &P) -> Result<()>
+ where
+ P: AsRef<Path>,
+ {
+ <Self as DatabaseExt>::remove_message(self, path)
+ }
+
+ pub fn default_indexopts<'d, P>(&'d self) -> Result<IndexOpts<'d>>
+ {
+ <Self as DatabaseExt>::default_indexopts(self)
+ }
+
+ pub fn index_file<'d, P>(&'d self, path: &P, indexopts: Option<IndexOpts<'d>>) -> Result<Message<'d, Self>>
+ where
+ P: AsRef<Path>,
+ {
+ <Self as DatabaseExt>::index_file(self, path, indexopts)
+ }
+
+ pub fn begin_atomic(&self) -> Result<()> {
+ unsafe { ffi::notmuch_database_begin_atomic(self.ptr) }.as_result()
+ }
+
+ pub fn end_atomic(&self) -> Result<()> {
+ unsafe { ffi::notmuch_database_end_atomic(self.ptr) }.as_result()
+ }
+}
+
+pub trait DatabaseExt {
+ fn create_query<'d, D>(database: D, query_string: &str) -> Result<Query<'d>>
+ where
+ D: Into<Supercow<'d, Database>>,
+ {
+ let dbref = database.into();
+ let query_str = CString::new(query_string).unwrap();
+
+ let query = unsafe { ffi::notmuch_query_create(dbref.ptr, query_str.as_ptr()) };
+
+ Ok(Query::from_ptr(query, Supercow::phantom(dbref)))
+ }
+
+ fn all_tags<'d, D>(database: D) -> Result<Tags<'d, Database>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ {
+ let dbref = database.into();
+
+ let tags = unsafe { ffi::notmuch_database_get_all_tags(dbref.ptr) };
+
+ Ok(Tags::from_ptr(tags, ScopedSupercow::phantom(dbref)))
+ }
+
+ fn directory<'d, D, P>(database: D, path: &P) -> Result<Option<Directory<'d>>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ P: AsRef<Path>,
+ {
+ let dbref = database.into();
+
+ let path_str = CString::new(path.as_ref().to_str().unwrap()).unwrap();
+
+ let mut dir = ptr::null_mut();
+ unsafe {
+ ffi::notmuch_database_get_directory(dbref.ptr, path_str.as_ptr(), &mut dir)
+ }.as_result()?;
+
+ if dir.is_null() {
+ Ok(None)
+ } else {
+ Ok(Some(Directory::from_ptr(dir, Supercow::phantom(dbref))))
+ }
+ }
+
+ fn config_list<'d, D>(database: D, prefix: &str) -> Result<ConfigList<'d>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>
+ {
+ let dbref = database.into();
+
+ let prefix_str = CString::new(prefix).unwrap();
+
+ let mut cfgs = ptr::null_mut();
+ unsafe {
+ ffi::notmuch_database_get_config_list(dbref.ptr, prefix_str.as_ptr(), &mut cfgs)
+ }.as_result()?;
+
+ Ok(ConfigList::from_ptr(cfgs, Supercow::phantom(dbref)))
+ }
+
+ fn find_message<'d, D>(database: D, message_id: &str) -> Result<Option<Message<'d, Database>>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>
+ {
+ let dbref = database.into();
+ let message_id_str = CString::new(message_id).unwrap();
+
+ let mut msg = ptr::null_mut();
+ unsafe {
+ ffi::notmuch_database_find_message(dbref.ptr, message_id_str.as_ptr(), &mut msg)
+ }.as_result()?;
+
+ if msg.is_null() {
+ Ok(None)
+ } else {
+ Ok(Some(Message::from_ptr(msg, Supercow::phantom(dbref))))
+ }
+ }
+
+ fn find_message_by_filename<'d, D, P>(database: D, filename: &P) -> Result<Option<Message<'d, Database>>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ P: AsRef<Path>
+ {
+ let dbref = database.into();
+ let path_str = CString::new(filename.as_ref().to_str().unwrap()).unwrap();
+
+ let mut msg = ptr::null_mut();
+ unsafe {
+ ffi::notmuch_database_find_message_by_filename(dbref.ptr, path_str.as_ptr(), &mut msg)
+ }.as_result()?;
+
+ if msg.is_null() {
+ Ok(None)
+ } else {
+ Ok(Some(Message::from_ptr(msg, Supercow::phantom(dbref))))
+ }
+ }
+
+ fn remove_message<'d, D, P>(database: D, path: &P) -> Result<()>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ P: AsRef<Path>,
+ {
+ let dbref = database.into();
+ match path.as_ref().to_str() {
+ Some(path_str) => {
+ let msg_path = CString::new(path_str).unwrap();
+
+ unsafe { ffi::notmuch_database_remove_message(dbref.ptr, msg_path.as_ptr()) }
+ .as_result()
+ }
+ None => Err(Error::NotmuchError(Status::FileError)),
+ }
+ }
+
+ fn default_indexopts<'d, D>(database: D) -> Result<IndexOpts<'d>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>
+ {
+ let dbref = database.into();
+
+ let opts = unsafe { ffi::notmuch_database_get_default_indexopts(dbref.ptr) };
+
+ Ok(IndexOpts::from_ptr(opts, ScopedSupercow::phantom(dbref)))
+ }
+
+
+ fn index_file<'d, D, P>(database: D, path: &P, indexopts: Option<IndexOpts<'d>>) -> Result<Message<'d, Database>>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ P: AsRef<Path>,
+ {
+ let dbref = database.into();
+
+ let opts = indexopts.map_or(ptr::null_mut(), |opt| opt.ptr);
+
+ match path.as_ref().to_str() {
+ Some(path_str) => {
+ let msg_path = CString::new(path_str).unwrap();
+
+ let mut msg = ptr::null_mut();
+ unsafe { ffi::notmuch_database_index_file(dbref.ptr, msg_path.as_ptr(), opts, &mut msg) }
+ .as_result()?;
+
+ Ok(Message::from_ptr(msg, ScopedSupercow::phantom(dbref)))
+ }
+ None => Err(Error::NotmuchError(Status::FileError)),
+ }
+ }
+}
+
+impl DatabaseExt for Database {}
+
+unsafe impl Send for Database {}
+unsafe impl Sync for Database {}
+
+
+#[derive(Debug)]
+pub struct AtomicOperation<'d> {
+ database: ScopedSupercow<'d, Database>,
+}
+
+impl<'d> AtomicOperation<'d> {
+ pub fn new<D>(db: D) -> Result<Self>
+ where
+ D: Into<ScopedSupercow<'d, Database>>,
+ {
+ let database = db.into();
+ database.begin_atomic()?;
+ Ok(AtomicOperation{
+ database
+ })
+ }
+}
+
+impl<'d> Drop for AtomicOperation<'d> {
+ fn drop(&mut self) {
+ let _ = self.database.end_atomic();
+ }
+}
+
diff --git a/notmuch/src/directory.rs b/notmuch/src/directory.rs
new file mode 100644
index 0000000..1c8a3df
--- /dev/null
+++ b/notmuch/src/directory.rs
@@ -0,0 +1,57 @@
+use std::ops::Drop;
+use supercow::Supercow;
+
+use ffi;
+use Database;
+use Filenames;
+use FilenamesOwner;
+use utils::{ScopedSupercow, ScopedPhantomcow};
+
+
+#[derive(Debug)]
+pub struct Directory<'d> {
+ ptr: *mut ffi::notmuch_directory_t,
+ marker: ScopedPhantomcow<'d, Database>,
+}
+
+impl<'d> Drop for Directory<'d> {
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_directory_destroy(self.ptr) };
+ }
+}
+
+impl<'d> FilenamesOwner for Directory<'d> {}
+
+impl<'d> Directory<'d> {
+ pub(crate) fn from_ptr<O>(ptr: *mut ffi::notmuch_directory_t, owner: O) -> Directory<'d>
+ where
+ O: Into<ScopedPhantomcow<'d, Database>>,
+ {
+ Directory {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+
+ pub fn child_directories(&self) -> Filenames<Self> {
+ <Self as DirectoryExt>::child_directories(self)
+ }
+}
+
+pub trait DirectoryExt<'d> {
+ fn child_directories<'s, S>(directory: S) -> Filenames<'s, Directory<'d>>
+ where
+ S: Into<ScopedSupercow<'s, Directory<'d>>>,
+ {
+ let dir = directory.into();
+ Filenames::from_ptr(
+ unsafe { ffi::notmuch_directory_get_child_directories(dir.ptr) },
+ Supercow::phantom(dir),
+ )
+ }
+}
+
+impl<'d> DirectoryExt<'d> for Directory<'d> {}
+
+unsafe impl<'d> Send for Directory<'d> {}
+unsafe impl<'d> Sync for Directory<'d> {}
diff --git a/notmuch/src/error.rs b/notmuch/src/error.rs
new file mode 100644
index 0000000..64d0716
--- /dev/null
+++ b/notmuch/src/error.rs
@@ -0,0 +1,55 @@
+use std;
+use std::{error, fmt, io, result};
+
+use ffi;
+
+pub type Result<T> = result::Result<T, Error>;
+
+#[derive(Debug)]
+pub enum Error {
+ IoError(io::Error),
+ NotmuchError(ffi::Status),
+ UnspecifiedError,
+}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", error::Error::description(self))
+ }
+}
+
+impl std::error::Error for Error {
+ fn description(&self) -> &str {
+ match self {
+ Error::IoError(e) => error::Error::description(e),
+ Error::NotmuchError(e) => e.description(),
+ Error::UnspecifiedError => "Generic notmuch error",
+ }
+ }
+
+ fn cause(&self) -> Option<&dyn error::Error> {
+ match *self {
+ Error::IoError(ref e) => Some(e),
+ Error::NotmuchError(ref e) => Some(e),
+ Error::UnspecifiedError => None,
+ }
+ }
+}
+
+impl std::convert::From<io::Error> for Error {
+ fn from(err: io::Error) -> Error {
+ Error::IoError(err)
+ }
+}
+
+impl std::convert::From<ffi::Status> for Error {
+ fn from(err: ffi::Status) -> Error {
+ Error::NotmuchError(err)
+ }
+}
+
+impl std::convert::From<ffi::notmuch_status_t> for Error {
+ fn from(err: ffi::notmuch_status_t) -> Error {
+ Error::NotmuchError(ffi::Status::from(err))
+ }
+}
diff --git a/notmuch/src/ffi.rs b/notmuch/src/ffi.rs
new file mode 100644
index 0000000..5c276d8
--- /dev/null
+++ b/notmuch/src/ffi.rs
@@ -0,0 +1,1852 @@
+#![allow(dead_code, non_camel_case_types)]
+
+//! Re-presentation of the notmuch C API.
+
+use libc::{c_char, c_double, c_int, c_uint, c_ulong, c_void, time_t};
+
+use error::{Error, Result};
+use std::{error, fmt, str};
+use std::borrow::Cow;
+use utils::ToStr;
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Debug, Eq, PartialEq, Clone, Copy)]
+ pub enum notmuch_status_t => Status {
+ NOTMUCH_STATUS_SUCCESS => Success,
+ NOTMUCH_STATUS_OUT_OF_MEMORY => OutOfMemory,
+ NOTMUCH_STATUS_READ_ONLY_DATABASE => ReadOnlyDatabase,
+ NOTMUCH_STATUS_XAPIAN_EXCEPTION => XapianException,
+ NOTMUCH_STATUS_FILE_ERROR => FileError,
+ NOTMUCH_STATUS_FILE_NOT_EMAIL => FileNotEmail,
+ NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID => DuplicateMessageID,
+ NOTMUCH_STATUS_NULL_POINTER => NullPointer,
+ NOTMUCH_STATUS_TAG_TOO_LONG => TagTooLong,
+ NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW => UnbalancedFreezeThaw,
+ NOTMUCH_STATUS_UNBALANCED_ATOMIC => UnbalancedAtomic,
+ NOTMUCH_STATUS_UNSUPPORTED_OPERATION => UnsupportedOperation,
+ NOTMUCH_STATUS_UPGRADE_REQUIRED => UpgradeRequired,
+ // Not an actual status value. Just a way to find out how many
+ // valid status values there are.
+ NOTMUCH_STATUS_LAST_STATUS => LastStatus
+ }
+}
+
+impl notmuch_status_t {
+ pub fn is_ok(self) -> bool {
+ match self {
+ notmuch_status_t::NOTMUCH_STATUS_SUCCESS => true,
+ _ => false,
+ }
+ }
+
+ pub fn is_err(self) -> bool {
+ !self.is_ok()
+ }
+
+ pub fn as_result(self) -> Result<()> {
+ if self.is_ok() {
+ Ok(())
+ } else {
+ Err(Error::NotmuchError(Status::from(self)))
+ }
+ }
+}
+
+impl ToStr for Status {
+ fn to_str<'a>(&self) -> std::result::Result<&'a str, str::Utf8Error> {
+ unsafe { notmuch_status_to_string((*self).into()) }.to_str()
+ }
+
+ fn to_str_unchecked<'a>(&self) -> &'a str {
+ unsafe { notmuch_status_to_string((*self).into()) }.to_str_unchecked()
+ }
+
+ fn to_string_lossy<'a>(&self) -> Cow<'a, str> {
+ unsafe { notmuch_status_to_string((*self).into()) }.to_string_lossy()
+ }
+}
+
+impl fmt::Display for Status {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.to_str().unwrap())
+ }
+}
+
+impl error::Error for Status {
+ fn description(&self) -> &str {
+ self.to_str().unwrap()
+ }
+}
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Debug, Eq, PartialEq, Clone, Copy)]
+ pub enum notmuch_database_mode_t => DatabaseMode {
+ NOTMUCH_DATABASE_MODE_READ_ONLY => ReadOnly,
+ NOTMUCH_DATABASE_MODE_READ_WRITE => ReadWrite
+ }
+}
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Debug, Eq, PartialEq, Clone, Copy)]
+ pub enum notmuch_sort_t => Sort {
+ NOTMUCH_SORT_OLDEST_FIRST => OldestFirst,
+ NOTMUCH_SORT_NEWEST_FIRST => NewestFirst,
+ NOTMUCH_SORT_MESSAGE_ID => MessageID,
+ NOTMUCH_SORT_UNSORTED => Unsorted
+ }
+}
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Copy, Clone, Debug)]
+ pub enum notmuch_exclude_t => Exclude {
+ NOTMUCH_EXCLUDE_FLAG => Flag,
+ NOTMUCH_EXCLUDE_TRUE => True,
+ NOTMUCH_EXCLUDE_FALSE => False,
+ NOTMUCH_EXCLUDE_ALL => All
+ }
+}
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Debug, Eq, PartialEq, Clone, Copy)]
+ pub enum notmuch_message_flag_t => MessageFlag {
+ NOTMUCH_MESSAGE_FLAG_MATCH => Match,
+ NOTMUCH_MESSAGE_FLAG_EXCLUDED => Excluded,
+ NOTMUCH_MESSAGE_FLAG_GHOST => Ghost
+ }
+}
+
+notmuch_enum! {
+ #[repr(C)]
+ #[derive(Debug, Eq, PartialEq, Clone, Copy)]
+ pub enum notmuch_decryption_policy_t => DecryptionPolicy {
+ NOTMUCH_DECRYPT_FALSE => False,
+ NOTMUCH_DECRYPT_TRUE => True,
+ NOTMUCH_DECRYPT_AUTO => Auto,
+ NOTMUCH_DECRYPT_NOSTASH => NoStash
+ }
+}
+
+#[repr(C)]
+pub struct notmuch_database_t(c_void);
+#[repr(C)]
+pub struct notmuch_query_t(c_void);
+#[repr(C)]
+pub struct notmuch_threads_t(c_void);
+#[repr(C)]
+pub struct notmuch_thread_t(c_void);
+#[repr(C)]
+pub struct notmuch_messages_t(c_void);
+#[repr(C)]
+pub struct notmuch_message_t(c_void);
+#[repr(C)]
+pub struct notmuch_tags_t(c_void);
+#[repr(C)]
+pub struct notmuch_directory_t(c_void);
+#[repr(C)]
+pub struct notmuch_filenames_t(c_void);
+#[repr(C)]
+pub struct notmuch_message_properties_t(c_void);
+#[repr(C)]
+pub struct notmuch_config_list_t(c_void);
+#[repr(C)]
+pub struct notmuch_indexopts_t(c_void);
+
+pub type notmuch_compact_status_cb_t = extern "C" fn(message: *const c_char, closure: *mut c_void);
+pub type notmuch_database_upgrade_cb_t = extern "C" fn(closure: *mut c_void, progress: c_double);
+
+pub type notmuch_bool_t = c_int;
+pub const TRUE: notmuch_bool_t = 1;
+pub const FALSE: notmuch_bool_t = 0;
+
+#[link(name = "notmuch")]
+extern "C" {
+
+ /// Get a string representation of a `notmuch_status_t` value.
+ ///
+ /// The result is read-only.
+ pub fn notmuch_status_to_string(status: notmuch_status_t) -> *const c_char;
+
+ /// Create a new, empty notmuch database located at 'path'.
+ ///
+ /// The path should be a top-level directory to a collection of
+ /// plain-text email messages (one message per file). This call will
+ /// create a new ".notmuch" directory within 'path' where notmuch will
+ /// store its data.
+ ///
+ /// After a successful call to `notmuch_database_create`, the returned
+ /// database will be open so the caller should call
+ /// `notmuch_database_destroy` when finished with it.
+ ///
+ /// The database will not yet have any data in it
+ /// (`notmuch_database_create` itself is a very cheap function). Messages
+ /// contained within 'path' can be added to the database by calling
+ /// `notmuch_database_add_message`.
+ ///
+ /// In case of any failure, this function returns an error status and
+ /// sets *database to NULL (after printing an error message on stderr).
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully created the database.
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The given 'path' argument is NULL.
+ ///
+ /// * `notmuch_status_t::OUT_OF_MEMORY`: Out of memory.
+ ///
+ /// * `notmuch_status_t::FILE_ERROR`: An error occurred trying to create the
+ /// database file (such as permission denied, or file not found,
+ /// etc.), or the database already exists.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred.
+ pub fn notmuch_database_create(
+ path: *const c_char,
+ database: *mut *mut notmuch_database_t,
+ ) -> notmuch_status_t;
+
+ /// Like `notmuch_database_create`, except optionally return an error
+ /// message. This message is allocated by malloc and should be freed by
+ /// the caller.
+ pub fn notmuch_database_create_verbose(
+ path: *const c_char,
+ database: *mut *mut notmuch_database_t,
+ error_message: *mut *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Open an existing notmuch database located at 'path'.
+ ///
+ /// The database should have been created at some time in the past,
+ /// (not necessarily by this process), by calling
+ /// notmuch_database_create with 'path'. By default the database should be
+ /// opened for reading only. In order to write to the database you need to
+ /// pass the `notmuch_database_mode_t::READ_WRITE` mode.
+ ///
+ /// An existing notmuch database can be identified by the presence of a
+ /// directory named ".notmuch" below 'path'.
+ ///
+ /// The caller should call notmuch_database_destroy when finished with
+ /// this database.
+ ///
+ /// In case of any failure, this function returns an error status and
+ /// sets *database to NULL (after printing an error message on stderr).
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully opened the database.
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The given 'path' argument is NULL.
+ ///
+ /// * `notmuch_status_t::OUT_OF_MEMORY`: Out of memory.
+ ///
+ /// * `notmuch_status_t::FILE_ERROR`: An error occurred trying to open the
+ /// database file (such as permission denied, or file not found,
+ /// etc.), or the database version is unknown.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred.
+ pub fn notmuch_database_open(
+ path: *const c_char,
+ mode: notmuch_database_mode_t,
+ database: *mut *mut notmuch_database_t,
+ ) -> notmuch_status_t;
+
+ /// Like notmuch_database_open, except optionally return an error
+ /// message. This message is allocated by malloc and should be freed by
+ /// the caller.
+ pub fn notmuch_database_open_verbose(
+ path: *const c_char,
+ mode: notmuch_database_mode_t,
+ database: *mut *mut notmuch_database_t,
+ error_message: *mut *mut c_char,
+ ) -> notmuch_status_t;
+
+ /// Retrieve last status string for given database.
+ pub fn notmuch_database_status_string(notmuch: *mut notmuch_database_t) -> *const c_char;
+
+ /// Commit changes and close the given notmuch database.
+ ///
+ /// After `notmuch_database_close` has been called, calls to other
+ /// functions on objects derived from this database may either behave
+ /// as if the database had not been closed (e.g., if the required data
+ /// has been cached) or may fail with a
+ /// `notmuch_status_t::XAPIAN_EXCEPTION`. The only further operation
+ /// permitted on the database itself is to call `notmuch_database_destroy`.
+ ///
+ /// `notmuch_database_close` can be called multiple times. Later calls have
+ /// no effect.
+ ///
+ /// For writable databases, `notmuch_database_close` commits all changes
+ /// to disk before closing the database. If the caller is currently in
+ /// an atomic section (there was a `notmuch_database_begin_atomic`
+ /// without a matching `notmuch_database_end_atomic`), this will discard
+ /// changes made in that atomic section (but still commit changes made
+ /// prior to entering the atomic section).
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully closed the database.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred; the
+ /// database has been closed but there are no guarantees the
+ /// changes to the database, if any, have been flushed to disk.
+ pub fn notmuch_database_close(database: *mut notmuch_database_t) -> notmuch_status_t;
+
+ /// Compact a notmuch database, backing up the original database to the
+ /// given path.
+ ///
+ /// The database will be opened with notmuch_database_mode_t::READ_WRITE
+ /// during the compaction process to ensure no writes are made.
+ ///
+ /// If the optional callback function 'status_cb' is non-NULL, it will
+ /// be called with diagnostic and informational messages. The argument
+ /// 'closure' is passed verbatim to any callback invoked.
+ pub fn notmuch_database_compact(
+ path: *const c_char,
+ backup_path: *const c_char,
+ status_cb: Option<notmuch_compact_status_cb_t>,
+ closure: *mut c_void,
+ ) -> notmuch_status_t;
+
+ /// Destroy the notmuch database, closing it if necessary and freeing
+ /// all associated resources.
+ ///
+ /// Return value as in `notmuch_database_close` if the database was open;
+ /// `notmuch_database_destroy` itself has no failure modes.
+ pub fn notmuch_database_destroy(database: *mut notmuch_database_t) -> notmuch_status_t;
+
+ /// Return the database path of the given database.
+ ///
+ /// The return value is a string owned by notmuch so should not be
+ /// modified nor freed by the caller.
+ pub fn notmuch_database_get_path(database: *mut notmuch_database_t) -> *const c_char;
+
+ /// Return the database format version of the given database.
+ pub fn notmuch_database_get_version(database: *mut notmuch_database_t) -> c_uint;
+
+ /// Can the database be upgraded to a newer database version?
+ ///
+ /// If this function returns TRUE, then the caller may call
+ /// `notmuch_database_upgrade` to upgrade the database. If the caller
+ /// does not upgrade an out-of-date database, then some functions may
+ /// fail with `notmuch_status_t::UPGRADE_REQUIRED`. This always returns
+ /// FALSE for a read-only database because there's no way to upgrade a
+ /// read-only database.
+ pub fn notmuch_database_needs_upgrade(database: *mut notmuch_database_t) -> notmuch_bool_t;
+
+ /// Upgrade the current database to the latest supported version.
+ ///
+ /// This ensures that all current notmuch functionality will be
+ /// available on the database. After opening a database in read-write
+ /// mode, it is recommended that clients check if an upgrade is needed
+ /// (`notmuch_database_needs_upgrade`) and if so, upgrade with this
+ /// function before making any modifications. If
+ /// `notmuch_database_needs_upgrade` returns FALSE, this will be a no-op.
+ ///
+ /// The optional progress_notify callback can be used by the caller to
+ /// provide progress indication to the user. If non-NULL it will be
+ /// called periodically with 'progress' as a floating-point value in
+ /// the range of [0.0 .. 1.0] indicating the progress made so far in
+ /// the upgrade process. The argument 'closure' is passed verbatim to
+ /// any callback invoked.
+ pub fn notmuch_database_upgrade(
+ database: *mut notmuch_database_t,
+ progress_notify: Option<extern "C" fn(closure: *mut c_void, progress: c_double)>,
+ closure: *mut c_void,
+ ) -> notmuch_status_t;
+
+ /// Begin an atomic database operation.
+ ///
+ /// Any modifications performed between a successful begin and a
+ /// `notmuch_database_end_atomic` will be applied to the database
+ /// atomically. Note that, unlike a typical database transaction, this
+ /// only ensures atomicity, not durability; neither begin nor end
+ /// necessarily flush modifications to disk.
+ ///
+ /// Atomic sections may be nested. begin_atomic and end_atomic must
+ /// always be called in pairs.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully entered atomic section.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred;
+ /// atomic section not entered.
+ pub fn notmuch_database_begin_atomic(notmuch: *mut notmuch_database_t) -> notmuch_status_t;
+
+ /// Indicate the end of an atomic database operation.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully completed atomic section.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred;
+ /// atomic section not ended.
+ ///
+ /// * `notmuch_status_t::UNBALANCED_ATOMIC`: The database is not currently in
+ /// an atomic section.
+ pub fn notmuch_database_end_atomic(notmuch: *mut notmuch_database_t) -> notmuch_status_t;
+
+ /// Return the committed database revision and UUID.
+ ///
+ /// The database revision number increases monotonically with each
+ /// commit to the database. Hence, all messages and message changes
+ /// committed to the database (that is, visible to readers) have a last
+ /// modification revision <= the committed database revision. Any
+ /// messages committed in the future will be assigned a modification
+ /// revision > the committed database revision.
+ ///
+ /// The UUID is a NUL-terminated opaque string that uniquely identifies
+ /// this database. Two revision numbers are only comparable if they
+ /// have the same database UUID.
+ pub fn notmuch_database_get_revision(
+ notmuch: *mut notmuch_database_t,
+ uuid: *mut *const c_char,
+ ) -> c_ulong;
+
+ /// Retrieve a directory object from the database for 'path'.
+ ///
+ /// Here, 'path' should be a path relative to the path of 'database'
+ /// (see `notmuch_database_get_path`), or else should be an absolute path
+ /// with initial components that match the path of 'database'.
+ ///
+ /// If this directory object does not exist in the database, this
+ /// returns `notmuch_status_t::SUCCESS` and sets *directory to NULL.
+ ///
+ /// Otherwise the returned directory object is owned by the database
+ /// and as such, will only be valid until `notmuch_database_destroy` is
+ /// called.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successfully retrieved directory.
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The given 'directory' argument is NULL.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred;
+ /// directory not retrieved.
+ ///
+ /// * `notmuch_status_t::UPGRADE_REQUIRED`: The caller must upgrade the
+ /// database to use this function.
+ pub fn notmuch_database_get_directory(
+ database: *mut notmuch_database_t,
+ path: *const c_char,
+ directory: *mut *mut notmuch_directory_t,
+ ) -> notmuch_status_t;
+
+ /// Add a message file to a database, indexing it for retrieval by
+ /// future searches. If a message already exists with the same message
+ /// ID as the specified file, their indexes will be merged, and this
+ /// new filename will also be associated with the existing message.
+ ///
+ /// Here, 'filename' should be a path relative to the path of
+ /// 'database' (see notmuch_database_get_path), or else should be an
+ /// absolute filename with initial components that match the path of
+ /// 'database'.
+ ///
+ /// The file should be a single mail message (not a multi-message mbox)
+ /// that is expected to remain at its current location, (since the
+ /// notmuch database will reference the filename, and will not copy the
+ /// entire contents of the file.
+ ///
+ /// If another message with the same message ID already exists in the
+ /// database, rather than creating a new message, this adds the search
+ /// terms from the identified file to the existing message's index, and
+ /// adds 'filename' to the list of filenames known for the message.
+ ///
+ /// The 'indexopts' parameter can be NULL (meaning, use the indexing
+ /// defaults from the database), or can be an explicit choice of
+ /// indexing options that should govern the indexing of this specific
+ /// 'filename'.
+ ///
+ /// If 'message' is not NULL, then, on successful return
+ /// (NOTMUCH_STATUS_SUCCESS or NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) '*message'
+ /// will be initialized to a message object that can be used for things
+ /// such as adding tags to the just-added message. The user should call
+ /// notmuch_message_destroy when done with the message. On any failure
+ /// '*message' will be set to NULL.
+ ///
+ /// Return value:
+ ///
+ /// NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ ///
+ /// NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred,
+ /// message not added.
+ ///
+ /// NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ /// ID as another message already in the database. The new
+ /// filename was successfully added to the message in the database
+ /// (if not already present) and the existing message is returned.
+ ///
+ /// NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
+ /// file, (such as permission denied, or file not found,
+ /// etc.). Nothing added to the database.
+ ///
+ /// NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look
+ /// like an email message. Nothing added to the database.
+ ///
+ /// NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ /// mode so no message can be added.
+ ///
+ /// NOTMUCH_STATUS_UPGRADE_REQUIRED: The caller must upgrade the
+ /// database to use this function.
+ ///
+ /// @since libnotmuch 5.1 (notmuch 0.26)
+ pub fn notmuch_database_index_file(
+ database: *mut notmuch_database_t,
+ filename: *const c_char,
+ indexopts: *mut notmuch_indexopts_t,
+ message: *mut *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Deprecated alias for notmuch_database_index_file called with
+ /// NULL indexopts.
+ ///
+ /// @deprecated Deprecated as of libnotmuch 5.1 (notmuch 0.26). Please
+ /// use notmuch_database_index_file instead.
+ pub fn notmuch_database_add_message(
+ database: *mut notmuch_database_t,
+ filename: *const c_char,
+ message: *mut *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Remove a message filename from the given notmuch database. If the
+ /// message has no more filenames, remove the message.
+ ///
+ /// If the same message (as determined by the message ID) is still
+ /// available via other filenames, then the message will persist in the
+ /// database for those filenames. When the last filename is removed for
+ /// a particular message, the database content for that message will be
+ /// entirely removed.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: The last filename was removed and the
+ /// message was removed from the database.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred,
+ /// message not removed.
+ ///
+ /// * `notmuch_status_t::DUPLICATE_MESSAGE_ID`: This filename was removed but
+ /// the message persists in the database with at least one other
+ /// filename.
+ ///
+ /// * `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only
+ /// mode so no message can be removed.
+ ///
+ /// * `notmuch_status_t::UPGRADE_REQUIRED`: The caller must upgrade the
+ /// database to use this function.
+ pub fn notmuch_database_remove_message(
+ database: *mut notmuch_database_t,
+ filename: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Find a message with the given message_id.
+ ///
+ /// If a message with the given message_id is found then, on successful return
+ /// (`notmuch_status_t::SUCCESS`) '*message' will be initialized to a message
+ /// object. The caller should call `notmuch_message_destroy` when done with the
+ /// message.
+ ///
+ /// On any failure or when the message is not found, this function initializes
+ /// '*message' to NULL. This means, when `notmuch_status_t::SUCCESS` is returned, the
+ /// caller is supposed to check '*message' for NULL to find out whether the
+ /// message with the given message_id was found.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successful return, check '*message'.
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The given 'message' argument is NULL
+ ///
+ /// * `notmuch_status_t::OUT_OF_MEMORY`: Out of memory, creating message object
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred
+ pub fn notmuch_database_find_message(
+ database: *mut notmuch_database_t,
+ message_id: *const c_char,
+ message: *mut *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Find a message with the given filename.
+ ///
+ /// If the database contains a message with the given filename then, on
+ /// successful return (`notmuch_status_t::SUCCESS`) '*message' will be initialized to
+ /// a message object. The caller should call `notmuch_message_destroy` when done
+ /// with the message.
+ ///
+ /// On any failure or when the message is not found, this function initializes
+ /// '*message' to NULL. This means, when `notmuch_status_t::SUCCESS` is returned, the
+ /// caller is supposed to check '*message' for NULL to find out whether the
+ /// message with the given filename is found.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Successful return, check '*message'
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The given 'message' argument is NULL
+ ///
+ /// * `notmuch_status_t::OUT_OF_MEMORY`: Out of memory, creating the message object
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred
+ ///
+ /// * `notmuch_status_t::UPGRADE_REQUIRED`: The caller must upgrade the
+ /// database to use this function.
+ pub fn notmuch_database_find_message_by_filename(
+ database: *mut notmuch_database_t,
+ filename: *const c_char,
+ message: *mut *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Return a list of all tags found in the database.
+ ///
+ /// This function creates a list of all tags found in the database. The
+ /// resulting list contains all tags from all messages found in the database.
+ ///
+ /// On error this function returns NULL.
+ pub fn notmuch_database_get_all_tags(db: *mut notmuch_database_t) -> *mut notmuch_tags_t;
+
+ /// Create a new query for 'database'.
+ ///
+ /// Here, 'database' should be an open database, (see
+ /// notmuch_database_open and `notmuch_database_create`).
+ ///
+ /// For the query string, we'll document the syntax here more
+ /// completely in the future, but it's likely to be a specialized
+ /// version of the general Xapian query syntax:
+ ///
+ /// https://xapian.org/docs/queryparser.html
+ ///
+ /// As a special case, passing either a length-zero string, (that is ""),
+ /// or a string consisting of a single asterisk (that is "*"), will
+ /// result in a query that returns all messages in the database.
+ ///
+ /// See `notmuch_query_set_sort` for controlling the order of results.
+ /// See notmuch_query_search_messages and `notmuch_query_search_threads`
+ /// to actually execute the query.
+ ///
+ /// User should call `notmuch_query_destroy` when finished with this
+ /// query.
+ ///
+ /// Will return NULL if insufficient memory is available.
+ pub fn notmuch_query_create(
+ database: *mut notmuch_database_t,
+ query_string: *const c_char,
+ ) -> *mut notmuch_query_t;
+
+ /// Return the query_string of this query. See `notmuch_query_create`.
+ pub fn notmuch_query_get_query_string(query: *mut notmuch_query_t) -> *const c_char;
+
+ /// Return the notmuch database of this query. See `notmuch_query_create`.
+ pub fn notmuch_query_get_database(query: *const notmuch_query_t) -> *mut notmuch_database_t;
+
+ /// Specify whether to omit excluded results or simply flag them. By
+ /// default, this is set to TRUE.
+ ///
+ /// If set to TRUE or ALL, `notmuch_query_search_messages` will omit excluded
+ /// messages from the results, and `notmuch_query_search_threads` will omit
+ /// threads that match only in excluded messages. If set to TRUE,
+ /// `notmuch_query_search_threads` will include all messages in threads that
+ /// match in at least one non-excluded message. Otherwise, if set to ALL,
+ /// `notmuch_query_search_threads` will omit excluded messages from all threads.
+ ///
+ /// If set to FALSE or FLAG then both `notmuch_query_search_messages` and
+ /// `notmuch_query_search_threads` will return all matching
+ /// messages/threads regardless of exclude status. If set to FLAG then
+ /// the exclude flag will be set for any excluded message that is
+ /// returned by `notmuch_query_search_messages`, and the thread counts
+ /// for threads returned by `notmuch_query_search_threads` will be the
+ /// number of non-excluded messages/matches. Otherwise, if set to
+ /// FALSE, then the exclude status is completely ignored.
+ ///
+ /// The performance difference when calling
+ /// `notmuch_query_search_messages` should be relatively small (and both
+ /// should be very fast). However, in some cases,
+ /// `notmuch_query_search_threads` is very much faster when omitting
+ /// excluded messages as it does not need to construct the threads that
+ /// only match in excluded messages.
+ pub fn notmuch_query_set_omit_excluded(
+ query: *mut notmuch_query_t,
+ omit_excluded: notmuch_exclude_t,
+ );
+
+ /// Specify the sorting desired for this query.
+ pub fn notmuch_query_set_sort(query: *mut notmuch_query_t, sort: notmuch_sort_t);
+
+ /// Return the sort specified for this query. See
+ /// `notmuch_query_set_sort`.
+ pub fn notmuch_query_get_sort(query: *mut notmuch_query_t) -> notmuch_sort_t;
+
+ /// Add a tag that will be excluded from the query results by default.
+ /// This exclusion will be overridden if this tag appears explicitly in
+ /// the query.
+ pub fn notmuch_query_add_tag_exclude(query: *mut notmuch_query_t, tag: *const c_char) -> notmuch_status_t;
+
+ /// Execute a query for threads, returning a `notmuch_threads_t` object
+ /// which can be used to iterate over the results. The returned threads
+ /// object is owned by the query and as such, will only be valid until
+ /// `notmuch_query_destroy`.
+ ///
+ /// Typical usage might be:
+ ///
+ /// ```norun
+ /// notmuch_query_t *query;
+ /// notmuch_threads_t *threads;
+ /// notmuch_thread_t *thread;
+ ///
+ /// query = notmuch_query_create (database, query_string);
+ ///
+ /// for (threads = notmuch_query_search_threads (query);
+ /// notmuch_threads_valid (threads);
+ /// notmuch_threads_move_to_next (threads))
+ /// {
+ /// thread = notmuch_threads_get (threads);
+ /// ....
+ /// notmuch_thread_destroy (thread);
+ /// }
+ ///
+ /// notmuch_query_destroy (query);
+ /// ```
+ ///
+ /// Note: If you are finished with a thread before its containing
+ /// query, you can call `notmuch_thread_destroy` to clean up some memory
+ /// sooner (as in the above example). Otherwise, if your thread objects
+ /// are long-lived, then you don't need to call `notmuch_thread_destroy`
+ /// and all the memory will still be reclaimed when the query is
+ /// destroyed.
+ ///
+ /// Note that there's no explicit destructor needed for the
+ /// `notmuch_threads_t` object. (For consistency, we do provide a
+ /// `notmuch_threads_destroy` function, but there's no good reason
+ /// to call it if the query is about to be destroyed).
+ ///
+ /// @since libnotmuch 4.2 (notmuch 0.20)
+ pub fn notmuch_query_search_threads(
+ query: *mut notmuch_query_t,
+ out: *mut *mut notmuch_threads_t,
+ ) -> notmuch_status_t;
+
+ /// Execute a query for messages, returning a `notmuch_messages_t` object
+ /// which can be used to iterate over the results. The returned
+ /// messages object is owned by the query and as such, will only be
+ /// valid until `notmuch_query_destroy`.
+ ///
+ /// Typical usage might be:
+ ///
+ /// ```norun
+ /// notmuch_query_t *query;
+ /// notmuch_messages_t *messages;
+ /// notmuch_message_t *message;
+ ///
+ /// query = notmuch_query_create (database, query_string);
+ ///
+ /// for (messages = notmuch_query_search_messages (query);
+ /// notmuch_messages_valid (messages);
+ /// notmuch_messages_move_to_next (messages))
+ /// {
+ /// message = notmuch_messages_get (messages);
+ /// ....
+ /// notmuch_message_destroy (message);
+ /// }
+ ///
+ /// notmuch_query_destroy (query);
+ /// ```
+ ///
+ /// Note: If you are finished with a message before its containing
+ /// query, you can call `notmuch_message_destroy` to clean up some memory
+ /// sooner (as in the above example). Otherwise, if your message
+ /// objects are long-lived, then you don't need to call
+ /// `notmuch_message_destroy` and all the memory will still be reclaimed
+ /// when the query is destroyed.
+ ///
+ /// Note that there's no explicit destructor needed for the
+ /// `notmuch_messages_t` object. (For consistency, we do provide a
+ /// `notmuch_messages_destroy` function, but there's no good
+ /// reason to call it if the query is about to be destroyed).
+ ///
+ /// If a Xapian exception occurs this function will return NULL.
+ ///
+ /// @since libnotmuch 5 (notmuch 0.25)
+ pub fn notmuch_query_search_messages(
+ query: *mut notmuch_query_t,
+ out: *mut *mut notmuch_messages_t,
+ ) -> notmuch_status_t;
+
+ /// Destroy a `notmuch_query_t` along with any associated resources.
+ ///
+ /// This will in turn destroy any `notmuch_threads_t` and
+ /// `notmuch_messages_t` objects generated by this query, (and in
+ /// turn any notmuch_thread_t and `notmuch_message_t` objects generated
+ /// from those results, etc.), if such objects haven't already been
+ /// destroyed.
+ pub fn notmuch_query_destroy(query: *mut notmuch_query_t);
+
+ /// Is the given 'threads' iterator pointing at a valid thread.
+ ///
+ /// When this function returns TRUE, `notmuch_threads_get` will return a
+ /// valid object. Whereas when this function returns FALSE,
+ /// `notmuch_threads_get` will return NULL.
+ ///
+ /// If passed a NULL pointer, this function returns FALSE
+ ///
+ /// See the documentation of `notmuch_query_search_threads` for example
+ /// code showing how to iterate over a `notmuch_threads_t` object.
+ pub fn notmuch_threads_valid(threads: *mut notmuch_threads_t) -> notmuch_bool_t;
+
+ /// Get the current thread from 'threads' as a `notmuch_thread_t`.
+ ///
+ /// Note: The returned thread belongs to 'threads' and has a lifetime
+ /// identical to it (and the query to which it belongs).
+ ///
+ /// See the documentation of `notmuch_query_search_threads` for example
+ /// code showing how to iterate over a `notmuch_threads_t` object.
+ ///
+ /// If an out-of-memory situation occurs, this function will return
+ /// NULL.
+ pub fn notmuch_threads_get(threads: *mut notmuch_threads_t) -> *mut notmuch_thread_t;
+
+ /// Move the 'threads' iterator to the next thread.
+ ///
+ /// If 'threads' is already pointing at the last thread then the
+ /// iterator will be moved to a point just beyond that last thread,
+ /// (where `notmuch_threads_valid` will return FALSE and
+ /// `notmuch_threads_get` will return NULL).
+ ///
+ /// See the documentation of `notmuch_query_search_threads` for example
+ /// code showing how to iterate over a `notmuch_threads_t` object.
+ pub fn notmuch_threads_move_to_next(threads: *mut notmuch_threads_t);
+
+ /// Destroy a `notmuch_threads_t` object.
+ ///
+ /// It's not strictly necessary to call this function. All memory from
+ /// the `notmuch_threads_t` object will be reclaimed when the
+ /// containing query object is destroyed.
+ pub fn notmuch_threads_destroy(threads: *mut notmuch_threads_t);
+
+ /// Return the number of messages matching a search.
+ ///
+ /// This function performs a search and returns the number of matching
+ /// messages.
+ ///
+ /// @returns
+ ///
+ /// `notmuch_status_t::SUCCESS`: query completed successfully.
+ ///
+ /// `notmuch_status_t::XAPIAN_EXCEPTION`: a Xapian exception occured. The
+ /// value of *count is not defined.
+ ///
+ /// @since libnotmuch 4.3 (notmuch 0.21)
+ pub fn notmuch_query_count_messages(
+ query: *mut notmuch_query_t,
+ count: *mut c_uint,
+ ) -> notmuch_status_t;
+
+ /// Return the number of threads matching a search.
+ ///
+ /// This function performs a search and returns the number of unique thread IDs
+ /// in the matching messages. This is the same as number of threads matching a
+ /// search.
+ ///
+ /// Note that this is a significantly heavier operation than
+ /// `notmuch_query_count_messages`{_st}().
+ ///
+ /// @returns
+ ///
+ /// * `notmuch_status_t::OUT_OF_MEMORY`: Memory allocation failed. The value
+ /// of *count is not defined
+
+ /// * `notmuch_status_t::SUCCESS`: query completed successfully.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: a Xapian exception occured. The
+ /// value of *count is not defined.
+ ///
+ /// @since libnotmuch 4.3 (notmuch 0.21)
+ pub fn notmuch_query_count_threads(
+ query: *mut notmuch_query_t,
+ count: *mut c_uint,
+ ) -> notmuch_status_t;
+
+ /// Get the thread ID of 'thread'.
+ ///
+ /// The returned string belongs to 'thread' and as such, should not be
+ /// modified by the caller and will only be valid for as long as the
+ /// thread is valid, (which is until `notmuch_thread_destroy` or until
+ /// the query from which it derived is destroyed).
+ pub fn notmuch_thread_get_thread_id(thread: *mut notmuch_thread_t) -> *const c_char;
+
+ /// Get the total number of messages in 'thread'.
+ ///
+ /// This count consists of all messages in the database belonging to
+ /// this thread. Contrast with `notmuch_thread_get_matched_messages`().
+ pub fn notmuch_thread_get_total_messages(thread: *mut notmuch_thread_t) -> c_int;
+
+ /// Get the total number of files in 'thread'.
+ ///
+ /// This sums notmuch_message_count_files over all messages in the
+ /// thread
+ /// @returns Non-negative integer
+ /// @since libnotmuch 5.0 (notmuch 0.25)
+ ///
+ pub fn notmuch_thread_get_total_files(thread: *mut notmuch_thread_t) -> c_int;
+
+ /// Get a `notmuch_messages_t` iterator for the top-level messages in
+ /// 'thread' in oldest-first order.
+ ///
+ /// This iterator will not necessarily iterate over all of the messages
+ /// in the thread. It will only iterate over the messages in the thread
+ /// which are not replies to other messages in the thread.
+ ///
+ /// The returned list will be destroyed when the thread is destroyed.
+ pub fn notmuch_thread_get_toplevel_messages(
+ thread: *mut notmuch_thread_t,
+ ) -> *mut notmuch_messages_t;
+
+ /// Get a `notmuch_thread_t` iterator for all messages in 'thread' in
+ /// oldest-first order.
+ ///
+ /// The returned list will be destroyed when the thread is destroyed.
+ pub fn notmuch_thread_get_messages(thread: *mut notmuch_thread_t) -> *mut notmuch_messages_t;
+
+ /// Get the number of messages in 'thread' that matched the search.
+ ///
+ /// This count includes only the messages in this thread that were
+ /// matched by the search from which the thread was created and were
+ /// not excluded by any exclude tags passed in with the query (see
+ /// `notmuch_query_add_tag_exclude`). Contrast with
+ /// `notmuch_thread_get_total_messages`() .
+ pub fn notmuch_thread_get_matched_messages(thread: *mut notmuch_thread_t) -> c_int;
+
+ /// Get the authors of 'thread' as a UTF-8 string.
+ ///
+ /// The returned string is a comma-separated list of the names of the
+ /// authors of mail messages in the query results that belong to this
+ /// thread.
+ ///
+ /// The string contains authors of messages matching the query first, then
+ /// non-matched authors (with the two groups separated by '|'). Within
+ /// each group, authors are ordered by date.
+ ///
+ /// The returned string belongs to 'thread' and as such, should not be
+ /// modified by the caller and will only be valid for as long as the
+ /// thread is valid, (which is until `notmuch_thread_destroy` or until
+ /// the query from which it derived is destroyed).
+ pub fn notmuch_thread_get_authors(thread: *mut notmuch_thread_t) -> *const c_char;
+
+ /// Get the subject of 'thread' as a UTF-8 string.
+ ///
+ /// The subject is taken from the first message (according to the query
+ /// order---see `notmuch_query_set_sort`) in the query results that
+ /// belongs to this thread.
+ ///
+ /// The returned string belongs to 'thread' and as such, should not be
+ /// modified by the caller and will only be valid for as long as the
+ /// thread is valid, (which is until `notmuch_thread_destroy` or until
+ /// the query from which it derived is destroyed).
+ pub fn notmuch_thread_get_subject(thread: *mut notmuch_thread_t) -> *const c_char;
+
+ /// Get the date of the oldest message in 'thread' as a time_t value.
+ pub fn notmuch_thread_get_oldest_date(thread: *mut notmuch_thread_t) -> time_t;
+
+ /// Get the date of the newest message in 'thread' as a time_t value.
+ pub fn notmuch_thread_get_newest_date(thread: *mut notmuch_thread_t) -> time_t;
+
+ /// Get the tags for 'thread', returning a `notmuch_tags_t` object which
+ /// can be used to iterate over all tags.
+ ///
+ /// Note: In the Notmuch database, tags are stored on individual
+ /// messages, not on threads. So the tags returned here will be all
+ /// tags of the messages which matched the search and which belong to
+ /// this thread.
+ ///
+ /// The tags object is owned by the thread and as such, will only be
+ /// valid for as long as the thread is valid, (for example, until
+ /// `notmuch_thread_destroy` or until the query from which it derived is
+ /// destroyed).
+ ///
+ /// Typical usage might be:
+ ///
+ /// ```norun
+ /// notmuch_thread_t *thread;
+ /// notmuch_tags_t *tags;
+ /// const char *tag;
+ ///
+ /// thread = notmuch_threads_get (threads);
+ ///
+ /// for (tags = notmuch_thread_get_tags (thread);
+ /// notmuch_tags_valid (tags);
+ /// notmuch_tags_move_to_next (tags))
+ /// {
+ /// tag = notmuch_tags_get (tags);
+ /// ....
+ /// }
+ ///
+ /// notmuch_thread_destroy (thread);
+ /// ```
+ ///
+ /// Note that there's no explicit destructor needed for the
+ /// `notmuch_tags_t` object. (For consistency, we do provide a
+ /// `notmuch_tags_destroy` function, but there's no good reason to call
+ /// it if the message is about to be destroyed).
+ pub fn notmuch_thread_get_tags(thread: *mut notmuch_thread_t) -> *mut notmuch_tags_t;
+
+ /// Destroy a `notmuch_thread_t` object.
+ pub fn notmuch_thread_destroy(thread: *mut notmuch_thread_t);
+
+ /// Is the given 'messages' iterator pointing at a valid message.
+ ///
+ /// When this function returns TRUE, `notmuch_messages_get` will return a
+ /// valid object. Whereas when this function returns FALSE,
+ /// `notmuch_messages_get` will return NULL.
+ ///
+ /// See the documentation of `notmuch_query_search_messages` for example
+ /// code showing how to iterate over a `notmuch_messages_t` object.
+ pub fn notmuch_messages_valid(messages: *mut notmuch_messages_t) -> notmuch_bool_t;
+
+ /// Get the current message from 'messages' as a `notmuch_message_t`.
+ ///
+ /// Note: The returned message belongs to 'messages' and has a lifetime
+ /// identical to it (and the query to which it belongs).
+ ///
+ /// See the documentation of `notmuch_query_search_messages` for example
+ /// code showing how to iterate over a `notmuch_messages_t` object.
+ ///
+ /// If an out-of-memory situation occurs, this function will return
+ /// NULL.
+ pub fn notmuch_messages_get(messages: *mut notmuch_messages_t) -> *mut notmuch_message_t;
+
+ /// Move the 'messages' iterator to the next message.
+ ///
+ /// If 'messages' is already pointing at the last message then the
+ /// iterator will be moved to a point just beyond that last message,
+ /// (where `notmuch_messages_valid` will return FALSE and
+ /// `notmuch_messages_get` will return NULL).
+ ///
+ /// See the documentation of `notmuch_query_search_messages` for example
+ /// code showing how to iterate over a `notmuch_messages_t` object.
+ pub fn notmuch_messages_move_to_next(messages: *mut notmuch_messages_t);
+
+ /// Destroy a `notmuch_messages_t` object.
+ ///
+ /// It's not strictly necessary to call this function. All memory from
+ /// the `notmuch_messages_t` object will be reclaimed when the containing
+ /// query object is destroyed.
+ pub fn notmuch_messages_destroy(messages: *mut notmuch_messages_t);
+
+ /// Return a list of tags from all messages.
+ ///
+ /// The resulting list is guaranteed not to contain duplicated tags.
+ ///
+ /// WARNING: You can no longer iterate over messages after calling this
+ /// function, because the iterator will point at the end of the list.
+ /// We do not have a function to reset the iterator yet and the only
+ /// way how you can iterate over the list again is to recreate the
+ /// message list.
+ ///
+ /// The function returns NULL on error.
+ pub fn notmuch_messages_collect_tags(messages: *mut notmuch_messages_t) -> *mut notmuch_tags_t;
+
+ /// Get the message ID of 'message'.
+ ///
+ /// The returned string belongs to 'message' and as such, should not be
+ /// modified by the caller and will only be valid for as long as the
+ /// message is valid, (which is until the query from which it derived
+ /// is destroyed).
+ ///
+ /// This function will not return NULL since Notmuch ensures that every
+ /// message has a unique message ID, (Notmuch will generate an ID for a
+ /// message if the original file does not contain one).
+ pub fn notmuch_message_get_message_id(message: *mut notmuch_message_t) -> *const c_char;
+
+ /// Get the thread ID of 'message'.
+ ///
+ /// The returned string belongs to 'message' and as such, should not be
+ /// modified by the caller and will only be valid for as long as the
+ /// message is valid, (for example, until the user calls
+ /// `notmuch_message_destroy` on 'message' or until a query from which it
+ /// derived is destroyed).
+ ///
+ /// This function will not return NULL since Notmuch ensures that every
+ /// message belongs to a single thread.
+ pub fn notmuch_message_get_thread_id(message: *mut notmuch_message_t) -> *const c_char;
+
+ /// Get a `notmuch_messages_t` iterator for all of the replies to
+ /// 'message'.
+ ///
+ /// Note: This call only makes sense if 'message' was ultimately
+ /// obtained from a `notmuch_thread_t` object, (such as by coming
+ /// directly from the result of calling
+ /// `notmuch_thread_get_toplevel_messages` or by any number of subsequent
+ /// calls to `notmuch_message_get_replies`).
+ ///
+ /// If 'message' was obtained through some non-thread means, (such as
+ /// by a call to `notmuch_query_search_messages`), then this function
+ /// will return NULL.
+ ///
+ /// If there are no replies to 'message', this function will return
+ /// NULL. (Note that `notmuch_messages_valid` will accept that NULL
+ /// value as legitimate, and simply return FALSE for it.)
+ pub fn notmuch_message_get_replies(message: *mut notmuch_message_t) -> *mut notmuch_messages_t;
+
+ /// Get the total number of files associated with a message.
+ /// @returns Non-negative integer
+ /// @since libnotmuch 5.0 (notmuch 0.25)
+ pub fn notmuch_message_count_files(message: *mut notmuch_message_t) -> c_int;
+
+ /// Get a filename for the email corresponding to 'message'.
+ ///
+ /// The returned filename is an absolute filename, (the initial
+ /// component will match `notmuch_database_get_path`() ).
+ ///
+ /// The returned string belongs to the message so should not be
+ /// modified or freed by the caller (nor should it be referenced after
+ /// the message is destroyed).
+ ///
+ /// Note: If this message corresponds to multiple files in the mail
+ /// store, (that is, multiple files contain identical message IDs),
+ /// this function will arbitrarily return a single one of those
+ /// filenames. See `notmuch_message_get_filenames` for returning the
+ /// complete list of filenames.
+ pub fn notmuch_message_get_filename(message: *mut notmuch_message_t) -> *const c_char;
+
+ /// Get all filenames for the email corresponding to 'message'.
+ ///
+ /// Returns a `notmuch_filenames_t` iterator listing all the filenames
+ /// associated with 'message'. These files may not have identical
+ /// content, but each will have the identical Message-ID.
+ ///
+ /// Each filename in the iterator is an absolute filename, (the initial
+ /// component will match `notmuch_database_get_path`() ).
+ pub fn notmuch_message_get_filenames(
+ message: *mut notmuch_message_t,
+ ) -> *mut notmuch_filenames_t;
+
+ /// Re-index the e-mail corresponding to 'message' using the supplied index options
+ ///
+ /// Returns the status of the re-index operation. (see the return
+ /// codes documented in notmuch_database_index_file)
+ ///
+ /// After reindexing, the user should discard the message object passed
+ /// in here by calling notmuch_message_destroy, since it refers to the
+ /// original message, not to the reindexed message.
+ pub fn notmuch_message_reindex(
+ message: *mut notmuch_message_t,
+ indexopts: *mut notmuch_indexopts_t
+ ) -> notmuch_status_t;
+
+ /// Get a value of a flag for the email corresponding to 'message'.
+ pub fn notmuch_message_get_flag(
+ message: *mut notmuch_message_t,
+ flag: notmuch_message_flag_t,
+ ) -> notmuch_bool_t;
+
+ /// Set a value of a flag for the email corresponding to 'message'.
+ pub fn notmuch_message_set_flag(
+ message: *mut notmuch_message_t,
+ flag: notmuch_message_flag_t,
+ value: notmuch_bool_t,
+ );
+
+ /// Get the date of 'message' as a time_t value.
+ ///
+ /// For the original textual representation of the Date header from the
+ /// message call `notmuch_message_get_header`() with a header value of
+ /// "date".
+ pub fn notmuch_message_get_date(message: *mut notmuch_message_t) -> time_t;
+
+ /// Get the value of the specified header from 'message' as a UTF-8 string.
+ ///
+ /// Common headers are stored in the database when the message is
+ /// indexed and will be returned from the database. Other headers will
+ /// be read from the actual message file.
+ ///
+ /// The header name is case insensitive.
+ ///
+ /// The returned string belongs to the message so should not be
+ /// modified or freed by the caller (nor should it be referenced after
+ /// the message is destroyed).
+ ///
+ /// Returns an empty string ("") if the message does not contain a
+ /// header line matching 'header'. Returns NULL if any error occurs.
+ pub fn notmuch_message_get_header(
+ message: *mut notmuch_message_t,
+ header: *const c_char,
+ ) -> *const c_char;
+
+ /// Get the tags for 'message', returning a `notmuch_tags_t` object which
+ /// can be used to iterate over all tags.
+ ///
+ /// The tags object is owned by the message and as such, will only be
+ /// valid for as long as the message is valid, (which is until the
+ /// query from which it derived is destroyed).
+ ///
+ /// Typical usage might be:
+ ///
+ /// ```norun
+ /// notmuch_message_t *message;
+ /// notmuch_tags_t *tags;
+ /// const char *tag;
+ ///
+ /// message = notmuch_database_find_message (database, message_id);
+ ///
+ /// for (tags = `notmuch_message_get_tags` (message);
+ /// notmuch_tags_valid (tags);
+ /// notmuch_tags_move_to_next (tags))
+ /// {
+ /// tag = notmuch_tags_get (tags);
+ /// ....
+ /// }
+ ///
+ /// notmuch_message_destroy (message);
+ /// ```
+ ///
+ /// Note that there's no explicit destructor needed for the
+ /// `notmuch_tags_t` object. (For consistency, we do provide a
+ /// `notmuch_tags_destroy` function, but there's no good reason to call
+ /// it if the message is about to be destroyed).
+ pub fn notmuch_message_get_tags(message: *mut notmuch_message_t) -> *mut notmuch_tags_t;
+
+ /// Add a tag to the given message.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Tag successfully added to message
+ ///
+ /// * `notmuch_status_t::NULL_POINTER`: The 'tag' argument is NULL
+ ///
+ /// * `notmuch_status_t::TAG_TOO_LONG`: The length of 'tag' is too long
+ /// (exceeds TAG_MAX)
+ ///
+ /// * `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only
+ /// mode so message cannot be modified.
+ pub fn notmuch_message_add_tag(
+ message: *mut notmuch_message_t,
+ tag: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Remove a tag from the given message.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: Tag successfully removed from message
+ /// * `notmuch_status_t::NULL_POINTER`: The 'tag' argument is NULL
+ /// * `notmuch_status_t::TAG_TOO_LONG`: The length of 'tag' is too long (exceeds `TAG_MAX`)
+ /// * `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only mode so message
+ /// cannot be modified.
+ pub fn notmuch_message_remove_tag(
+ message: *mut notmuch_message_t,
+ tag: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Remove all tags from the given message.
+ ///
+ /// See `notmuch_message_freeze` for an example showing how to safely
+ /// replace tag values.
+ ///
+ /// `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only
+ /// mode so message cannot be modified.
+ pub fn notmuch_message_remove_all_tags(message: *mut notmuch_message_t) -> notmuch_status_t;
+
+ /// Add/remove tags according to maildir flags in the message filename(s).
+ ///
+ /// This function examines the filenames of 'message' for maildir flags, and adds or removes
+ /// tags on 'message' as follows when these flags are present:
+ ///
+ /// ```norun
+ /// Flag Action if present
+ /// ---- -----------------
+ /// 'D' Adds the "draft" tag to the message
+ /// 'F' Adds the "flagged" tag to the message
+ /// 'P' Adds the "passed" tag to the message
+ /// 'R' Adds the "replied" tag to the message
+ /// 'S' Removes the "unread" tag from the message
+ /// ```
+ ///
+ /// For each flag that is not present, the opposite action (add/remove)
+ /// is performed for the corresponding tags.
+ ///
+ /// Flags are identified as trailing components of the filename after a
+ /// sequence of ":2,".
+ ///
+ /// If there are multiple filenames associated with this message, the
+ /// flag is considered present if it appears in one or more
+ /// filenames. (That is, the flags from the multiple filenames are
+ /// combined with the logical OR operator.)
+ ///
+ /// A client can ensure that notmuch database tags remain synchronized
+ /// with maildir flags by calling this function after each call to
+ /// `notmuch_database_add_message`. See also
+ /// `notmuch_message_tags_to_maildir_flags` for synchronizing tag changes
+ /// back to maildir flags.
+ pub fn notmuch_message_maildir_flags_to_tags(
+ message: *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Rename message filename(s) to encode tags as maildir flags.
+ ///
+ /// Specifically, for each filename corresponding to this message:
+ ///
+ /// If the filename is not in a maildir directory, do nothing. (A
+ /// maildir directory is determined as a directory named "new" or
+ /// "cur".) Similarly, if the filename has invalid maildir info,
+ /// (repeated or outof-ASCII-order flag characters after ":2,"), then
+ /// do nothing.
+ ///
+ /// If the filename is in a maildir directory, rename the file so that
+ /// its filename ends with the sequence ":2," followed by zero or more
+ /// of the following single-character flags (in ASCII order):
+ ///
+ /// 'D' iff the message has the "draft" tag
+ /// 'F' iff the message has the "flagged" tag
+ /// 'P' iff the message has the "passed" tag
+ /// 'R' iff the message has the "replied" tag
+ /// 'S' iff the message does not have the "unread" tag
+ ///
+ /// Any existing flags unmentioned in the list above will be preserved
+ /// in the renaming.
+ ///
+ /// Also, if this filename is in a directory named "new", rename it to
+ /// be within the neighboring directory named "cur".
+ ///
+ /// A client can ensure that maildir filename flags remain synchronized
+ /// with notmuch database tags by calling this function after changing
+ /// tags, (after calls to `notmuch_message_add_tag`,
+ /// notmuch_message_remove_tag, or `notmuch_message_freeze`/
+ /// notmuch_message_thaw). See also `notmuch_message_maildir_flags_to_tags`
+ /// for synchronizing maildir flag changes back to tags.
+ pub fn notmuch_message_tags_to_maildir_flags(
+ message: *mut notmuch_message_t,
+ ) -> notmuch_status_t;
+
+ /// Freeze the current state of 'message' within the database.
+ ///
+ /// This means that changes to the message state, (via
+ /// notmuch_message_add_tag, `notmuch_message_remove_tag`, and
+ /// `notmuch_message_remove_all_tags`), will not be committed to the
+ /// database until the message is thawed with `notmuch_message_thaw`.
+ ///
+ /// Multiple calls to freeze/thaw are valid and these calls will
+ /// "stack". That is there must be as many calls to thaw as to freeze
+ /// before a message is actually thawed.
+ ///
+ /// The ability to do freeze/thaw allows for safe transactions to
+ /// change tag values. For example, explicitly setting a message to
+ /// have a given set of tags might look like this:
+ ///
+ /// ```norun
+ /// notmuch_message_freeze (message);
+ ///
+ /// notmuch_message_remove_all_tags (message);
+ ///
+ /// for (i = 0; i < NUM_TAGS; i++)
+ /// notmuch_message_add_tag (message, tags[i]);
+ ///
+ /// notmuch_message_thaw (message);
+ /// ```
+ ///
+ /// With freeze/thaw used like this, the message in the database is
+ /// guaranteed to have either the full set of original tag values, or
+ /// the full set of new tag values, but nothing in between.
+ ///
+ /// Imagine the example above without freeze/thaw and the operation
+ /// somehow getting interrupted. This could result in the message being
+ /// left with no tags if the interruption happened after
+ /// notmuch_message_remove_all_tags but before `notmuch_message_add_tag`.
+ ///
+ /// Return value:
+ ///
+ /// `notmuch_status_t::SUCCESS`: Message successfully frozen.
+ ///
+ /// `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only
+ /// mode so message cannot be modified.
+ pub fn notmuch_message_freeze(message: *mut notmuch_message_t) -> notmuch_status_t;
+
+ /// Thaw the current 'message', synchronizing any changes that may have
+ /// occurred while 'message' was frozen into the notmuch database.
+ ///
+ /// See `notmuch_message_freeze` for an example of how to use this
+ /// function to safely provide tag changes.
+ ///
+ /// Multiple calls to freeze/thaw are valid and these calls with
+ /// "stack". That is there must be as many calls to thaw as to freeze
+ /// before a message is actually thawed.
+ ///
+ /// Return value:
+ ///
+ /// `notmuch_status_t::SUCCESS`: Message successfully thawed, (or at least
+ /// its frozen count has successfully been reduced by 1).
+ ///
+ /// `notmuch_status_t::UNBALANCED_FREEZE_THAW`: An attempt was made to thaw
+ /// an unfrozen message. That is, there have been an unbalanced
+ /// number of calls to `notmuch_message_freeze` and
+ /// `notmuch_message_thaw`.
+ pub fn notmuch_message_thaw(message: *mut notmuch_message_t) -> notmuch_status_t;
+
+ /// Destroy a `notmuch_message_t` object.
+ ///
+ /// It can be useful to call this function in the case of a single
+ /// query object with many messages in the result, (such as iterating
+ /// over the entire database). Otherwise, it's fine to never call this
+ /// function and there will still be no memory leaks. (The memory from
+ /// the messages get reclaimed when the containing query is destroyed.)
+ pub fn notmuch_message_destroy(message: *mut notmuch_message_t);
+
+ /// Retrieve the value for a single property key
+ ///
+ /// *value* is set to a string owned by the message or NULL if there is
+ /// no such key. In the case of multiple values for the given key, the
+ /// first one is retrieved.
+ ///
+ /// @returns
+ /// - `notmuch_status_t::NULL_POINTER`: *value* may not be NULL.
+ /// - `notmuch_status_t::SUCCESS`: No error occured.
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_get_property(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ value: *mut *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Add a (key,value) pair to a message
+ ///
+ /// @returns
+ /// - `notmuch_status_t::ILLEGAL_ARGUMENT`: *key* may not contain an '=' character.
+ /// - `notmuch_status_t::NULL_POINTER`: Neither *key* nor *value* may be NULL.
+ /// - `notmuch_status_t::SUCCESS`: No error occured.
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_add_property(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ value: *const c_char,
+ ) -> notmuch_status_t;
+
+ ///
+ /// Remove a `(key,value)` pair from a message.
+ ///
+ /// It is not an error to remove a non-existant `(key,value)` pair
+ ///
+ /// @returns
+ /// - `notmuch_status_t::ILLEGAL_ARGUMENT`: `key` may not contain an '=' character.
+ /// - `notmuch_status_t::NULL_POINTER`: Neither `key` nor *value* may be NULL.
+ /// - `notmuch_status_t::SUCCESS`: No error occured.
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_remove_property(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ value: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Remove all `(key,value)` pairs from the given message.
+ ///
+ /// @param[in,out] message message to operate on.
+ /// @param[in] key key to delete properties for. If NULL, delete
+ /// properties for all keys
+ /// @returns
+ /// - `notmuch_status_::READ_ONLY_DATABASE`: Database was opened in
+ /// read-only mode so message cannot be modified.
+ /// - `notmuch_status_t::SUCCESS`: No error occured.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_remove_all_properties(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Remove all (prefix*,value) pairs from the given message
+ ///
+ /// @param[in,out] message message to operate on.
+ /// @param[in] prefix delete properties with keys that start with prefix.
+ /// If NULL, delete all properties
+ /// @returns
+ /// - NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in
+ /// read-only mode so message cannot be modified.
+ /// - NOTMUCH_STATUS_SUCCESS: No error occurred.
+ ///
+ /// @since libnotmuch 5.1 (notmuch 0.26)
+ ///
+ pub fn notmuch_message_remove_all_properties_with_prefix(
+ message: *mut notmuch_message_t,
+ prefix: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// Get the properties for *message*, returning a
+ /// `notmuch_message_properties_t` object which can be used to iterate over
+ /// all properties.
+ ///
+ /// The `notmuch_message_properties_t` object is owned by the message and as
+ /// such, will only be valid for as long as the message is valid, (which is
+ /// until the query from which it derived is destroyed).
+ ///
+ /// @param[in] message The message to examine
+ /// @param[in] key key or key prefix
+ /// @param[in] exact if TRUE, require exact match with key. Otherwise
+ /// treat as prefix.
+ ///
+ /// Typical usage might be:
+ ///
+ /// ```norun
+ /// notmuch_message_properties_t *list;
+ ///
+ /// for (list = notmuch_message_get_properties (message, "testkey1", TRUE);
+ /// notmuch_message_properties_valid (list); notmuch_message_properties_move_to_next (list)) {
+ /// printf("%s\n", notmuch_message_properties_value(list));
+ /// }
+ ///
+ /// notmuch_message_properties_destroy (list);
+ /// ```
+ ///
+ /// Note that there's no explicit destructor needed for the
+ /// `notmuch_message_properties_t` object. (For consistency, we do provide a
+ /// `notmuch_message_properities_destroy` function, but there's no good
+ /// reason to call it if the message is about to be destroyed).
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ ///
+ pub fn notmuch_message_get_properties(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ exact: notmuch_bool_t,
+ ) -> *mut notmuch_message_properties_t;
+
+
+ /// Return the number of properties named "key" belonging to the specific message.
+ ///
+ /// @param[in] message The message to examine
+ /// @param[in] key key to count
+ /// @param[out] count The number of matching properties associated with this message.
+ ///
+ /// @returns
+ ///
+ /// NOTMUCH_STATUS_SUCCESS: successful count, possibly some other error.
+ ///
+ /// @since libnotmuch 5.2 (notmuch 0.27)
+ pub fn notmuch_message_count_properties(
+ message: *mut notmuch_message_t,
+ key: *const c_char,
+ count: *mut c_uint,
+ ) -> notmuch_status_t;
+
+ /// Is the given *properties* iterator pointing at a valid `(key,value)` pair.
+ ///
+ /// When this function returns TRUE, `notmuch_message_properties_{key,value}`
+ /// will return a valid string, and `notmuch_message_properties_move_to_next`
+ /// will do what it says. Whereas when this function returns FALSE, calling any
+ /// of these functions results in undefined behaviour.
+ ///
+ /// See the documentation of `notmuch_message_properties_get` for example code
+ /// showing how to iterate over a `notmuch_message_properties_t` object.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_properties_valid(
+ properties: *const notmuch_message_properties_t,
+ ) -> notmuch_bool_t;
+
+ /// Move the *properties* iterator to the next `(key,value)` pair
+ ///
+ /// If *properties* is already pointing at the last pair then the iterator
+ /// will be moved to a point just beyond that last pair, (where
+ /// `notmuch_message_properties_valid` will return FALSE).
+ ///
+ /// See the documentation of `notmuch_message_get_properties` for example
+ /// code showing how to iterate over a `notmuch_message_properties_t` object.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_properties_move_to_next(properties: *mut notmuch_message_properties_t);
+
+ /// Return the `key` from the current `(key,value)` pair.
+ ///
+ /// this could be useful if iterating for a prefix
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ ///
+ pub fn notmuch_message_properties_key(
+ properties: *mut notmuch_message_properties_t,
+ ) -> *const c_char;
+
+ /// Return the `value` from the current `(key,value)` pair.
+ ///
+ /// This could be useful if iterating for a prefix.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_properties_value(
+ properties: *const notmuch_message_properties_t,
+ ) -> *const c_char;
+
+ /// Destroy a `notmuch_message_properties_t` object.
+ ///
+ /// It's not strictly necessary to call this function. All memory from
+ /// the `notmuch_message_properties_t` object will be reclaimed when the
+ /// containing message object is destroyed.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_message_properties_destroy(properties: *mut notmuch_message_properties_t);
+
+ /// Is the given 'tags' iterator pointing at a valid tag.
+ ///
+ /// When this function returns TRUE, `notmuch_tags_get` will return a
+ /// valid string. Whereas when this function returns FALSE,
+ /// `notmuch_tags_get` will return NULL.
+ ///
+ /// See the documentation of `notmuch_message_get_tags` for example code
+ /// showing how to iterate over a `notmuch_tags_t` object.
+ pub fn notmuch_tags_valid(tags: *mut notmuch_tags_t) -> notmuch_bool_t;
+
+ /// Get the current tag from 'tags' as a string.
+ ///
+ /// Note: The returned string belongs to 'tags' and has a lifetime
+ /// identical to it (and the query to which it ultimately belongs).
+ ///
+ /// See the documentation of `notmuch_message_get_tags` for example code
+ /// showing how to iterate over a `notmuch_tags_t` object.
+ pub fn notmuch_tags_get(tags: *mut notmuch_tags_t) -> *const c_char;
+
+ /// Move the 'tags' iterator to the next tag.
+ ///
+ /// If 'tags' is already pointing at the last tag then the iterator
+ /// will be moved to a point just beyond that last tag, (where
+ /// notmuch_tags_valid will return FALSE and `notmuch_tags_get` will
+ /// return NULL).
+ ///
+ /// See the documentation of `notmuch_message_get_tags` for example code
+ /// showing how to iterate over a `notmuch_tags_t` object.
+ pub fn notmuch_tags_move_to_next(tags: *mut notmuch_tags_t);
+
+ /// Destroy a `notmuch_tags_t` object.
+ ///
+ /// It's not strictly necessary to call this function. All memory from
+ /// the `notmuch_tags_t` object will be reclaimed when the containing
+ /// message or query objects are destroyed.
+ pub fn notmuch_tags_destroy(tags: *mut notmuch_tags_t);
+
+ /// Store an mtime within the database for 'directory'.
+ ///
+ /// The 'directory' should be an object retrieved from the database
+ /// with `notmuch_database_get_directory` for a particular path.
+ ///
+ /// The intention is for the caller to use the mtime to allow efficient
+ /// identification of new messages to be added to the database. The
+ /// recommended usage is as follows:
+ ///
+ /// * Read the mtime of a directory from the filesystem
+ ///
+ /// * Call add_message for all mail files in the directory
+ ///
+ /// * Call `notmuch_directory_set_mtime` with the mtime read from the filesystem.
+ ///
+ /// Then, when wanting to check for updates to the directory in the
+ /// future, the client can call `notmuch_directory_get_mtime` and know
+ /// that it only needs to add files if the mtime of the directory and
+ /// files are newer than the stored timestamp.
+ ///
+ /// Note: The `notmuch_directory_get_mtime` function does not allow the
+ /// caller to distinguish a timestamp of 0 from a non-existent
+ /// timestamp. So don't store a timestamp of 0 unless you are
+ /// comfortable with that.
+ ///
+ /// Return value:
+ ///
+ /// * `notmuch_status_t::SUCCESS`: mtime successfully stored in database.
+ ///
+ /// * `notmuch_status_t::XAPIAN_EXCEPTION`: A Xapian exception occurred, mtime not stored.
+ ///
+ /// * `notmuch_status_t::READ_ONLY_DATABASE`: Database was opened in read-only mode so
+ /// directory mtime cannot be modified.
+ pub fn notmuch_directory_set_mtime(
+ directory: *mut notmuch_directory_t,
+ mtime: time_t,
+ ) -> notmuch_status_t;
+
+ /// Get the mtime of a directory, (as previously stored with
+ /// `notmuch_directory_set_mtime`).
+ ///
+ /// Returns 0 if no mtime has previously been stored for this
+ /// directory.
+ pub fn notmuch_directory_get_mtime(directory: *mut notmuch_directory_t) -> time_t;
+
+ /// Get a `notmuch_filenames_t` iterator listing all the filenames of
+ /// messages in the database within the given directory.
+ ///
+ /// The returned filenames will be the basename-entries only (not
+ /// complete paths).
+ pub fn notmuch_directory_get_child_files(
+ directory: *mut notmuch_directory_t,
+ ) -> *mut notmuch_filenames_t;
+
+ /// Get a `notmuch_filenames_t` iterator listing all the filenames of
+ /// sub-directories in the database within the given directory.
+ ///
+ /// The returned filenames will be the basename-entries only (not
+ /// complete paths).
+ pub fn notmuch_directory_get_child_directories(
+ directory: *mut notmuch_directory_t,
+ ) -> *mut notmuch_filenames_t;
+
+ /// Delete directory document from the database, and destroy the
+ /// `notmuch_directory_t` object. Assumes any child directories and files
+ /// have been deleted by the caller.
+ ///
+ /// @since libnotmuch 4.3 (notmuch 0.21)
+ pub fn notmuch_directory_delete(directory: *mut notmuch_directory_t) -> notmuch_status_t;
+
+ /// Destroy a `notmuch_directory_t` object.
+ pub fn notmuch_directory_destroy(directory: *mut notmuch_directory_t);
+
+ /// Is the given 'filenames' iterator pointing at a valid filename.
+ ///
+ /// When this function returns TRUE, `notmuch_filenames_get` will return
+ /// a valid string. Whereas when this function returns FALSE,
+ /// `notmuch_filenames_get` will return NULL.
+ ///
+ /// It is acceptable to pass NULL for 'filenames', in which case this
+ /// function will always return FALSE.
+ pub fn notmuch_filenames_valid(filenames: *mut notmuch_filenames_t) -> notmuch_bool_t;
+
+ /// Get the current filename from 'filenames' as a string.
+ ///
+ /// Note: The returned string belongs to 'filenames' and has a lifetime
+ /// identical to it (and the directory to which it ultimately belongs).
+ ///
+ /// It is acceptable to pass NULL for 'filenames', in which case this
+ /// function will always return NULL.
+ pub fn notmuch_filenames_get(filenames: *mut notmuch_filenames_t) -> *const c_char;
+
+ /// Move the 'filenames' iterator to the next filename.
+ ///
+ /// If 'filenames' is already pointing at the last filename then the
+ /// iterator will be moved to a point just beyond that last filename,
+ /// (where `notmuch_filenames_valid` will return FALSE and
+ /// `notmuch_filenames_get` will return NULL).
+ ///
+ /// It is acceptable to pass NULL for 'filenames', in which case this
+ /// function will do nothing.
+ pub fn notmuch_filenames_move_to_next(filenames: *mut notmuch_filenames_t);
+
+ /// Destroy a `notmuch_filenames_t` object.
+ ///
+ /// It's not strictly necessary to call this function. All memory from
+ /// the `notmuch_filenames_t` object will be reclaimed when the
+ /// containing directory object is destroyed.
+ ///
+ /// It is acceptable to pass NULL for 'filenames', in which case this
+ /// function will do nothing.
+ pub fn notmuch_filenames_destroy(filenames: *mut notmuch_filenames_t);
+
+ /// set config 'key' to 'value'
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_database_set_config(
+ db: *mut notmuch_database_t,
+ key: *const c_char,
+ value: *const c_char,
+ ) -> notmuch_status_t;
+
+ /// retrieve config item 'key', assign to 'value'
+ ///
+ /// keys which have not been previously set with n_d_set_config will
+ /// return an empty string.
+ ///
+ /// return value is allocated by malloc and should be freed by the
+ /// caller.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_database_get_config(
+ db: *mut notmuch_database_t,
+ key: *const c_char,
+ value: *mut *mut c_char,
+ ) -> notmuch_status_t;
+
+ /// Create an iterator for all config items with keys matching a given prefix
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_database_get_config_list(
+ db: *mut notmuch_database_t,
+ prefix: *const c_char,
+ out: *mut *mut notmuch_config_list_t,
+ ) -> notmuch_status_t;
+
+ /// Is 'config_list' iterator valid (i.e. _key, _value, _move_to_next can be called).
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_config_list_valid(config_list: *mut notmuch_config_list_t) -> notmuch_bool_t;
+
+ /// return key for current config pair
+ ///
+ /// return value is owned by the iterator, and will be destroyed by the
+ /// next call to `notmuch_config_list_key` or `notmuch_config_list_destroy`.
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_config_list_key(config_list: *mut notmuch_config_list_t) -> *const c_char;
+
+ /// return 'value' for current config pair
+ ///
+ /// return value is owned by the iterator, and will be destroyed by the
+ /// next call to `notmuch_config_list_value` or notmuch `config_list_destroy`
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_config_list_value(config_list: *mut notmuch_config_list_t) -> *const c_char;
+
+ /// move 'config_list' iterator to the next pair
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_config_list_move_to_next(config_list: *mut notmuch_config_list_t);
+
+ /// free any resources held by 'config_list'
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_config_list_destroy(config_list: *mut notmuch_config_list_t);
+
+ /// get the current default indexing options for a given database.
+ ///
+ /// This object will survive until the database itself is destroyed,
+ /// but the caller may also release it earlier with
+ /// notmuch_indexopts_destroy.
+ ///
+ /// This object represents a set of options on how a message can be
+ /// added to the index. At the moment it is a featureless stub.
+ ///
+ /// @since libnotmuch 5.1 (notmuch 0.26)
+ pub fn notmuch_database_get_default_indexopts(db: *mut notmuch_database_t) -> *mut notmuch_indexopts_t;
+
+
+ ////
+ //// Stating a policy about how to decrypt messages.
+ ////
+ //// See index.decrypt in notmuch-config(1) for more details.
+ ////
+ //// typedef enum {
+ //// NOTMUCH_DECRYPT_FALSE,
+ //// NOTMUCH_DECRYPT_TRUE,
+ //// NOTMUCH_DECRYPT_AUTO,
+ //// NOTMUCH_DECRYPT_NOSTASH,
+ //// } notmuch_decryption_policy_t;
+ ////
+ ////
+ //// Specify whether to decrypt encrypted parts while indexing.
+ ////
+ //// Be aware that the index is likely sufficient to reconstruct the
+ //// cleartext of the message itself, so please ensure that the notmuch
+ //// message index is adequately protected. DO NOT SET THIS FLAG TO TRUE
+ //// without considering the security of your index.
+ ////
+ //// @since libnotmuch 5.1 (notmuch 0.26)
+ pub fn notmuch_indexopts_set_decrypt_policy(options: *mut notmuch_indexopts_t,
+ decrypt_policy: notmuch_decryption_policy_t) -> notmuch_status_t;
+
+ //// Return whether to decrypt encrypted parts while indexing.
+ //// see notmuch_indexopts_set_decrypt_policy.
+ ////
+ //// @since libnotmuch 5.1 (notmuch 0.26)
+ pub fn notmuch_indexopts_get_decrypt_policy(options: *const notmuch_indexopts_t) -> notmuch_decryption_policy_t;
+
+
+ /// Destroy a notmuch_indexopts_t object.
+ ///
+ /// @since libnotmuch 5.1 (notmuch 0.26)
+ pub fn notmuch_indexopts_destroy(options: *mut notmuch_indexopts_t);
+
+ /// interrogate the library for compile time features
+ ///
+ /// @since libnotmuch 4.4 (notmuch 0.23)
+ pub fn notmuch_built_with(name: *const c_char) -> notmuch_bool_t;
+}
diff --git a/notmuch/src/filenames.rs b/notmuch/src/filenames.rs
new file mode 100644
index 0000000..12e5857
--- /dev/null
+++ b/notmuch/src/filenames.rs
@@ -0,0 +1,68 @@
+use std::ffi::CStr;
+use std::iter::Iterator;
+use std::ops::Drop;
+use std::path::PathBuf;
+
+use ffi;
+use utils::ScopedPhantomcow;
+
+pub trait FilenamesOwner {}
+
+#[derive(Debug)]
+pub struct Filenames<'o, O>
+where
+ O: FilenamesOwner + 'o,
+{
+ pub(crate) ptr: *mut ffi::notmuch_filenames_t,
+ pub(crate) marker: ScopedPhantomcow<'o, O>,
+}
+
+impl<'o, O> Drop for Filenames<'o, O>
+where
+ O: FilenamesOwner + 'o,
+{
+ fn drop(self: &mut Self) {
+ unsafe { ffi::notmuch_filenames_destroy(self.ptr) };
+ }
+}
+
+impl<'o, O> Filenames<'o, O>
+where
+ O: FilenamesOwner + 'o,
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_filenames_t, owner: P) -> Filenames<'o, O>
+ where
+ P: Into<ScopedPhantomcow<'o, O>>,
+ {
+ Filenames {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+impl<'o, O> Iterator for Filenames<'o, O>
+where
+ O: FilenamesOwner + 'o,
+{
+ type Item = PathBuf;
+
+ fn next(self: &mut Self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_filenames_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let ctag = unsafe {
+ let t = ffi::notmuch_filenames_get(self.ptr);
+ ffi::notmuch_filenames_move_to_next(self.ptr);
+ CStr::from_ptr(t)
+ };
+
+ Some(PathBuf::from(ctag.to_str().unwrap()))
+ }
+}
+
+unsafe impl<'o, O> Send for Filenames<'o, O> where O: FilenamesOwner + 'o {}
+unsafe impl<'o, O> Sync for Filenames<'o, O> where O: FilenamesOwner + 'o {}
diff --git a/notmuch/src/index.rs b/notmuch/src/index.rs
new file mode 100644
index 0000000..4db6bc7
--- /dev/null
+++ b/notmuch/src/index.rs
@@ -0,0 +1,43 @@
+use std::ops::Drop;
+
+use error::Result;
+use ffi;
+use ffi::DecryptionPolicy;
+use Database;
+use utils::ScopedPhantomcow;
+
+
+#[derive(Debug)]
+pub struct IndexOpts<'d> {
+ pub(crate) ptr: *mut ffi::notmuch_indexopts_t,
+ marker: ScopedPhantomcow<'d, Database>,
+}
+
+impl<'d> Drop for IndexOpts<'d> {
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_indexopts_destroy(self.ptr) };
+ }
+}
+
+impl<'d> IndexOpts<'d> {
+ pub(crate) fn from_ptr<O>(ptr: *mut ffi::notmuch_indexopts_t, owner: O) -> IndexOpts<'d>
+ where
+ O: Into<ScopedPhantomcow<'d, Database>>,
+ {
+ IndexOpts {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+
+ pub fn set_decrypt_policy(self: &Self, decrypt_policy: DecryptionPolicy) -> Result<()> {
+ unsafe { ffi::notmuch_indexopts_set_decrypt_policy(self.ptr, decrypt_policy.into()) }.as_result()
+ }
+
+ pub fn decrypt_policy(self: &Self) -> DecryptionPolicy {
+ unsafe { ffi::notmuch_indexopts_get_decrypt_policy(self.ptr)}.into()
+ }
+}
+
+unsafe impl<'d> Send for IndexOpts<'d> {}
+unsafe impl<'d> Sync for IndexOpts<'d> {} \ No newline at end of file
diff --git a/notmuch/src/lib.rs b/notmuch/src/lib.rs
new file mode 100644
index 0000000..1eefaf2
--- /dev/null
+++ b/notmuch/src/lib.rs
@@ -0,0 +1,43 @@
+#![cfg_attr(feature = "clippy", feature(plugin))]
+#![cfg_attr(feature = "clippy", plugin(clippy))]
+
+#[macro_use]
+mod macros;
+
+extern crate libc;
+extern crate supercow;
+
+mod ffi;
+mod utils;
+
+mod database;
+mod directory;
+mod error;
+mod filenames;
+mod message;
+mod messages;
+mod query;
+mod tags;
+mod thread;
+mod threads;
+mod index;
+mod config_list;
+mod message_properties;
+
+pub use database::{Database, DatabaseExt, AtomicOperation};
+pub use directory::{Directory, DirectoryExt};
+pub use error::Error;
+pub use filenames::{Filenames, FilenamesOwner};
+pub use message::{Message, MessageExt, MessageOwner, FrozenMessage};
+pub use messages::{Messages, MessagesExt};
+pub use message_properties::{MessageProperties};
+pub use query::{Query, QueryExt};
+pub use tags::{Tags, TagsExt, TagsOwner};
+pub use thread::{Thread, ThreadExt};
+pub use threads::{Threads, ThreadsExt};
+pub use index::IndexOpts;
+pub use config_list::ConfigList;
+
+pub use ffi::{Status, DatabaseMode, Sort, DecryptionPolicy};
+
+pub use utils::{ScopedSupercow, ScopedPhantomcow}; \ No newline at end of file
diff --git a/notmuch/src/macros.rs b/notmuch/src/macros.rs
new file mode 100644
index 0000000..a36197b
--- /dev/null
+++ b/notmuch/src/macros.rs
@@ -0,0 +1,35 @@
+#[macro_escape]
+macro_rules! notmuch_enum {
+ (
+ $(#[$enum_attr:meta])*
+ pub enum $name:ident => $name_alias:ident {
+ $($variant:ident => $variant_alias:ident),*
+ }
+ ) => {
+ $(#[$enum_attr])*
+ pub enum $name {
+ $($variant),*
+ }
+
+ $(#[$enum_attr])*
+ pub enum $name_alias {
+ $($variant_alias),*
+ }
+
+ impl From<$name> for $name_alias {
+ fn from(t: $name) -> Self {
+ match t {
+ $($name::$variant => $name_alias::$variant_alias),*
+ }
+ }
+ }
+
+ impl Into<$name> for $name_alias {
+ fn into(self) -> $name {
+ match self {
+ $($name_alias::$variant_alias => $name::$variant),*
+ }
+ }
+ }
+ }
+}
diff --git a/notmuch/src/message.rs b/notmuch/src/message.rs
new file mode 100644
index 0000000..55a544f
--- /dev/null
+++ b/notmuch/src/message.rs
@@ -0,0 +1,329 @@
+use std::ffi::CString;
+use std::path::PathBuf;
+use std::cell::RefCell;
+use std::borrow::Cow;
+use std::ptr;
+
+use supercow::{Supercow};
+
+use error::{Error, Result};
+use ffi;
+use utils::{ToStr, ScopedPhantomcow, ScopedSupercow};
+use Filenames;
+use FilenamesOwner;
+use Messages;
+use MessageProperties;
+use Tags;
+use TagsOwner;
+use IndexOpts;
+
+pub trait MessageOwner: Send + Sync {}
+
+#[derive(Debug)]
+pub struct Message<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ pub(crate) ptr: *mut ffi::notmuch_message_t,
+ marker: RefCell<ScopedPhantomcow<'o, O>>,
+}
+
+impl<'o, O> MessageOwner for Message<'o, O> where O: MessageOwner + 'o {}
+impl<'o, O> FilenamesOwner for Message<'o, O> where O: MessageOwner + 'o {}
+impl<'o, O> TagsOwner for Message<'o, O> where O: MessageOwner + 'o {}
+
+
+// impl<'o, O> PartialEq for Message<'o, O>
+// where
+// O: MessageOwner + 'o
+// {
+// fn eq(self: &Self, other: &Message<'o, O>) -> bool{
+// self.id() == other.id()
+// }
+// }
+
+impl<'o, O> Message<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_message_t, owner: P) -> Message<'o, O>
+ where
+ P: Into<ScopedPhantomcow<'o, O>>,
+ {
+ Message {
+ ptr,
+ marker: RefCell::new(owner.into()),
+ }
+ }
+
+ pub fn id(self: &Self) -> Cow<'_, str> {
+ let mid = unsafe { ffi::notmuch_message_get_message_id(self.ptr) };
+ mid.to_string_lossy()
+ }
+
+ pub fn thread_id(self: &Self) -> Cow<'_, str> {
+ let tid = unsafe { ffi::notmuch_message_get_thread_id(self.ptr) };
+ tid.to_string_lossy()
+ }
+
+ pub fn replies(self: &Self) -> Messages<'o, O> {
+ Messages::<'o, O>::from_ptr(
+ unsafe { ffi::notmuch_message_get_replies(self.ptr) },
+ // will never panic since the borrow is released immediately
+ ScopedPhantomcow::<'o, O>::share(&mut *(self.marker.borrow_mut()))
+ )
+ }
+
+ #[cfg(feature = "v0_26")]
+ pub fn count_files(self: &Self) -> i32 {
+ unsafe { ffi::notmuch_message_count_files(self.ptr) }
+ }
+
+ pub fn filenames(self: &Self) -> Filenames<Self> {
+ <Self as MessageExt<'o, O>>::filenames(self)
+ }
+
+ pub fn filename(self: &Self) -> PathBuf {
+ PathBuf::from(
+ unsafe { ffi::notmuch_message_get_filename(self.ptr) }
+ .to_str()
+ .unwrap(),
+ )
+ }
+
+ pub fn date(&self) -> i64 {
+ unsafe { ffi::notmuch_message_get_date(self.ptr) as i64 }
+ }
+
+ pub fn header(&self, name: &str) -> Result<Option<Cow<'_, str>>> {
+ let name = CString::new(name).unwrap();
+ let ret = unsafe { ffi::notmuch_message_get_header(self.ptr, name.as_ptr()) };
+ if ret.is_null() {
+ Err(Error::UnspecifiedError)
+ } else {
+ let ret_str = ret.to_string_lossy();
+ if ret_str.is_empty() {
+ Ok(None)
+ } else{
+ Ok(Some(ret_str))
+ }
+ }
+ }
+
+ pub fn tags(&self) -> Tags<Self> {
+ <Self as MessageExt<'o, O>>::tags(self)
+ }
+
+ pub fn add_tag(self: &Self, tag: &str) -> Result<()> {
+ let tag = CString::new(tag).unwrap();
+ unsafe { ffi::notmuch_message_add_tag(self.ptr, tag.as_ptr()) }.as_result()
+ }
+
+ pub fn remove_tag(self: &Self, tag: &str) -> Result<()> {
+ let tag = CString::new(tag).unwrap();
+ unsafe { ffi::notmuch_message_remove_tag(self.ptr, tag.as_ptr()) }.as_result()
+ }
+
+ pub fn remove_all_tags(self: &Self) -> Result<()> {
+ unsafe { ffi::notmuch_message_remove_all_tags(self.ptr) }.as_result()
+ }
+
+ pub fn tags_to_maildir_flags(self: &Self) -> Result<()> {
+ unsafe { ffi::notmuch_message_tags_to_maildir_flags(self.ptr) }.as_result()
+ }
+
+ pub fn maildir_flags_to_tags(self: &Self) -> Result<()> {
+ unsafe { ffi::notmuch_message_maildir_flags_to_tags(self.ptr) }.as_result()
+ }
+
+ pub fn reindex<'d>(self: &Self, indexopts: IndexOpts<'d>) -> Result<()> {
+ unsafe { ffi::notmuch_message_reindex(self.ptr, indexopts.ptr) }.as_result()
+ }
+
+ pub fn freeze(self: &Self) -> Result<()> {
+ unsafe { ffi::notmuch_message_freeze(self.ptr) }.as_result()
+ }
+
+ pub fn thaw(self: &Self) -> Result<()> {
+ unsafe { ffi::notmuch_message_thaw(self.ptr) }.as_result()
+ }
+
+ pub fn properties<'m>(&'m self, key: &str, exact: bool) -> MessageProperties<'m, 'o, O> {
+ <Self as MessageExt<'o, O>>::properties(self, key, exact)
+ }
+
+ pub fn remove_all_properties(&self, key: Option<&str>) -> Result<()>
+ {
+ match key {
+ Some(k) => {
+ let key_str = CString::new(k).unwrap();
+ unsafe {
+ ffi::notmuch_message_remove_all_properties(self.ptr, key_str.as_ptr())
+ }.as_result()
+ },
+ None => {
+ let p = ptr::null();
+ unsafe {
+ ffi::notmuch_message_remove_all_properties(self.ptr, p)
+ }.as_result()
+ }
+ }
+ }
+
+ pub fn remove_all_properties_with_prefix(&self, prefix: Option<&str>) -> Result<()>
+ {
+ match prefix {
+ Some(k) => {
+ let key_str = CString::new(k).unwrap();
+ unsafe {
+ ffi::notmuch_message_remove_all_properties_with_prefix(self.ptr, key_str.as_ptr())
+ }.as_result()
+ },
+ None => {
+ let p = ptr::null();
+ unsafe {
+ ffi::notmuch_message_remove_all_properties_with_prefix(self.ptr, p)
+ }.as_result()
+ }
+ }
+ }
+
+
+ pub fn count_properties(&self, key: &str) -> Result<u32>
+ {
+ let key_str = CString::new(key).unwrap();
+ let mut cnt = 0;
+ unsafe {
+ ffi::notmuch_message_count_properties(self.ptr, key_str.as_ptr(), &mut cnt)
+ }.as_result()?;
+
+ Ok(cnt)
+ }
+
+ pub fn property(&self, key: &str) -> Result<Cow<'_, str>>
+ {
+ let key_str = CString::new(key).unwrap();
+ let mut prop = ptr::null();
+ unsafe {
+ ffi::notmuch_message_get_property(self.ptr, key_str.as_ptr(), &mut prop)
+ }.as_result()?;
+
+ if prop.is_null() {
+ Err(Error::UnspecifiedError)
+ } else {
+ // TODO: the unwrap here is not good
+ Ok(prop.to_string_lossy())
+ }
+ }
+
+ pub fn add_property(&self, key: &str, value: &str) -> Result<()>
+ {
+ let key_str = CString::new(key).unwrap();
+ let value_str = CString::new(value).unwrap();
+ unsafe {
+ ffi::notmuch_message_add_property(self.ptr, key_str.as_ptr(), value_str.as_ptr())
+ }.as_result()
+ }
+
+ pub fn remove_property(&self, key: &str, value: &str) -> Result<()>
+ {
+ let key_str = CString::new(key).unwrap();
+ let value_str = CString::new(value).unwrap();
+ unsafe {
+ ffi::notmuch_message_remove_property(self.ptr, key_str.as_ptr(), value_str.as_ptr())
+ }.as_result()
+ }
+}
+
+pub trait MessageExt<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ fn tags<'m, M>(message: M) -> Tags<'m, Message<'o, O>>
+ where
+ M: Into<ScopedSupercow<'m, Message<'o, O>>>,
+ {
+ let messageref = message.into();
+ Tags::from_ptr(
+ unsafe { ffi::notmuch_message_get_tags(messageref.ptr) },
+ Supercow::phantom(messageref),
+ )
+ }
+
+ // fn replies<'s, S>(message: S) -> Messages<'s, Message<'o, O>>
+ // where
+ // S: Into<ScopedSupercow<'s, Message<'o, O>>>,
+ // {
+ // let messageref = message.into();
+ // Messages::from_ptr(
+ // unsafe { ffi::notmuch_message_get_replies(messageref.ptr) },
+ // Supercow::phantom(messageref),
+ // )
+ // }
+
+ fn filenames<'m, M>(message: M) -> Filenames<'m, Message<'o, O>>
+ where
+ M: Into<ScopedSupercow<'m, Message<'o, O>>>,
+ {
+ let messageref = message.into();
+ Filenames::from_ptr(
+ unsafe { ffi::notmuch_message_get_filenames(messageref.ptr) },
+ Supercow::phantom(messageref),
+ )
+ }
+
+ fn properties<'m, M>(message: M, key: &str, exact: bool) -> MessageProperties<'m, 'o, O>
+ where
+ M: Into<ScopedSupercow<'m, Message<'o, O>>>,
+ {
+ let messageref = message.into();
+ let key_str = CString::new(key).unwrap();
+
+ let props = unsafe {
+ ffi::notmuch_message_get_properties(messageref.ptr, key_str.as_ptr(), exact as i32)
+ };
+
+ MessageProperties::from_ptr(props, Supercow::phantom(messageref))
+ }
+}
+
+impl<'o, O> MessageExt<'o, O> for Message<'o, O> where O: MessageOwner + 'o {}
+
+unsafe impl<'o, O> Send for Message<'o, O> where O: MessageOwner + 'o {}
+unsafe impl<'o, O> Sync for Message<'o, O> where O: MessageOwner + 'o {}
+
+
+pub struct FrozenMessage<'m ,'o, O>
+where
+ O: MessageOwner + 'o
+{
+ message: ScopedSupercow<'m, Message<'o, O>>
+}
+
+
+impl<'m, 'o, O> FrozenMessage<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ pub fn new<M>(message: M) -> Result<Self>
+ where
+ M: Into<ScopedSupercow<'m, Message<'o, O>>>
+ {
+ let msg = message.into();
+ msg.freeze()?;
+ Ok(FrozenMessage{
+ message: msg
+ })
+ }
+}
+
+impl<'m, 'o, O> Drop for FrozenMessage<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ fn drop(&mut self) {
+ let _ = self.message.thaw();
+ }
+}
+
+
diff --git a/notmuch/src/message_properties.rs b/notmuch/src/message_properties.rs
new file mode 100644
index 0000000..bd20aa0
--- /dev/null
+++ b/notmuch/src/message_properties.rs
@@ -0,0 +1,73 @@
+use std::ops::Drop;
+use std::ffi::CStr;
+
+use ffi;
+use Message;
+use MessageOwner;
+use utils::{ScopedPhantomcow};
+
+
+#[derive(Debug)]
+pub struct MessageProperties<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ ptr: *mut ffi::notmuch_message_properties_t,
+ marker: ScopedPhantomcow<'m, Message<'o, O>>,
+}
+
+impl<'m, 'o, O> Drop for MessageProperties<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_message_properties_destroy(self.ptr) };
+ }
+}
+
+impl<'m, 'o, O> MessageProperties<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ pub(crate) fn from_ptr<S>(ptr: *mut ffi::notmuch_message_properties_t, owner: S) -> MessageProperties<'m, 'o, O>
+ where
+ S: Into<ScopedPhantomcow<'m, Message<'o, O>>>,
+ {
+ MessageProperties {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+
+impl<'m, 'o, O> Iterator for MessageProperties<'m, 'o, O>
+where
+ O: MessageOwner + 'o
+{
+ type Item = (String, String);
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_message_properties_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let (k, v) = unsafe {
+ let key = CStr::from_ptr(ffi::notmuch_message_properties_key(self.ptr));
+ let value = CStr::from_ptr(ffi::notmuch_message_properties_value(self.ptr));
+
+ ffi::notmuch_message_properties_move_to_next(self.ptr);
+
+ (key, value)
+ };
+
+ Some((k.to_string_lossy().to_string(), v.to_string_lossy().to_string()))
+ }
+}
+
+unsafe impl<'m, 'o, O> Send for MessageProperties<'m, 'o, O> where
+ O: MessageOwner + 'o {}
+unsafe impl<'m, 'o, O> Sync for MessageProperties<'m, 'o, O> where
+ O: MessageOwner + 'o {}
diff --git a/notmuch/src/messages.rs b/notmuch/src/messages.rs
new file mode 100644
index 0000000..cfba547
--- /dev/null
+++ b/notmuch/src/messages.rs
@@ -0,0 +1,136 @@
+use ffi;
+use utils::ScopedPhantomcow;
+use MessageOwner;
+use Message;
+use Tags;
+use TagsOwner;
+
+#[derive(Debug)]
+pub struct Messages<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ pub(crate) ptr: *mut ffi::notmuch_messages_t,
+ marker: ScopedPhantomcow<'o, O>,
+}
+
+// impl<'o, O> Drop for Messages<'o, O>
+// where
+// O: MessageOwner + 'o,
+// {
+// fn drop(self: &mut Self) {
+// unsafe { ffi::notmuch_messages_destroy(self.ptr) };
+// }
+// }
+
+impl<'o, O> Messages<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_messages_t, owner: P) -> Messages<'o, O>
+ where
+ P: Into<ScopedPhantomcow<'o, O>>,
+ {
+ Messages {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+impl<'o, O> MessageOwner for Messages<'o, O> where O: MessageOwner + 'o {}
+impl<'o, O> TagsOwner for Messages<'o, O> where O: MessageOwner + 'o {}
+
+impl<'o, O> Messages<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ /**
+ * Return a list of tags from all messages.
+ *
+ * The resulting list is guaranteed not to contain duplicated tags.
+ *
+ * WARNING: You can no longer iterate over messages after calling this
+ * function, because the iterator will point at the end of the list.
+ * We do not have a function to reset the iterator yet and the only
+ * way how you can iterate over the list again is to recreate the
+ * message list.
+ *
+ * The function returns NULL on error.
+ */
+ pub fn collect_tags<'m>(self: &'o Self) -> Tags<'m, Self> {
+ Tags::from_ptr(
+ unsafe { ffi::notmuch_messages_collect_tags(self.ptr) },
+ self,
+ )
+ }
+}
+
+impl<'o, O> Iterator for Messages<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+ type Item = Message<'o, O>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_messages_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let cthrd = unsafe {
+ let thrd = ffi::notmuch_messages_get(self.ptr);
+ ffi::notmuch_messages_move_to_next(self.ptr);
+ thrd
+ };
+
+ Some(Message::from_ptr(cthrd, ScopedPhantomcow::<'o, O>::share(&mut self.marker)))
+ }
+}
+
+
+
+pub trait MessagesExt<'o, O>
+where
+ O: MessageOwner + 'o,
+{
+}
+
+impl<'o, O> MessagesExt<'o, O> for Messages<'o, O> where O: MessageOwner + 'o {}
+
+
+unsafe impl<'o, O> Send for Messages<'o, O> where O: MessageOwner + 'o {}
+unsafe impl<'o, O> Sync for Messages<'o, O> where O: MessageOwner + 'o {}
+
+#[cfg(test)]
+mod tests {
+ // This will not compile if ownership can't be subject to recursion
+ fn descend<'o, O: 'o + super::MessageOwner, T: Iterator<Item=super::Message<'o, O>>>(iter: T)
+ -> usize {
+ iter.map(|msg| descend(msg.replies()) ).count()
+ }
+
+ use query::Query;
+ use database;
+
+ #[test]
+ #[should_panic] // until test data is filled in
+ fn recurse() {
+ match database::Database::open(
+ &String::new(),
+ database::DatabaseMode::ReadOnly,
+ ) {
+ /* This will not happen without test data, but will force the compiler to compile
+ * the descend function.
+ */
+ Ok(db) => {
+ let q = Query::create(db, &String::new()).unwrap();
+ descend::<Query, super::Messages<Query>>(q.search_messages().unwrap());
+ }
+ Err(err) => {
+ panic!("Got error while trying to open db: {:?}", err);
+ }
+ }
+ }
+}
diff --git a/notmuch/src/query.rs b/notmuch/src/query.rs
new file mode 100644
index 0000000..50b56e5
--- /dev/null
+++ b/notmuch/src/query.rs
@@ -0,0 +1,132 @@
+use std::ops::Drop;
+use std::ptr;
+use std::ffi::{CStr, CString};
+
+use supercow::{Phantomcow, Supercow};
+
+use error::Result;
+use ffi;
+use ffi::{Sort, Exclude};
+use Database;
+use Messages;
+use MessageOwner;
+use Threads;
+use DatabaseExt;
+use utils::ScopedSupercow;
+
+#[derive(Debug)]
+pub struct Query<'d> {
+ pub(crate) ptr: *mut ffi::notmuch_query_t,
+ marker: Phantomcow<'d, Database>,
+}
+
+impl<'d> Drop for Query<'d> {
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_query_destroy(self.ptr) };
+ }
+}
+
+impl<'d> MessageOwner for Query<'d> {}
+
+impl<'d> Query<'d> {
+ pub(crate) fn from_ptr<O>(ptr: *mut ffi::notmuch_query_t, owner: O) -> Query<'d>
+ where
+ O: Into<Phantomcow<'d, Database>>,
+ {
+ Query {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+
+ pub fn create<D>(db: D, query_string: &str) -> Result<Self>
+ where
+ D: Into<Supercow<'d, Database>>,
+ {
+ <Database as DatabaseExt>::create_query(db, query_string)
+ }
+
+ pub fn query_string(self: &Self) -> String {
+ let qstring = unsafe {
+ CStr::from_ptr(ffi::notmuch_query_get_query_string(self.ptr))
+ };
+ qstring.to_str().unwrap().to_string()
+ }
+
+ /// Specify the sorting desired for this query.
+ pub fn set_sort(self: &Self, sort: Sort) {
+ unsafe { ffi::notmuch_query_set_sort(self.ptr, sort.into()) }
+ }
+
+ /// Return the sort specified for this query. See
+ /// `set_sort`.
+ pub fn sort(self: &Self) -> Sort {
+ unsafe { ffi::notmuch_query_get_sort(self.ptr) }.into()
+ }
+
+ /// Filter messages according to the query and return
+ pub fn search_messages<'q>(self: &'d Self) -> Result<Messages<'q, Self>> {
+ <Query as QueryExt>::search_messages(self)
+ }
+
+ pub fn count_messages(self: &Self) -> Result<u32> {
+ let mut cnt = 0;
+ unsafe { ffi::notmuch_query_count_messages(self.ptr, &mut cnt) }.as_result()?;
+
+ Ok(cnt)
+ }
+
+ pub fn search_threads<'q>(self: &'d Self) -> Result<Threads<'d, 'q>> {
+ <Query<'d> as QueryExt>::search_threads(self)
+ }
+
+ pub fn count_threads(self: &Self) -> Result<u32> {
+ let mut cnt = 0;
+ unsafe { ffi::notmuch_query_count_threads(self.ptr, &mut cnt) }.as_result()?;
+
+ Ok(cnt)
+ }
+
+ pub fn add_tag_exclude(self: &Self, tag: &str) -> Result<()>
+ {
+ let tag_str = CString::new(tag).unwrap();
+ unsafe { ffi::notmuch_query_add_tag_exclude(self.ptr, tag_str.as_ptr()) }.as_result()
+ }
+
+ pub fn set_omit_excluded(self: &Self, omit_excluded: Exclude) {
+ unsafe { ffi::notmuch_query_set_omit_excluded(self.ptr, omit_excluded.into()) }
+ }
+}
+
+pub trait QueryExt<'d> {
+ fn search_threads<'q, Q>(query: Q) -> Result<Threads<'d, 'q>>
+ where
+ Q: Into<ScopedSupercow<'q, Query<'d>>>,
+ {
+ let queryref = query.into();
+
+ let mut thrds = ptr::null_mut();
+ unsafe { ffi::notmuch_query_search_threads(queryref.ptr, &mut thrds) }
+ .as_result()?;
+
+ Ok(Threads::from_ptr(thrds, ScopedSupercow::phantom(queryref)))
+ }
+
+ fn search_messages<'q, Q>(query: Q) -> Result<Messages<'q, Query<'d>>>
+ where
+ Q: Into<ScopedSupercow<'q, Query<'d>>>,
+ {
+ let queryref = query.into();
+
+ let mut msgs = ptr::null_mut();
+ unsafe { ffi::notmuch_query_search_messages(queryref.ptr, &mut msgs) }
+ .as_result()?;
+
+ Ok(Messages::from_ptr(msgs, ScopedSupercow::phantom(queryref)))
+ }
+}
+
+impl<'d> QueryExt<'d> for Query<'d> {}
+
+unsafe impl<'d> Send for Query<'d> {}
+unsafe impl<'d> Sync for Query<'d> {}
diff --git a/notmuch/src/tags.rs b/notmuch/src/tags.rs
new file mode 100644
index 0000000..5f3ec60
--- /dev/null
+++ b/notmuch/src/tags.rs
@@ -0,0 +1,85 @@
+use std::cmp::PartialEq;
+use std::ffi::CStr;
+use std::iter::Iterator;
+use std::ops::Drop;
+
+use ffi;
+use utils::ScopedPhantomcow;
+
+pub trait TagsOwner {}
+
+#[derive(Debug)]
+pub struct Tags<'o, O> where
+ O: TagsOwner + 'o,
+{
+ pub(crate) ptr: *mut ffi::notmuch_tags_t,
+ marker: ScopedPhantomcow<'o, O>,
+}
+
+impl<'o, O> Drop for Tags<'o, O>
+where
+ O: TagsOwner + 'o,
+{
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_tags_destroy(self.ptr) };
+ }
+}
+
+impl<'o, O> PartialEq for Tags<'o, O>
+where
+ O: TagsOwner + 'o,
+{
+ fn eq(&self, other: &Self) -> bool {
+ self.ptr == other.ptr
+ }
+}
+
+impl<'o, O> Tags<'o, O>
+where
+ O: TagsOwner + 'o,
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_tags_t, owner: P) -> Tags<'o, O>
+ where
+ P: Into<ScopedPhantomcow<'o, O>>,
+ {
+ Tags {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+impl<'o, O> Iterator for Tags<'o, O>
+where
+ O: TagsOwner + 'o,
+{
+ type Item = String;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_tags_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let ctag = unsafe {
+ let t = ffi::notmuch_tags_get(self.ptr);
+ ffi::notmuch_tags_move_to_next(self.ptr);
+
+ CStr::from_ptr(t)
+ };
+
+ Some(ctag.to_string_lossy().to_string())
+ }
+}
+
+pub trait TagsExt<'o, O>
+where
+ O: TagsOwner + 'o,
+{
+}
+
+impl<'o, O> TagsExt<'o, O> for Tags<'o, O> where O: TagsOwner + 'o {}
+
+unsafe impl<'o, O> Send for Tags<'o, O> where O: TagsOwner + 'o {}
+unsafe impl<'o, O> Sync for Tags<'o, O> where O: TagsOwner + 'o {}
diff --git a/notmuch/src/thread.rs b/notmuch/src/thread.rs
new file mode 100644
index 0000000..ffc1f2f
--- /dev/null
+++ b/notmuch/src/thread.rs
@@ -0,0 +1,148 @@
+use std::ops::Drop;
+use std::borrow::Cow;
+
+use ffi;
+use utils::{ToStr, ScopedSupercow, ScopedPhantomcow};
+use Messages;
+use MessageOwner;
+use Tags;
+use TagsOwner;
+use Query;
+
+#[derive(Debug)]
+pub struct Thread<'d, 'q>
+where
+ 'd: 'q
+{
+ pub(crate) ptr: *mut ffi::notmuch_thread_t,
+ pub(crate) marker: ScopedPhantomcow<'q, Query<'d>>,
+}
+
+impl<'d, 'q> Drop for Thread<'d, 'q>
+where
+ 'd: 'q
+{
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_thread_destroy(self.ptr) };
+ }
+}
+
+impl<'d, 'q> MessageOwner for Thread<'d, 'q> where 'd: 'q {}
+impl<'d, 'q> TagsOwner for Thread<'d, 'q> where 'd: 'q {}
+
+impl<'d, 'q> Thread<'d, 'q>
+where
+ 'd: 'q
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_thread_t, owner: P) -> Thread<'d, 'q>
+ where
+ P: Into<ScopedPhantomcow<'q, Query<'d>>>,
+ {
+ Thread {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+
+ pub fn id(self: &Self) -> &str {
+ let tid = unsafe { ffi::notmuch_thread_get_thread_id(self.ptr) };
+ tid.to_str().unwrap()
+ }
+
+ pub fn total_messages(self: &Self) -> i32 {
+ unsafe { ffi::notmuch_thread_get_total_messages(self.ptr) }
+ }
+
+ #[cfg(feature = "0.26")]
+ pub fn total_files(self: &Self) -> i32 {
+ unsafe { ffi::notmuch_thread_get_total_files(self.ptr) }
+ }
+
+ pub fn toplevel_messages(self: &Self) -> Messages<'_, Self> {
+ <Self as ThreadExt<'d, 'q>>::toplevel_messages(self)
+ }
+
+ pub fn matched_messages(self: &Self) -> i32 {
+ unsafe { ffi::notmuch_thread_get_matched_messages(self.ptr) }
+ }
+
+ /// Get a `Messages` iterator for all messages in 'thread' in
+ /// oldest-first order.
+ pub fn messages(self: &Self) -> Messages<'_, Self> {
+ <Self as ThreadExt<'d, 'q>>::messages(self)
+ }
+
+ pub fn tags(&self) -> Tags<'_, Self> {
+ <Self as ThreadExt<'d, 'q>>::tags(self)
+ }
+
+ pub fn subject(self: &Self) -> Cow<'_, str> {
+ let sub = unsafe { ffi::notmuch_thread_get_subject(self.ptr) };
+ sub.to_string_lossy()
+ }
+
+ pub fn authors(self: &Self) -> Vec<String> {
+ let athrs = unsafe { ffi::notmuch_thread_get_authors(self.ptr) };
+
+ athrs
+ .to_string_lossy()
+ .split(',')
+ .map(|s| s.to_string())
+ .collect()
+ }
+
+ /// Get the date of the oldest message in 'thread' as a time_t value.
+ pub fn oldest_date(self: &Self) -> i64 {
+ unsafe { ffi::notmuch_thread_get_oldest_date(self.ptr) as i64 }
+ }
+
+ /// Get the date of the newest message in 'thread' as a time_t value.
+ pub fn newest_date(self: &Self) -> i64 {
+ unsafe { ffi::notmuch_thread_get_newest_date(self.ptr) as i64 }
+ }
+}
+
+pub trait ThreadExt<'d, 'q>
+where
+ 'd: 'q
+{
+ fn tags<'s, S>(thread: S) -> Tags<'s, Thread<'d, 'q>>
+ where
+ S: Into<ScopedSupercow<'s, Thread<'d, 'q>>>,
+ {
+ let threadref = thread.into();
+ Tags::from_ptr(
+ unsafe { ffi::notmuch_thread_get_tags(threadref.ptr) },
+ ScopedSupercow::phantom(threadref),
+ )
+ }
+
+ fn toplevel_messages<'s, S>(thread: S) -> Messages<'s, Thread<'d, 'q>>
+ where
+ S: Into<ScopedSupercow<'s, Thread<'d, 'q>>>,
+ {
+ let threadref = thread.into();
+ Messages::from_ptr(
+ unsafe { ffi::notmuch_thread_get_toplevel_messages(threadref.ptr) },
+ ScopedSupercow::phantom(threadref),
+ )
+ }
+
+ /// Get a `Messages` iterator for all messages in 'thread' in
+ /// oldest-first order.
+ fn messages<'s, S>(thread: S) -> Messages<'s, Thread<'d, 'q>>
+ where
+ S: Into<ScopedSupercow<'s, Thread<'d, 'q>>>,
+ {
+ let threadref = thread.into();
+ Messages::from_ptr(
+ unsafe { ffi::notmuch_thread_get_messages(threadref.ptr) },
+ ScopedSupercow::phantom(threadref),
+ )
+ }
+}
+
+impl<'d, 'q> ThreadExt<'d, 'q> for Thread<'d, 'q> where 'd: 'q {}
+
+unsafe impl<'d, 'q> Send for Thread<'d, 'q> where 'd: 'q {}
+unsafe impl<'d, 'q> Sync for Thread<'d, 'q> where 'd: 'q {}
diff --git a/notmuch/src/threads.rs b/notmuch/src/threads.rs
new file mode 100644
index 0000000..9adb2c5
--- /dev/null
+++ b/notmuch/src/threads.rs
@@ -0,0 +1,76 @@
+use std::ops::Drop;
+
+use ffi;
+use Thread;
+use Query;
+use utils::ScopedPhantomcow;
+
+
+#[derive(Debug)]
+pub struct Threads<'d, 'q>
+where
+ 'd: 'q
+{
+ ptr: *mut ffi::notmuch_threads_t,
+ marker: ScopedPhantomcow<'q, Query<'d>>,
+}
+
+impl<'d, 'q> Drop for Threads<'d, 'q>
+where
+ 'd: 'q,
+{
+ fn drop(&mut self) {
+ unsafe { ffi::notmuch_threads_destroy(self.ptr) };
+ }
+}
+
+impl<'d, 'q> Threads<'d, 'q>
+where
+ 'd: 'q,
+{
+ pub(crate) fn from_ptr<P>(ptr: *mut ffi::notmuch_threads_t, owner: P) -> Threads<'d, 'q>
+ where
+ P: Into<ScopedPhantomcow<'q, Query<'d>>>,
+ {
+ Threads {
+ ptr,
+ marker: owner.into(),
+ }
+ }
+}
+
+impl<'d, 'q> Iterator for Threads<'d, 'q>
+where
+ 'd: 'q,
+{
+ type Item = Thread<'d, 'q>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let valid = unsafe { ffi::notmuch_threads_valid(self.ptr) };
+
+ if valid == 0 {
+ return None;
+ }
+
+ let cthrd = unsafe {
+ let thrd = ffi::notmuch_threads_get(self.ptr);
+ ffi::notmuch_threads_move_to_next(self.ptr);
+ thrd
+ };
+
+ Some(Thread::from_ptr(cthrd, ScopedPhantomcow::<'q, Query<'d>>::share(&mut self.marker)))
+ }
+}
+
+
+pub trait ThreadsExt<'d, 'q>
+where
+ 'd: 'q,
+{
+}
+
+impl<'d, 'q> ThreadsExt<'d, 'q> for Threads<'d, 'q> where 'd: 'q {}
+
+
+unsafe impl<'d, 'q> Send for Threads<'d, 'q> where 'd: 'q {}
+unsafe impl<'d, 'q> Sync for Threads<'d, 'q> where 'd: 'q {}
diff --git a/notmuch/src/utils.rs b/notmuch/src/utils.rs
new file mode 100644
index 0000000..b5b90f0
--- /dev/null
+++ b/notmuch/src/utils.rs
@@ -0,0 +1,61 @@
+use libc;
+use std::{ffi, str};
+use std::borrow::Cow;
+use supercow::{Supercow, DefaultFeatures/*, NonSyncFeatures*/};
+use supercow::ext::{BoxedStorage};
+
+
+pub trait ToStr {
+ fn to_str<'a>(&self) -> Result<&'a str, str::Utf8Error>;
+
+ fn to_str_unchecked<'a>(&self) -> &'a str;
+
+ fn to_string_lossy<'a>(&self) -> Cow<'a, str>;
+}
+
+impl ToStr for *const libc::c_char {
+ fn to_str<'a>(&self) -> Result<&'a str, str::Utf8Error> {
+ str::from_utf8(unsafe { ffi::CStr::from_ptr(*self) }.to_bytes())
+ }
+
+ fn to_str_unchecked<'a>(&self) -> &'a str {
+ unsafe { str::from_utf8_unchecked(ffi::CStr::from_ptr(*self).to_bytes()) }
+ }
+
+ fn to_string_lossy<'a>(&self) -> Cow<'a, str> {
+ unsafe { ffi::CStr::from_ptr(*self) }.to_string_lossy()
+ }
+}
+
+pub trait ToString {
+ fn to_string(&self) -> String;
+}
+
+impl ToString for *const libc::c_char {
+ fn to_string(&self) -> String {
+ unsafe { ffi::CStr::from_ptr(*self).to_string_lossy().into_owned() }
+ }
+}
+
+
+#[cfg(not(nonsync))]
+pub type ScopedPhantomcow<'a, OWNED, BORROWED = OWNED,
+ SHARED = Box<dyn DefaultFeatures<'a> + 'a>,
+ STORAGE = BoxedStorage> =
+ Supercow<'a, OWNED, BORROWED, SHARED, STORAGE, ()>;
+
+#[cfg(not(nonsync))]
+pub type ScopedSupercow<'a, OWNED, BORROWED = OWNED, SHARED = Box<dyn DefaultFeatures<'a> + 'a>> =
+ Supercow<'a, OWNED, BORROWED, SHARED, BoxedStorage>;
+
+
+#[cfg(nonsync)]
+pub type ScopedPhantomcow<'a, OWNED, BORROWED = OWNED,
+ SHARED = Box<dyn NonSyncFeatures<'a> + 'a>,
+ STORAGE = BoxedStorage> =
+ Supercow<'a, OWNED, BORROWED, SHARED, STORAGE, ()>;
+
+#[cfg(nonsync)]
+pub type ScopedSupercow<'a, OWNED, BORROWED = OWNED, SHARED = Box<dyn NonSyncFeatures<'a> + 'a>> =
+ Supercow<'a, OWNED, BORROWED, SHARED, BoxedStorage>;
+
diff --git a/notmuch/tests/fixtures.rs b/notmuch/tests/fixtures.rs
new file mode 100644
index 0000000..7075905
--- /dev/null
+++ b/notmuch/tests/fixtures.rs
@@ -0,0 +1,198 @@
+extern crate dirs;
+extern crate tempfile;
+extern crate notmuch;
+extern crate gethostname;
+extern crate maildir;
+extern crate lettre;
+extern crate lettre_email;
+
+use std::ffi::OsStr;
+use std::io::{Result, Write};
+use std::fs::{self, File};
+use std::path::PathBuf;
+use tempfile::{tempdir, TempDir};
+use std::process::Command;
+use maildir::Maildir;
+use lettre_email::{EmailBuilder, Header};
+use lettre::SendableEmail;
+
+
+// A basic test interface to a valid maildir directory.
+//
+// This creates a valid maildir and provides a simple mechanism to
+// deliver test emails to it. It also writes a notmuch-config file
+// in the top of the maildir.
+pub struct MailBox {
+ root_dir: TempDir,
+ maildir: Maildir
+}
+
+impl MailBox {
+
+ // Creates a new maildir fixture. Since this is only used for tests,
+ // may just panic of something is wrong
+ pub fn new() -> Self {
+
+ let root_dir = tempdir().unwrap();
+ let root_path = root_dir.path().to_path_buf();
+
+ let tmp_path = root_path.join("tmp");
+ fs::create_dir(&tmp_path).unwrap();
+
+ let cfg_fname = root_path.join("notmuch-config");
+ let mut cfg_file = File::create(cfg_fname).unwrap();
+ write!(cfg_file, r#"
+ [database]
+ path={tmppath}
+ [user]
+ name=Some Hacker
+ primary_email=dst@example.com
+ [new]
+ tags=unread;inbox;
+ ignore=
+ [search]
+ exclude_tags=deleted;spam;
+ [maildir]
+ synchronize_flags=true
+ [crypto]
+ gpg_path=gpg
+ "#, tmppath=root_path.to_string_lossy()).unwrap();
+
+ let maildir = Maildir::from(root_path.to_path_buf());
+ maildir.create_dirs().unwrap();
+
+ Self {
+ root_dir,
+ maildir
+ }
+ }
+
+ /// Return a new unique message ID
+ // fn next_msgid(&mut self) -> String{
+ // let hostname = gethostname::gethostname();
+ // let msgid = format!("{}@{}", self.idcount, hostname.to_string_lossy());
+ // self.idcount += 1;
+ // msgid
+ // }
+
+ pub fn path(&self) -> PathBuf {
+ self.root_dir.path().into()
+ }
+
+ /// Deliver a new mail message in the mbox.
+ /// This does only adds the message to maildir, does not insert it
+ /// into the notmuch database.
+ /// returns a tuple of (msgid, pathname).
+ pub fn deliver(&self,
+ subject: Option<String>,
+ body: Option<String>,
+ to: Option<String>,
+ from: Option<String>,
+ headers: Vec<(String, String)>,
+ is_new: bool, // Move to new dir or cur dir?
+ _keywords: Option<Vec<String>>, // List of keywords or labels
+ seen: bool, // Seen flag (cur dir only)
+ replied: bool, // Replied flag (cur dir only)
+ flagged: bool) // Flagged flag (cur dir only)
+ -> Result<(String, PathBuf)>
+ {
+
+ let mut builder = EmailBuilder::new()
+ .subject(subject.unwrap_or_else(|| "Test mail".to_string()));
+
+
+ if let Some(val) = body {
+ builder = builder.text(val);
+ }
+
+ builder = builder.to(to.unwrap_or_else(|| "to@example.com".to_string()))
+ .from(from.unwrap_or_else(|| "src@example.com".to_string()));
+
+ for h in headers.into_iter(){
+ let hdr: Header = h.into();
+ builder = builder.header(hdr);
+ }
+
+ let msg:SendableEmail = builder.build().unwrap().into();
+
+ // not sure why lettre doesn't add the host suffix itself
+ let msg_id = msg.message_id().to_string() + ".lettre@localhost";
+ let id = if is_new {
+ self.maildir.store_new(&msg.message_to_string().unwrap().as_bytes()).unwrap()
+ }else{
+ let mut flags = String::from("");
+ if flagged {
+ flags += "F";
+ }
+ if replied {
+ flags += "R";
+ }
+ if seen {
+ flags += "S";
+ }
+ println!("flags: {:?}", flags);
+ let mid = self.maildir.store_cur_with_flags(&msg.message_to_string().unwrap().as_bytes(), flags.as_str()).unwrap();
+
+ // I have no idea what the reasoning for the :2 here is, but ok.
+ format!("{}:2,{}", mid, flags)
+ };
+
+
+ let mut msgpath = self.path();
+ msgpath = if is_new {
+ msgpath.join("new")
+ } else {
+ msgpath.join("cur")
+ };
+
+ msgpath = msgpath.join(&id);
+
+ Ok((msg_id, msgpath))
+ }
+}
+
+impl Drop for MailBox {
+ fn drop(&mut self) {
+ }
+}
+
+
+#[derive(Clone, Debug)]
+pub struct NotmuchCommand {
+ maildir_path: PathBuf
+}
+
+impl NotmuchCommand {
+
+ /// Return a function which runs notmuch commands on our test maildir.
+ ///
+ /// This uses the notmuch-config file created by the ``maildir``
+ /// fixture.
+ pub fn new(maildir_path: &PathBuf) -> Self {
+ Self {
+ maildir_path: maildir_path.clone()
+ }
+ }
+
+ /// Run a notmuch comand.
+ ///
+ /// This function runs with a timeout error as many notmuch
+ /// commands may block if multiple processes are trying to open
+ /// the database in write-mode. It is all too easy to
+ /// accidentally do this in the unittests.
+ pub fn run<I, S>(&self, args: I) -> Result<()>
+ where
+ I: IntoIterator<Item=S>,
+ S: AsRef<OsStr>
+ {
+ let cfg_fname = self.maildir_path.join("notmuch-config");
+
+ Command::new("notmuch").env("NOTMUCH_CONFIG", &cfg_fname)
+ .args(args)
+ .status()?;
+ Ok(())
+ }
+
+}
+
+
diff --git a/notmuch/tests/lib.rs b/notmuch/tests/lib.rs
new file mode 100644
index 0000000..44ec82f
--- /dev/null
+++ b/notmuch/tests/lib.rs
@@ -0,0 +1,15 @@
+extern crate dirs;
+extern crate tempfile;
+extern crate notmuch;
+extern crate gethostname;
+extern crate maildir;
+extern crate lettre;
+extern crate lettre_email;
+
+mod fixtures;
+mod test_database;
+mod test_query;
+mod test_thread;
+mod test_message;
+mod test_tags;
+
diff --git a/notmuch/tests/main.rs b/notmuch/tests/main.rs
new file mode 100644
index 0000000..17db2bc
--- /dev/null
+++ b/notmuch/tests/main.rs
@@ -0,0 +1,62 @@
+extern crate dirs;
+extern crate tempfile;
+extern crate notmuch;
+extern crate gethostname;
+extern crate maildir;
+extern crate lettre;
+extern crate lettre_email;
+
+use std::sync::Arc;
+
+use notmuch::{Query, QueryExt};
+
+mod fixtures;
+use fixtures::{MailBox, NotmuchCommand};
+
+
+
+
+// fn main() {
+// let mut mail_path = dirs::home_dir().unwrap();
+// mail_path.push(".mail");
+
+// let md = MailBox::new();
+// let nmcmd = NotMuchCommand::new(md.path());
+
+// match notmuch::Database::open(
+// &mail_path.to_str().unwrap().to_string(),
+// notmuch::DatabaseMode::ReadOnly,
+// ) {
+// Ok(db) => {
+// #[cfg(feature = "v0_21")]
+// {
+// let rev = db.revision();
+// println!("db revision: {:?}", rev);
+// }
+// let query = {
+// let dbr = Arc::new(db);
+
+// notmuch::Query::create(dbr.clone(), &"".to_string()).unwrap()
+// };
+
+// // let mut threads = query.search_threads().unwrap();
+
+// // let mut threads = db.create_query(&"".to_string()).unwrap().search_threads().unwrap();
+
+// let mut threads = Arc::new(<Query as QueryExt>::search_threads(query).unwrap());
+
+// for thread in Arc::get_mut(&mut threads).unwrap()
+// {
+// println!("thread {:?} {:?}", thread.subject(), thread.authors());
+// }
+// }
+// Err(err) => {
+// println!("Got error while trying to open db: {:?}", err);
+// }
+// }
+// }
+
+
+
+
+
diff --git a/notmuch/tests/test_database.rs b/notmuch/tests/test_database.rs
new file mode 100644
index 0000000..098e271
--- /dev/null
+++ b/notmuch/tests/test_database.rs
@@ -0,0 +1,339 @@
+use fixtures::{NotmuchCommand, MailBox};
+
+// #[test]
+// // fn test_config_pathname_default(){
+
+// // monkeypatch.delenv('NOTMUCH_CONFIG', raising=False)
+// // user = pathlib.Path('~/.notmuch-config').expanduser()
+// // assert dbmod._config_pathname() == user
+
+// // }
+
+mod database {
+
+ use super::*;
+
+ #[test]
+ fn test_create(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path());
+ assert!(db.is_ok());
+
+ assert!(mailbox.path().join(".notmuch/xapian").exists());
+ }
+
+ #[test]
+ fn test_create_already_open(){
+ let mailbox = MailBox::new();
+ let db1 = notmuch::Database::create(&mailbox.path());
+ assert!(db1.is_ok());
+
+ let db2 = notmuch::Database::create(&mailbox.path());
+ assert!(db2.is_err());
+ }
+
+
+ #[test]
+ fn test_create_existing(){
+ let mailbox = MailBox::new();
+ notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let db2 = notmuch::Database::create(&mailbox.path());
+ assert!(db2.is_err());
+ }
+
+
+ #[test]
+ fn test_close(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ assert!(db.close().is_ok());
+ }
+
+ #[test]
+ fn test_drop_noclose(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ drop(db);
+ }
+
+ #[test]
+ fn test_close_drop(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+ db.close().unwrap();
+ drop(db);
+ }
+
+ #[test]
+ fn test_path(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+ assert_eq!(db.path(), mailbox.path());
+ }
+
+ #[test]
+ fn test_version(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+ assert!(db.version() > 0);
+ }
+
+}
+
+
+mod atomic {
+ // use super::*;
+
+ // TODO: how do I test this??
+
+}
+
+
+#[cfg(feature = "v0_21")]
+mod revision {
+ use super::*;
+
+ #[test]
+ fn test_single_rev(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let rev0 = db.revision();
+ let rev1 = db.revision();
+
+ assert!(rev0 == rev1);
+ assert!(rev0 <= rev1);
+ assert!(rev0 >= rev1);
+
+ assert!(!(rev0 < rev1));
+ assert!(!(rev0 > rev1));
+ }
+
+ #[test]
+ fn test_diff_db(){
+ let mailbox0 = MailBox::new();
+ let db0 = notmuch::Database::create(&mailbox0.path()).unwrap();
+ let rev0 = db0.revision();
+
+
+ let mailbox1 = MailBox::new();
+ let db1 = notmuch::Database::create(&mailbox1.path()).unwrap();
+ let rev1 = db1.revision();
+
+ assert_ne!(rev0, rev1);
+ assert_ne!(rev0.uuid, rev1.uuid);
+ }
+
+ #[test]
+ fn test_cmp(){
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let rev0 = db.revision();
+
+ let (_, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+
+ db.index_file(&filename, None).unwrap();
+
+ let rev1 = db.revision();
+
+ assert!(rev0 < rev1);
+ assert!(rev0 <= rev1);
+ assert!(!(rev0 > rev1));
+ assert!(!(rev0 >= rev1));
+ assert!(!(rev0 == rev1));
+ assert!(rev0 != rev1);
+
+
+ }
+
+ // TODO: add tests for revisions comparisons
+
+}
+
+
+mod messages {
+ use super::*;
+
+ #[test]
+ fn test_add_message() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+ let msg = db.index_file(&filename, None).unwrap();
+
+ assert_eq!(msg.filename(), filename);
+ assert_eq!(msg.id(), msgid);
+
+ }
+
+ #[test]
+ fn test_remove_message() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+ db.index_file(&filename, None).unwrap();
+ assert!(db.find_message(&msgid).unwrap().is_some());
+
+ db.remove_message(&filename).unwrap();
+ assert!(db.find_message(&msgid).unwrap().is_none());
+ }
+
+ #[test]
+ fn test_find_message() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+ let msg0 = db.index_file(&filename, None).unwrap();
+
+ let msg1 = db.find_message(&msgid).unwrap().unwrap();
+ assert_eq!(msg0.id(), msgid);
+ assert_eq!(msg0.id(), msg1.id());
+
+ assert_eq!(msg0.filename(), filename);
+ assert_eq!(msg0.filename(), msg1.filename());
+ }
+
+ #[test]
+ fn test_find_message_notfound() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ assert!(db.find_message(&"foo").unwrap().is_none());
+ }
+
+}
+
+mod tags {
+ use super::*;
+
+ #[test]
+ fn test_none() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let tags = db.all_tags().unwrap();
+
+ assert_eq!(tags.count(), 0);
+ }
+
+ #[test]
+ fn test_some() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+ let (_, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+ let msg = db.index_file(&filename, None).unwrap();
+
+ msg.add_tag(&"hello").unwrap();
+ let tags: Vec<String> = db.all_tags().unwrap().collect();
+
+ assert_eq!(tags.len(), 1);
+ assert!(tags.iter().any(|x| x == "hello"));
+ }
+
+ #[test]
+ fn test_iters() {
+ let mailbox = MailBox::new();
+ let db = notmuch::Database::create(&mailbox.path()).unwrap();
+
+ let t1: Vec<String> = db.all_tags().unwrap().collect();
+ let t2: Vec<String> = db.all_tags().unwrap().collect();
+ assert_eq!(t1, t2);
+ }
+
+}
+
+struct DatabaseFixture {
+ // Return a read-write Database.
+ // The database will have 3 messages, 2 threads.
+
+ pub mailbox: MailBox,
+ pub database: notmuch::Database,
+}
+
+impl DatabaseFixture {
+ pub fn new() -> Self{
+ let mailbox = MailBox::new();
+
+ let (msgid, _) = mailbox.deliver(None, Some("foo".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("bar".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("baz".to_string()), None, None, vec![("In-Reply-To".to_string(), format!("<{}>", msgid))], true, None, false, false, false).unwrap();
+
+ let cmd = NotmuchCommand::new(&mailbox.path());
+ cmd.run(vec!["new"]).unwrap();
+
+ let database = notmuch::Database::open(&mailbox.path(), notmuch::DatabaseMode::ReadWrite).unwrap();
+
+ Self {
+ mailbox,
+ database
+ }
+ }
+}
+
+mod query {
+ use super::*;
+
+ #[test]
+ fn test_count_messages() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("*").unwrap();
+ assert_eq!(query.count_messages().unwrap(), 3);
+ }
+
+ #[test]
+ fn test_message_no_results() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("not_a_matching_query").unwrap();
+ let mut messages = query.search_messages().unwrap();
+ let msg = messages.next();
+ assert!(msg.is_none());
+ }
+
+ #[test]
+ fn test_message_match() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("*").unwrap();
+ let mut messages = query.search_messages().unwrap();
+ let msg = messages.next();
+ assert!(msg.is_some());
+ }
+
+ #[test]
+ fn test_count_threads() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("*").unwrap();
+ assert_eq!(query.count_threads().unwrap(), 2);
+ }
+
+ #[test]
+ fn test_threads_no_results() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("not_a_matching_query").unwrap();
+ let mut threads = query.search_threads().unwrap();
+ let thrd = threads.next();
+ assert!(thrd.is_none());
+ }
+
+ #[test]
+ fn test_threads_match() {
+ let db = DatabaseFixture::new();
+
+ let query = db.database.create_query("*").unwrap();
+ let mut threads = query.search_threads().unwrap();
+ let thrd = threads.next();
+ assert!(thrd.is_some());
+ }
+}
+
diff --git a/notmuch/tests/test_message.rs b/notmuch/tests/test_message.rs
new file mode 100644
index 0000000..7af6f2c
--- /dev/null
+++ b/notmuch/tests/test_message.rs
@@ -0,0 +1,291 @@
+use std::sync::Arc;
+use std::path::PathBuf;
+use fixtures::MailBox;
+
+struct MessageFixture {
+ // Return a single thread with 2 messages
+ pub mailbox: MailBox,
+ pub database: Arc<notmuch::Database>,
+ pub maildir_msg: (String, PathBuf),
+ pub message: notmuch::Message<'static, notmuch::Database>,
+}
+
+impl MessageFixture {
+ pub fn new() -> Self{
+ let mailbox = MailBox::new();
+
+ let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap();
+
+ let database = Arc::new(notmuch::Database::create(&mailbox.path()).unwrap());
+ let message = <notmuch::Database as notmuch::DatabaseExt>::index_file(database.clone(), &filename, None).unwrap();
+
+ Self {
+ mailbox,
+ database,
+ maildir_msg: (msgid, filename),
+ message
+ }
+ }
+}
+
+mod message {
+
+ use super::*;
+
+ #[test]
+ fn test_messageid() {
+ let msg = MessageFixture::new();
+ let copy = <notmuch::Database as notmuch::DatabaseExt>::find_message_by_filename(msg.database.clone(), &msg.message.filename()).unwrap().unwrap();
+ assert_eq!(msg.message.id(), copy.id())
+ }
+
+ #[test]
+ fn test_messageid_find() {
+ let msg = MessageFixture::new();
+ let copy = <notmuch::Database as notmuch::DatabaseExt>::find_message(msg.database.clone(), &msg.message.id()).unwrap().unwrap();
+ assert_eq!(msg.message.id(), copy.id())
+ }
+
+ #[test]
+ fn test_path() {
+ let msg = MessageFixture::new();
+ assert_eq!(msg.message.filename(), msg.maildir_msg.1)
+ }
+
+
+ #[test]
+ fn test_filenames() {
+ let msg = MessageFixture::new();
+ let mut filenames = msg.message.filenames();
+ let filename = filenames.next().unwrap();
+
+ assert_eq!(filename, msg.message.filename());
+
+ assert!(filenames.next().is_none());
+ let names: Vec<PathBuf> = msg.message.filenames().collect();
+
+ assert_eq!(names, vec![msg.maildir_msg.1]);
+ }
+
+ #[test]
+ fn test_header() {
+ let msg = MessageFixture::new();
+ assert_eq!(msg.message.header(&"from").unwrap().unwrap().to_string(), "<src@example.com>");
+ }
+
+ #[test]
+ fn test_header_not_present() {
+ let msg = MessageFixture::new();
+ assert_eq!(msg.message.header(&"foo").unwrap(), None);
+ }
+
+ #[test]
+ fn test_freeze() {
+ let msg = MessageFixture::new();
+
+ msg.message.freeze().unwrap();
+ msg.message.add_tag(&"foo").unwrap();
+ msg.message.add_tag(&"bar").unwrap();
+ msg.message.remove_tag(&"foo").unwrap();
+ msg.message.thaw().unwrap();
+
+ assert!(msg.message.tags().all(|x| x != "foo"));
+ assert!(msg.message.tags().any(|x| x == "bar"));
+ }
+
+ #[test]
+ fn test_freeze_context() {
+ let msg = MessageFixture::new();
+
+ {
+ let _frozen = notmuch::FrozenMessage::new(&msg.message).unwrap();
+ msg.message.add_tag(&"foo").unwrap();
+ msg.message.add_tag(&"bar").unwrap();
+ msg.message.remove_tag(&"foo").unwrap();
+
+ }
+ assert!(msg.message.tags().all(|x| x != "foo"));
+ assert!(msg.message.tags().any(|x| x == "bar"));
+ }
+
+
+ #[test]
+ fn test_freeze_err() {
+ // not sure if this test is ok?
+ let msg = MessageFixture::new();
+
+ msg.message.add_tag(&"foo").unwrap();
+
+ msg.message.freeze().unwrap();
+ assert!(msg.message.remove_all_tags().is_ok());
+
+ let copy = <notmuch::Database as notmuch::DatabaseExt>::find_message(msg.database.clone(), &msg.message.id()).unwrap().unwrap();
+ assert!(copy.tags().any(|x| x == "foo"));
+
+ msg.message.thaw().unwrap();
+
+ assert!(!msg.message.tags().any(|x| x == "foo"));
+
+ let copy2 = <notmuch::Database as notmuch::DatabaseExt>::find_message(msg.database.clone(), &msg.message.id()).unwrap().unwrap();
+ assert!(!copy2.tags().any(|x| x == "foo"));
+ }
+
+ #[test]
+ fn test_freeze_context_err() {
+ // not sure if this test is ok?
+ let msg = MessageFixture::new();
+ msg.message.add_tag(&"foo").unwrap();
+
+ {
+ let _frozen = notmuch::FrozenMessage::new(&msg.message).unwrap();
+ assert!(msg.message.remove_all_tags().is_ok());
+ assert!(!msg.message.tags().any(|x| x == "foo"));
+
+ let copy = <notmuch::Database as notmuch::DatabaseExt>::find_message(msg.database.clone(), &msg.message.id()).unwrap().unwrap();
+ assert!(copy.tags().any(|x| x == "foo"));
+ }
+
+ let copy2 = <notmuch::Database as notmuch::DatabaseExt>::find_message(msg.database.clone(), &msg.message.id()).unwrap().unwrap();
+ assert!(!copy2.tags().any(|x| x == "foo"));
+ assert!(!msg.message.tags().any(|x| x == "foo"));
+ }
+
+ #[test]
+ fn test_replies() {
+ let msg = MessageFixture::new();
+ assert_eq!(msg.message.replies().count(), 0);
+ }
+
+}
+
+
+// def test_date(self, msg):
+// # XXX Someone seems to treat things as local time instead of
+// # UTC or the other way around.
+// now = int(time.time())
+// assert abs(now - msg.date) < 3600*24
+
+mod properties {
+ use super::*;
+
+ #[test]
+ fn test_add_single() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"bar").unwrap();
+ assert_eq!(msg.message.property(&"foo").unwrap(), "bar");
+
+ msg.message.add_property(&"bar", &"baz").unwrap();
+ assert_eq!(msg.message.property(&"bar").unwrap(), "baz");
+ }
+
+ #[test]
+ fn test_add_dup() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"bar").unwrap();
+ msg.message.add_property(&"foo", &"baz").unwrap();
+
+ assert_eq!(msg.message.property(&"foo").unwrap(), "bar");
+
+ let props = msg.message.properties(&"foo", true);
+ let expect = vec![("foo", "bar"), ("foo", "baz")];
+ for (&(ek, ev), (pk, pv)) in expect.iter().zip(props) {
+ assert_eq!(ek, pk);
+ assert_eq!(ev, pv);
+ }
+ }
+
+ #[test]
+ fn test_len() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+ msg.message.add_property(&"foo", &"b").unwrap();
+ msg.message.add_property(&"bar", &"a").unwrap();
+
+ let num_props = msg.message.properties(&"", false).count();
+ assert_eq!(num_props, 3);
+
+ let mut prop_keys: Vec<String> = msg.message.properties(&"", false).map(|x| x.0).collect();
+ prop_keys.sort();
+ prop_keys.dedup();
+ assert_eq!(prop_keys.len(), 2);
+
+ let mut prop_vals: Vec<String> = msg.message.properties(&"", false).map(|x| x.1).collect();
+ prop_vals.sort();
+ prop_vals.dedup();
+ assert_eq!(prop_vals.len(), 2);
+ }
+
+ #[test]
+ fn test_del() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+ msg.message.add_property(&"foo", &"b").unwrap();
+
+ msg.message.remove_all_properties(Some(&"foo")).unwrap();
+ assert!(msg.message.property(&"foo").is_err());
+ }
+
+ #[test]
+ fn test_remove() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+ msg.message.add_property(&"foo", &"b").unwrap();
+
+ msg.message.remove_property(&"foo", &"a").unwrap();
+ assert_eq!(msg.message.property(&"foo").unwrap(), "b");
+ }
+
+ #[test]
+ fn test_clear() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+
+ msg.message.remove_all_properties(None).unwrap();
+ assert!(msg.message.property(&"foo").is_err());
+ }
+
+ #[test]
+ fn test_getall() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+
+ let prop_keys: Vec<String> = msg.message.properties(&"foo", false).map(|x| x.0).collect();
+ assert_eq!(prop_keys.len(), 1);
+ assert_eq!(prop_keys, vec!["foo"]);
+
+ let prop_vals: Vec<String> = msg.message.properties(&"foo", false).map(|x| x.1).collect();
+ assert_eq!(prop_vals.len(), 1);
+ assert_eq!(prop_vals, vec!["a"]);
+ }
+
+ #[test]
+ fn test_getall_prefix() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+ msg.message.add_property(&"foobar", &"b").unwrap();
+
+ let prop_keys: Vec<String> = msg.message.properties(&"foo", false).map(|x| x.0).collect();
+ assert_eq!(prop_keys.len(), 2);
+ assert_eq!(prop_keys, vec!["foo", "foobar"]);
+
+ let prop_vals: Vec<String> = msg.message.properties(&"foo", false).map(|x| x.1).collect();
+ assert_eq!(prop_vals.len(), 2);
+ assert_eq!(prop_vals, vec!["a", "b"]);
+ }
+
+ #[test]
+ fn test_getall_exact() {
+ let msg = MessageFixture::new();
+ msg.message.add_property(&"foo", &"a").unwrap();
+ msg.message.add_property(&"foobar", &"b").unwrap();
+
+ let prop_keys: Vec<String> = msg.message.properties(&"foo", true).map(|x| x.0).collect();
+ assert_eq!(prop_keys.len(), 1);
+ assert_eq!(prop_keys, vec!["foo"]);
+
+ let prop_vals: Vec<String> = msg.message.properties(&"foo", true).map(|x| x.1).collect();
+ assert_eq!(prop_vals.len(), 1);
+ assert_eq!(prop_vals, vec!["a"]);
+ }
+}
+
diff --git a/notmuch/tests/test_query.rs b/notmuch/tests/test_query.rs
new file mode 100644
index 0000000..ad8f299
--- /dev/null
+++ b/notmuch/tests/test_query.rs
@@ -0,0 +1,97 @@
+use std::sync::Arc;
+use fixtures::{NotmuchCommand, MailBox};
+
+
+struct QueryFixture {
+ // Return a single thread with 2 messages
+ pub mailbox: MailBox,
+ pub query: notmuch::Query<'static>,
+}
+
+impl QueryFixture {
+ pub fn new() -> Self{
+ let mailbox = MailBox::new();
+
+ let (msgid, _) = mailbox.deliver(None, Some("foo".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("bar".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("baz".to_string()), None, None, vec![("In-Reply-To".to_string(), format!("<{}>", msgid))], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("foo qux".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("foo quux".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+
+ let cmd = NotmuchCommand::new(&mailbox.path());
+ cmd.run(vec!["new"]).unwrap();
+
+ let query = {
+ let database = Arc::new(notmuch::Database::open(&mailbox.path(), notmuch::DatabaseMode::ReadWrite).unwrap());
+
+ notmuch::Query::create(database, &"foo".to_string()).unwrap()
+ };
+
+ Self {
+ mailbox,
+ query
+ }
+ }
+}
+
+#[test]
+fn test_iter_threads() {
+ let q = QueryFixture::new();
+
+ let threads = q.query.search_threads().unwrap();
+
+ let mut num = 0;
+ for _thread in threads {
+ num += 1;
+ }
+
+ assert_eq!(num, 3);
+
+}
+
+#[test]
+fn test_iter_threads_ext() {
+ let q = QueryFixture::new();
+
+ let threads = <notmuch::Query as notmuch::QueryExt>::search_threads(q.query).unwrap();
+
+ let mut num = 0;
+ for _thread in threads {
+ num += 1;
+ }
+
+ assert_eq!(num, 3);
+
+}
+
+
+#[test]
+fn test_iter_messages() {
+ let q = QueryFixture::new();
+
+ let messages = q.query.search_messages().unwrap();
+
+ let mut num = 0;
+ for _message in messages {
+ num += 1;
+ }
+
+ assert_eq!(num, 3);
+
+}
+
+#[test]
+fn test_iter_messages_ext() {
+ let q = QueryFixture::new();
+
+ let messages = <notmuch::Query as notmuch::QueryExt>::search_messages(q.query).unwrap();
+
+ let mut num = 0;
+ for _message in messages {
+ num += 1;
+ }
+
+ assert_eq!(num, 3);
+
+}
+
diff --git a/notmuch/tests/test_tags.rs b/notmuch/tests/test_tags.rs
new file mode 100644
index 0000000..3d39486
--- /dev/null
+++ b/notmuch/tests/test_tags.rs
@@ -0,0 +1,142 @@
+use std::sync::Arc;
+use fixtures::{MailBox, NotmuchCommand};
+
+struct TagSetFixture {
+ // An non-empty immutable tagset.
+ // This will have the default new mail tags: inbox, unread.
+ pub mailbox: MailBox,
+ pub cmd: NotmuchCommand,
+ pub database: Arc<notmuch::Database>,
+ pub message: notmuch::Message<'static, notmuch::Database>
+}
+
+impl TagSetFixture {
+ pub fn new(mutable: bool, flagged: bool) -> Self{
+ let mailbox = MailBox::new();
+ let (_msg, filename) = mailbox.deliver(None, None, None, None, vec![], !flagged, None, false, false, flagged).unwrap();
+
+ let cmd = NotmuchCommand::new(&mailbox.path());
+ cmd.run(vec!["new"]).unwrap();
+
+ let database = Arc::new(notmuch::Database::open(&mailbox.path(), if !mutable {notmuch::DatabaseMode::ReadOnly} else { notmuch::DatabaseMode::ReadWrite }).unwrap());
+ let message = <notmuch::Database as notmuch::DatabaseExt>::find_message_by_filename(database.clone(), &filename).unwrap().unwrap();
+
+ Self {
+ mailbox,
+ database,
+ cmd,
+ message
+ }
+ }
+}
+
+mod immutable {
+
+ use super::*;
+
+ #[test]
+ fn test_neg(){
+ let tagset = TagSetFixture::new(false, false);
+
+ let tags: Vec<String> = tagset.database.all_tags().unwrap().collect();
+ tagset.cmd.run(vec!["tag", "+foo", "*"]).unwrap();
+
+ let database = notmuch::Database::open(&tagset.mailbox.path(), notmuch::DatabaseMode::ReadOnly).unwrap();
+ let ntags: Vec<String> = database.all_tags().unwrap().collect();
+
+ assert_ne!(tags, ntags);
+ }
+
+ #[test]
+ fn test_contains(){
+ let tagset = TagSetFixture::new(false, false);
+ let tags: Vec<String> = tagset.database.all_tags().unwrap().collect();
+
+ assert!(tags.iter().any(|x| x == "unread"));
+ assert!(!tags.iter().any(|x| x == "foo"));
+ }
+
+
+ #[test]
+ fn test_len(){
+ let tagset = TagSetFixture::new(false, false);
+ assert_eq!(tagset.database.all_tags().unwrap().count(), 2);
+ }
+
+}
+
+mod mutable {
+
+ use super::*;
+
+ #[test]
+ fn test_add(){
+ let tagset = TagSetFixture::new(true, false);
+ assert!(!tagset.message.tags().any(|x| x == "foo"));
+
+ tagset.message.add_tag("foo").unwrap();
+ assert!(tagset.message.tags().any(|x| x == "foo"));
+ }
+
+ #[test]
+ fn test_discard(){
+ let tagset = TagSetFixture::new(true, false);
+ assert!(tagset.message.tags().any(|x| x == "inbox"));
+
+ tagset.message.remove_tag("inbox").unwrap();
+ assert!(!tagset.message.tags().any(|x| x == "unbox"));
+ }
+
+ #[test]
+ fn test_discard_not_present(){
+ let tagset = TagSetFixture::new(true, false);
+ assert!(!tagset.message.tags().any(|x| x == "foo"));
+
+ tagset.message.remove_tag("foo").unwrap();
+ }
+
+ #[test]
+ fn test_clear(){
+ let tagset = TagSetFixture::new(true, false);
+ assert!(tagset.message.tags().count() > 0);
+ tagset.message.remove_all_tags().unwrap();
+
+ assert!(tagset.message.tags().count() == 0);
+ }
+
+ #[test]
+ fn test_from_maildir_flags(){
+ let tagset = TagSetFixture::new(true, true);
+
+ tagset.message.remove_tag(&"flagged").unwrap();
+ tagset.message.maildir_flags_to_tags().unwrap();
+
+ assert!(tagset.message.tags().any(|x| x == "flagged"));
+ }
+
+
+ #[test]
+ fn test_to_maildir_flags(){
+
+ let tagset = TagSetFixture::new(true, true);
+
+ let filename = tagset.message.filename();
+ let filestr = filename.to_string_lossy();
+
+ let file_parts: Vec<&str> = filestr.split(',').collect();
+ let flags = file_parts.last().unwrap();
+ println!("Flags {:?}", flags);
+
+ assert!(flags.contains('F'));
+ tagset.message.remove_tag(&"flagged").unwrap();
+ tagset.message.tags_to_maildir_flags().unwrap();
+
+ let filename = tagset.message.filename();
+ let filestr = filename.to_string_lossy();
+
+ let file_parts: Vec<&str> = filestr.split(',').collect();
+ let flags = file_parts.last().unwrap();
+ assert!(!flags.contains('F'));
+ }
+
+} \ No newline at end of file
diff --git a/notmuch/tests/test_thread.rs b/notmuch/tests/test_thread.rs
new file mode 100644
index 0000000..89f1ea0
--- /dev/null
+++ b/notmuch/tests/test_thread.rs
@@ -0,0 +1,102 @@
+use std::sync::Arc;
+use fixtures::{NotmuchCommand, MailBox};
+
+
+struct ThreadFixture {
+ // Return a single thread with 2 messages
+ pub mailbox: MailBox,
+ pub thread: notmuch::Thread<'static, 'static>,
+}
+
+impl ThreadFixture {
+ pub fn new() -> Self{
+ let mailbox = MailBox::new();
+
+ let (msgid, _) = mailbox.deliver(None, Some("foo".to_string()), None, None, vec![], true, None, false, false, false).unwrap();
+ mailbox.deliver(None, Some("bar".to_string()), None, None, vec![("In-Reply-To".to_string(), format!("<{}>", msgid))], true, None, false, false, false).unwrap();
+
+ let cmd = NotmuchCommand::new(&mailbox.path());
+ cmd.run(vec!["new"]).unwrap();
+
+ let mut threads = {
+ let database = Arc::new(notmuch::Database::open(&mailbox.path(), notmuch::DatabaseMode::ReadWrite).unwrap());
+
+ let query = notmuch::Query::create(database.clone(), &"foo".to_string()).unwrap();
+
+ <notmuch::Query as notmuch::QueryExt>::search_threads(query).unwrap()
+ };
+ let thread = threads.next().unwrap();
+
+ Self {
+ mailbox,
+ thread
+ }
+ }
+}
+
+#[test]
+fn test_threadid() {
+ let thread = ThreadFixture::new();
+ assert!(!thread.thread.id().is_empty());
+}
+
+
+#[test]
+fn test_toplevel() {
+ let thread = ThreadFixture::new();
+ let msgs = thread.thread.toplevel_messages();
+
+ assert_eq!(msgs.count(), 1);
+}
+
+
+#[test]
+fn test_toplevel_reply() {
+ let thread = ThreadFixture::new();
+ let msg = thread.thread.toplevel_messages().next().unwrap();
+
+ assert_eq!(msg.replies().count(), 1);
+}
+
+#[test]
+fn test_iter() {
+ let thread = ThreadFixture::new();
+ let msg_count0 = thread.thread.messages().count() as i32;
+ let msg_count1 = thread.thread.total_messages();
+
+ assert_eq!(msg_count0, msg_count1);
+}
+
+#[test]
+fn test_matched() {
+ let thread = ThreadFixture::new();
+ assert_eq!(thread.thread.matched_messages(), 1);
+}
+
+
+#[test]
+fn test_authors() {
+ let thread = ThreadFixture::new();
+
+ assert_eq!(thread.thread.authors(), vec!["src@example.com".to_string()]);
+}
+
+
+#[test]
+fn test_subject() {
+ let thread = ThreadFixture::new();
+
+ println!("{:?}", thread.thread.subject());
+ assert_eq!(thread.thread.subject(), "Test mail");
+}
+
+
+
+#[test]
+fn test_tags() {
+ let thread = ThreadFixture::new();
+
+ let tags: Vec<String> = thread.thread.tags().collect();
+ assert!(tags.iter().any(|x| x == "inbox"));
+}
+ \ No newline at end of file