Import notmuch_0.38.2.orig.tar.xz

[dgit import orig notmuch_0.38.2.orig.tar.xz]
This commit is contained in:
David Bremner 2023-12-01 07:51:09 -04:00
commit 126347b694
930 changed files with 137984 additions and 0 deletions

25
.dir-locals.el Normal file
View file

@ -0,0 +1,25 @@
;; emacs local configuration settings for notmuch source
;; surmised by dkg on 2010-11-23 13:43:18-0500
;; amended by amdragon on 2011-06-06
((c-mode
(indent-tabs-mode . t)
(tab-width . 8)
(c-basic-offset . 4)
(c-file-style . "linux"))
(c++-mode
(indent-tabs-mode . t)
(tab-width . 8)
(c-basic-offset . 4)
(c-file-style . "linux"))
(emacs-lisp-mode
(indent-tabs-mode . t)
(tab-width . 8))
(sh-mode
(indent-tabs-mode . t)
(tab-width . 8)
(sh-basic-offset . 4)
(sh-indentation . 4))
(nil
(fill-column . 70))
)

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
*.[ao]
*.stamp
*cscope*
*~
.*.swp
/.deps
/.first-build-message
/.stamps
/Makefile.config
/bindings/python-cffi/build/
/lib/libnotmuch*.dylib
/lib/libnotmuch.so*
/nmbug
/notmuch
/notmuch-git
/notmuch-shared
/releases
/sh.config
/sphinx.config
/version.stamp
/bindings/python-cffi/_notmuch_config.py
TAGS
tags

8
.mailmap Normal file
View file

@ -0,0 +1,8 @@
Peter Feigl <craven@gmx.net>
Nate <nstraz@redhat.com>
Ali Polatel <alip@penguen.ev>
Stefan <aeuii@posteo.de>
Patrick Totzke <patricktotzke@googlemail.com>
Patrick Totzke <patricktotzke@gmail.com>
Patrick Totzke <p.totzke@ed.ac.uk>
Mark Walters <markwalters1009@gmail.com>

29
.travis.yml Normal file
View file

@ -0,0 +1,29 @@
language: c
dist: bionic
addons:
apt:
sources:
- sourceline: 'ppa:xapian-backports/ppa'
packages:
- dtach
- libxapian-dev
- libgmime-3.0-dev
- libtalloc-dev
- python3-sphinx
- python3-cffi
- python3-pytest
- python3-setuptools
- libpython3-all-dev
- gpgsm
script:
- ./configure
- make test
notifications:
irc:
channels:
- "irc.libera.chat#notmuch"
on_success: change

129
AUTHORS Normal file
View file

@ -0,0 +1,129 @@
Carl Worth <cworth@cworth.org> was the original author of Notmuch.
David Bremner has maintained Notmuch since release 0.6 (2011). But
there's really not much that they've done. There's been a lot of
standing on shoulders here:
William Morgan deserves credit for providing the primary inspiration
for Notmuch with his program Sup (https://sup-heliotrope.github.io/).
Some people have contributed code that has made it into Notmuch
without their specific knowledge (but with their full permission
thanks to the GNU General Public License). This includes:
Brian Gladman (with Mikhail Gusarov <dottedmag@dottedmag.net>)
Implementation of SHA-1 (nice and small) (libsha1.c)
Please see the various files in the Notmuch distribution for
individual copyright statements.
And of course, though their code isn't distributed here, Notmuch would
be not much of anything without the contributors to Xapian, the search
engine that does the really heavy lifting, as well as the various
system libraries, compilers, and the kernel that make it all work
(thanks GNU, thanks Linux). Thanks to everyone who has played a part!
The following list of people have at least 15 lines of code in the
Notmuch 0.31 release (calculated by devel/author-scan.sh).
David Bremner
Carl Worth
Jani Nikula
Austin Clements
Daniel Kahn Gillmor
Mark Walters
Floris Bruynooghe
David Edmondson
Tomi Ollila
Sebastian Spaeth
Ali Polatel
Michal Sojka
Justus Winter
Sebastien Binet
W. Trevor King
Jameson Graef Rollins
Felipe Contreras
Jonas Bernoulli
Pieter Praet
Peter Feigl
Dmitry Kurochkin
Peter Wang
Gregor Zattler
Daniel Schoepe
Keith Packard
Adam Wolfe Gordon
Stefano Zacchiroli
Vincent Breitmoser
laochailan
Ben Gamari
Aaron Ecay
l-m-h@web.de
Thomas Jost
Jesse Rosenthal
Dirk Hohndel
Blake Jones
Damien Cassou
Anton Khirnov
Matt Armstrong
Vladimir Panteleev
William Casarin
Örjan Ekeberg
Jan Janak
Patrick Totzke
Ruben Pollan
rhn
Ioan-Adrian Ratiu
Ethan Glasser-Camp
Chunyang Xu
Todd
Chris Wilson
Yuri Volchkov
Cédric Cabessa
Mark Anderson
Jed Brown
Maxime Coste
Ludovic LANGE
Sebastian Poeplau
Mikhail
Keith Amidon
Gaute Hope
martin f. krafft
Jeffrey C. Ollie
Jameson Rollins
Scott Henson
Bart Trojanowski
Vladimir Marek
Servilio Afre Puentes
Tomas Carnecky
Kevin McCarthy
Kevin J. McCarthy
Scott Robinson
Wael M. Nasreddine
Charles Celerier
Olly Betts
Istvan Marko
Florian Klink
Thibaut Horel
Joel Borggrén-Franck
Ingmar Vanhassel
Olivier Taïbi
Ian Main
Alexander Botero-Lowry
Luis Ressel
Sergei Shilovsky
Trevor Jim
Uli Scholler
Matthew Lear
Jinwoo Lee
Amadeusz Żołnowski
Here is an incomplete list of other people that have made
contributions to Notmuch (whether by code, bug reporting/fixes,
ideas, inspiration, testing or feedback):
Martin Krafft
Jamey Sharp
The Notmuch project acknowledges the contributions of the following
organizations via their employees
Google LLC

15
COPYING Normal file
View file

@ -0,0 +1,15 @@
Notmuch 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, (in the COPYING-GPL-3 file in this
directory). If not, see https://www.gnu.org/licenses/

676
COPYING-GPL-3 Normal file
View file

@ -0,0 +1,676 @@
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/philosophy/why-not-lgpl.html>.

106
INSTALL Normal file
View file

@ -0,0 +1,106 @@
Build and install instructions for Notmuch.
Compilation commands
--------------------
The process for compiling and installing Notmuch is the very standard
sequence of:
./configure
make
sudo make install
In fact, if you don't plan to pass any arguments to the configure
script, then you can skip that step and just start with "make", (which
will call configure for you). See this command:
./configure --help
for detailed documentation of the things you can control at the
configure stage.
Dependencies
------------
Notmuch depends on four libraries: Xapian, GMime 3.0,
Talloc, and zlib which are each described below:
Xapian
------
Xapian is the search-engine library underlying Notmuch.
It provides all the real machinery of indexing and searching,
(including the very nice parsing of the query string).
Xapian is available from https://xapian.org
GMime
-----
GMime provides decoding of MIME email messages for Notmuch.
Without GMime, Notmuch would not be able to extract and index
the actual text from email message encoded as BASE64, etc.
GMime is available from https://github.com/jstedfast/gmime
Sfsexp
------
sfsexp is the "small fast s-expression" library. Notmuch
optionally use it to provide a second query parser.
sfsexp is available from https://github.com/mjsottile/sfsexp.
In Debian Bookworm and later, install libsexp-dev.
Talloc
------
Talloc is a memory-pool allocator used by Notmuch.
Talloc is an extremely lightweight and easy-to-use tool for
allocating memory in a hierarchical fashion and then freeing
it with a single call of the top-level handle. Using it has
made development of Notmuch much easier and much less prone to
memory leaks.
Talloc is available from https://talloc.samba.org/
zlib
----
zlib is an extremely popular compression library. It is used
by Xapian, so if you installed that you will already have
zlib. You may need to install the zlib headers separately.
Notmuch needs the transparent write feature of zlib introduced
in version 1.2.5.2 (Dec. 2011).
zlib is available from https://zlib.net
Building Documentation
----------------------
To build the documentation for notmuch you need at least version 1.0
of sphinx (Jul. 2010).
Sphinx is available from www.sphinx-doc.org.
To install the documentation as "info" pages, you will need the
additional tools makeinfo and install-info.
Installing Dependencies from Packages
-------------------------------------
On a modern, package-based operating system you can install all of the
dependencies with a single simple command line. For example:
For Debian and similar:
sudo apt-get install libxapian-dev libgmime-3.0-dev libtalloc-dev zlib1g-dev python3-sphinx texinfo install-info
For Fedora and similar:
sudo dnf install xapian-core-devel gmime30-devel libtalloc-devel zlib-devel python3-sphinx texinfo info
On other systems, a similar command can be used, but the details of
the package names may be different.

71
Makefile Normal file
View file

@ -0,0 +1,71 @@
# We want the all target to be the implicit target (if no target is
# given explicitly on the command line) so mention it first.
all:
# Sub-directory Makefile.local fragments can append to these variables
# to have directory-specific cflags as necessary.
extra_cflags :=
extra_cxxflags :=
# Get settings from the output of configure by running it to generate
# Makefile.config if it doesn't exist yet.
# If Makefile.config doesn't exist, then srcdir won't be
# set. Conditionally set it (assuming a plain srcdir build) so that
# the rule to generate Makefile.config can actually work.
srcdir ?= .
include Makefile.config
# We make all targets depend on the Makefiles themselves.
global_deps = Makefile Makefile.config Makefile.local \
$(subdirs:%=%/Makefile) $(subdirs:%=%/Makefile.local)
INCLUDE_MORE := yes
ifneq ($(filter clean distclean dataclean, $(word 1, $(MAKECMDGOALS))),)
CLEAN_GOAL := $(word 1, $(MAKECMDGOALS))
# If there are more goals following CLEAN_GOAL, run $(MAKE)s in parts.
ifneq ($(word 2, $(MAKECMDGOALS)),)
INCLUDE_MORE := no
FOLLOWING_GOALS := $(wordlist 2, 99, $(MAKECMDGOALS))
.PHONY: $(FOLLOWING_GOALS) make_in_parts
$(FOLLOWING_GOALS):
@true
$(CLEAN_GOAL): make_in_parts
make_in_parts:
$(MAKE) $(CLEAN_GOAL)
$(MAKE) $(FOLLOWING_GOALS) configure_options="$(configure_options)"
endif
else
CLEAN_GOAL :=
endif
# Potentially speedup make clean, distclean and dataclean ; avoid
# re-creating Makefile.config if it exists but configure is newer.
ifneq ($(CLEAN_GOAL),)
Makefile.config: | $(srcdir)/configure
else
Makefile.config: $(srcdir)/configure
endif
ifeq ($(configure_options),)
@echo ""
@echo "Note: Calling ./configure with no command-line arguments. This is often fine,"
@echo " but if you want to specify any arguments (such as an alternate prefix"
@echo " into which to install), call ./configure explicitly and then make again."
@echo " See \"./configure --help\" for more details."
@echo ""
endif
$(srcdir)/configure $(configure_options)
ifeq ($(INCLUDE_MORE),yes)
# runtime variable definitions available in all subdirs
include $(srcdir)/Makefile.global
# Finally, include all of the Makefile.local fragments where all the
# real work is done.
include $(subdirs:%=%/Makefile.local) Makefile.local
endif

65
Makefile.global Normal file
View file

@ -0,0 +1,65 @@
# -*- makefile-gmake -*-
# Here's the (hopefully simple) versioning scheme.
#
# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
# increment the second digit for each release and increment the first
# digit when we reach particularly major milestones of usability.
#
# Between releases, (such as when compiling notmuch from the git
# repository), we let git append identification of the actual commit.
PACKAGE=notmuch
IS_GIT:=$(if $(wildcard ${srcdir}/.git),yes,no)
ifeq ($(IS_GIT),yes)
DATE:=$(shell git --git-dir=${srcdir}/.git log --date=short -1 --pretty=format:%cd)
else
DATE:=$(shell date +%F)
endif
VERSION:=$(shell cat ${srcdir}/version.txt)
ELPA_VERSION:=$(subst ~,_,$(VERSION))
ifeq ($(filter release release-message pre-release update-versions,$(MAKECMDGOALS)),)
ifeq ($(IS_GIT),yes)
VERSION:=$(shell git --git-dir=${srcdir}/.git describe --abbrev=7 --match '[0-9.]*'|sed -e s/_/~/ -e s/-/+/ -e s/-/~/)
# drop the ~g$sha1 part
ELPA_VERSION:=$(word 1,$(subst ~, ,$(VERSION)))
# convert git version to package.el friendly form
ELPA_VERSION:=$(subst +,snapshot,$(ELPA_VERSION))
# Write the file 'version.stamp' in case its contents differ from $(VERSION)
FILE_VERSION:=$(shell test -f version.stamp && read vs < version.stamp || vs=; echo $$vs)
ifneq ($(FILE_VERSION),$(VERSION))
$(shell echo "$(VERSION)" > version.stamp)
endif
endif
endif
UPSTREAM_TAG=$(subst ~,_,$(VERSION))
RELEASE_HOST=notmuchmail.org
RELEASE_DIR=/srv/notmuchmail.org/www/releases
DOC_DIR=/srv/notmuchmail.org/www/doc/latest
RELEASE_URL=https://notmuchmail.org/releases
TAR_FILE=$(PACKAGE)-$(VERSION).tar.xz
ELPA_FILE:=$(PACKAGE)-emacs-$(ELPA_VERSION).tar
DEB_TAR_FILE=$(PACKAGE)_$(VERSION).orig.tar.xz
SHA256_FILE=$(TAR_FILE).sha256.asc
DETACHED_SIG_FILE=$(TAR_FILE).asc
PV_FILE=bindings/python/notmuch/version.py
# Smash together user's values with our extra values
FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(WARN_CFLAGS) $(extra_cflags) $(CPPFLAGS) $(CONFIGURE_CFLAGS) $(CFLAGS)
FINAL_CXXFLAGS = $(WARN_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) $(CPPFLAGS) $(CONFIGURE_CXXFLAGS) $(CXXFLAGS)
FINAL_NOTMUCH_LDFLAGS = -Lutil -lnotmuch_util -Llib -lnotmuch $(LDFLAGS)
ifeq ($(LIBDIR_IN_LDCONFIG),0)
FINAL_NOTMUCH_LDFLAGS += $(RPATH_LDFLAGS)
endif
FINAL_NOTMUCH_LDFLAGS += $(AS_NEEDED_LDFLAGS) $(GMIME_LDFLAGS) $(TALLOC_LDFLAGS) $(ZLIB_LDFLAGS)
FINAL_NOTMUCH_LINKER = CC
ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1)
FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS)
FINAL_NOTMUCH_LINKER = CXX
endif
FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(AS_NEEDED_LDFLAGS) $(CONFIGURE_LDFLAGS)

324
Makefile.local Normal file
View file

@ -0,0 +1,324 @@
# -*- makefile-gmake -*-
.PHONY: all
all: notmuch notmuch-shared build-man build-info ruby-bindings python-cffi-bindings notmuch-git nmbug
ifeq ($(MAKECMDGOALS),)
ifeq ($(shell cat .first-build-message 2>/dev/null),)
@NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
@echo ""
@echo "Compilation of notmuch is now complete. You can install notmuch with:"
@echo ""
@echo " make install"
@echo ""
@echo "Note that depending on the prefix to which you are installing"
@echo "you may need root permission (such as \"sudo make install\")."
@echo "See \"./configure --help\" for help on setting an alternate prefix."
@echo Printed > .first-build-message
endif
endif
# Depend (also) on the file 'version'. In case of ifeq ($(IS_GIT),yes)
# this file may already have been updated.
version.stamp: $(srcdir)/version.txt
echo $(VERSION) > $@
$(TAR_FILE):
if git tag -v $(UPSTREAM_TAG) >/dev/null 2>&1; then \
ref=$(UPSTREAM_TAG); \
else \
ref="HEAD" ; \
echo "Warning: No signed tag for $(VERSION)"; \
fi ; \
git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ $$ref > $(TAR_FILE).tmp
echo $(VERSION) > version.txt.tmp
ct=`git --no-pager log -1 --pretty=format:%ct $$ref` ; \
tar --owner root --group root --append -f $(TAR_FILE).tmp \
--transform s_^_$(PACKAGE)-$(VERSION)/_ \
--transform 's_.tmp$$__' --mtime=@$$ct version.txt.tmp
rm version.txt.tmp
xz -C sha256 -9 < $(TAR_FILE).tmp > $(TAR_FILE)
@echo "Source is ready for release in $(TAR_FILE)"
$(SHA256_FILE): $(TAR_FILE)
sha256sum $^ | gpg --clear-sign --output $@ -
$(DETACHED_SIG_FILE): $(TAR_FILE)
gpg --armor --detach-sign $^
CLEAN := $(CLEAN) notmuch-git
notmuch-git: notmuch-git.py
cp $< $@
chmod ugo+x $@
CLEAN := $(CLEAN) nmbug
nmbug: notmuch-git
ln -s $< $@
.PHONY: dist
dist: $(TAR_FILE)
.PHONY: update-versions
update-versions:
sed -i -e "s/^__VERSION__[[:blank:]]*=.*$$/__VERSION__ = \'${VERSION}\'/" \
-e "s/^SOVERSION[[:blank:]]*=.*$$/SOVERSION = \'${LIBNOTMUCH_VERSION_MAJOR}\'/" \
${PV_FILE}
# We invoke make recursively only to force ordering of our phony
# targets in the case of parallel invocation of make (-j).
#
# We carefully ensure that our VERSION variable is passed down to any
# sub-ordinate make invocations (which won't otherwise know that they
# are part of the release and need to take the version from the
# version file).
.PHONY: release
release: verify-source-tree-and-version
$(MAKE) VERSION=$(VERSION) verify-newer
$(MAKE) VERSION=$(VERSION) clean
$(MAKE) VERSION=$(VERSION) sphinx-html
$(MAKE) VERSION=$(VERSION) test
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
$(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
mkdir -p releases
mv $(TAR_FILE) $(DEB_TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) releases
$(MAKE) VERSION=$(VERSION) release-message > $(PACKAGE)-$(VERSION).announce
ifeq ($(REALLY_UPLOAD),yes)
git push origin $(VERSION) release pristine-tar
cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-* ; ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(TAR_FILE)"
rsync --verbose --delete --recursive doc/_build/html/ $(RELEASE_HOST):$(DOC_DIR)
endif
@echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template."
.PHONY: pre-release
pre-release:
$(MAKE) VERSION=$(VERSION) clean
$(MAKE) VERSION=$(VERSION) test
git tag -s -m "$(PACKAGE) $(VERSION) release" $(UPSTREAM_TAG)
$(MAKE) VERSION=$(VERSION) $(SHA256_FILE) $(DETACHED_SIG_FILE)
ln -sf $(TAR_FILE) $(DEB_TAR_FILE)
pristine-tar commit $(DEB_TAR_FILE) $(UPSTREAM_TAG)
mkdir -p releases
mv $(TAR_FILE) $(DEB_TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) releases
ifeq ($(REALLY_UPLOAD),yes)
git push origin $(UPSTREAM_TAG) release pristine-tar
cd releases && scp $(TAR_FILE) $(SHA256_FILE) $(DETACHED_SIG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
endif
.PHONY: debian-snapshot
debian-snapshot:
make VERSION=$(VERSION) clean
RETVAL=0 && \
TMPFILE=$$(mktemp /tmp/notmuch.XXXXXX) && \
cp debian/changelog $${TMPFILE} && \
(EDITOR=/bin/true dch -b -v $(VERSION)+1 \
-D UNRELEASED 'test build, not for upload' && \
echo '3.0 (native)' > debian/source/format && \
debuild -us -uc); RETVAL=$$? \
mv -f $${TMPFILE} debian/changelog; \
echo '3.0 (quilt)' > debian/source/format; \
exit $$RETVAL
.PHONY: release-message
release-message:
@echo "To: notmuch@notmuchmail.org"
@echo "Subject: $(PACKAGE) release $(VERSION) now available"
@echo ""
@echo "Where to obtain notmuch $(VERSION)"
@echo "==========================="
@echo " $(RELEASE_URL)/$(TAR_FILE)"
@echo ""
@echo "Which can be verified with:"
@echo ""
@echo " $(RELEASE_URL)/$(SHA256_FILE)"
@sed "s/^/ /" releases/$(SHA256_FILE)
@echo ""
@echo " $(RELEASE_URL)/$(DETACHED_SIG_FILE)"
@echo " (signed by `getent passwd "$$USER" | cut -d: -f 5 | cut -d, -f 1`)"
@echo ""
@echo "What's new in notmuch $(VERSION)"
@echo "========================="
@sed -ne '/^[Nn]otmuch $(VERSION)/{n;n;b NEWS}; d; :NEWS /^===/q; {p;n;b NEWS}' < NEWS | head -n -2
@echo ""
@echo "What is notmuch"
@echo "==============="
@echo "Notmuch is a system for indexing, searching, reading, and tagging"
@echo "large collections of email messages in maildir or mh format. It uses"
@echo "the Xapian library to provide fast, full-text search with a convenient"
@echo "search syntax."
@echo ""
@echo "For more about notmuch, see https://notmuchmail.org"
# This is a chain of dependencies rather than a simple list simply to
# avoid the messages getting interleaved in the case of a parallel
# make invocation.
.PHONY: verify-source-tree-and-version
verify-source-tree-and-version: verify-no-dirty-code
.PHONY: verify-no-dirty-code
verify-no-dirty-code: release-checks
ifeq ($(IS_GIT),yes)
@printf "Checking that source tree is clean..."
ifneq ($(shell git --git-dir=${srcdir}/.git ls-files -m),)
@echo "No"
@echo "The following files have been modified since the most recent git commit:"
@echo ""
@git --git-dir=${srcdir}/.git ls-files -m
@echo ""
@echo "The release will be made from the committed state, but perhaps you meant"
@echo "to commit this code first? Please clean this up to make it more clear."
@false
else
@echo "Good"
endif
endif
.PHONY: release-checks
release-checks:
devel/release-checks.sh
.PHONY: verify-newer
verify-newer:
@printf %s "Checking that no $(VERSION) release already exists..."
@wget -q --no-check-certificate -O /dev/null $(RELEASE_URL)/$(TAR_FILE) ; \
case $$? in \
8) echo "Good." ;; \
0) echo "Ouch."; \
echo "Found: $(RELEASE_URL)/$(TAR_FILE)"; \
echo "Refusing to replace an existing release."; \
echo "Don't forget to update \"version\" as described in RELEASING before release." ; \
false ;; \
*) echo "An unexpected error occurred"; \
false;; esac
# The user has not set any verbosity, default to quiet mode and inform the
# user how to enable verbose compiles.
ifeq ($(V),)
quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
endif
# The user has explicitly enabled quiet compilation.
ifeq ($(V),0)
quiet = @printf "$(1) $(or $(2),$@)\n"; $($(word 1, $(1)))
endif
# Otherwise, print the full command line.
quiet ?= $($(word 1, $(1)))
%.o: %.cc $(global_deps)
@mkdir -p $(patsubst %/.,%,.deps/$(@D))
$(call quiet,CXX $(CPPFLAGS) $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
%.o: %.c $(global_deps)
@mkdir -p $(patsubst %/.,%,.deps/$(@D))
$(call quiet,CC $(CPPFLAGS) $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ -MD -MP -MF .deps/$*.d
CPPCHECK=cppcheck
.stamps/cppcheck/%: %
@mkdir -p $(@D)
$(call quiet,CPPCHECK,$<) --template=gcc --error-exitcode=1 --quiet $<
@touch $@
CLEAN := $(CLEAN) .stamps
.PHONY : clean
clean:
rm -rf $(CLEAN)
.PHONY: distclean
distclean: clean
rm -rf $(DISTCLEAN)
.PHONY: dataclean
dataclean: distclean
rm -rf $(DATACLEAN)
notmuch_client_srcs = \
$(notmuch_compat_srcs) \
command-line-arguments.c\
debugger.c \
status.c \
gmime-filter-reply.c \
hooks.c \
notmuch.c \
notmuch-client-init.c \
notmuch-compact.c \
notmuch-config.c \
notmuch-count.c \
notmuch-dump.c \
notmuch-insert.c \
notmuch-new.c \
notmuch-reindex.c \
notmuch-reply.c \
notmuch-restore.c \
notmuch-search.c \
notmuch-setup.c \
notmuch-show.c \
notmuch-tag.c \
notmuch-time.c \
sprinter-json.c \
sprinter-sexp.c \
sprinter-text.c \
query-string.c \
mime-node.c \
tag-util.c
notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
notmuch.o: version.stamp
notmuch: $(notmuch_client_modules) lib/libnotmuch.a util/libnotmuch_util.a parse-time-string/libparse-time-string.a
$(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@
notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME)
$(call quiet,$(FINAL_NOTMUCH_LINKER) $(CFLAGS)) $(notmuch_client_modules) $(FINAL_NOTMUCH_LDFLAGS) -o $@
.PHONY: install
install: all install-man install-info
mkdir -p "$(DESTDIR)$(prefix)/bin/"
install notmuch-shared "$(DESTDIR)$(prefix)/bin/notmuch"
ifeq ($(MAKECMDGOALS), install)
@echo ""
@echo "Notmuch is now installed to $(DESTDIR)$(prefix)"
@echo ""
@echo "New users should simply run \"notmuch\" to be guided"
@echo "through the process of configuring notmuch and creating"
@echo "a database of existing email messages. The \"notmuch\""
@echo "command will also offer some sample search commands."
ifeq ($(WITH_EMACS), 1)
@echo ""
@echo "Beyond the command-line interface, notmuch also offers"
@echo "a full-featured interface for reading and writing mail"
@echo "within emacs. To use this, each user should add the"
@echo "following line to the ~/.emacs file:"
@echo ""
@echo " (require 'notmuch)"
@echo ""
@echo "And then run emacs as \"emacs -f notmuch\" or invoke"
@echo "the command \"M-x notmuch\" from within emacs."
endif
endif
SRCS := $(SRCS) $(notmuch_client_srcs)
CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules)
CLEAN := $(CLEAN) version.stamp notmuch-*.tar.gz.tmp
CLEAN := $(CLEAN) .deps
DISTCLEAN := $(DISTCLEAN) .first-build-message Makefile.config sh.config sphinx.config
CPPCHECK_STAMPS := $(SRCS:%=.stamps/cppcheck/%)
.PHONY: cppcheck
ifeq ($(HAVE_CPPCHECK),1)
cppcheck: ${CPPCHECK_STAMPS}
else
cppcheck:
@echo "No cppcheck found during configure; skipping static checking"
endif
DEPS := $(SRCS:%.c=.deps/%.d)
DEPS := $(DEPS:%.cc=.deps/%.d)
-include $(DEPS)
.SUFFIXES: # Delete the default suffixes. Old-Fashioned Suffix Rules not used.

5120
NEWS Normal file

File diff suppressed because it is too large Load diff

77
README Normal file
View file

@ -0,0 +1,77 @@
Notmuch - thread-based email index, search and tagging.
Notmuch is a system for indexing, searching, reading, and tagging
large collections of email messages in maildir or mh format. It uses
the Xapian library to provide fast, full-text search with a convenient
search syntax.
Notmuch is free software, released under the GNU General Public
License version 3 (or later).
Building notmuch
----------------
See the INSTALL file for notes on compiling and installing notmuch.
Running notmuch
---------------
After installing notmuch, start by running "notmuch setup" which will
interactively prompt for configuration information such as your name,
email address, and the directory which contains your mail archive to
be indexed. You can change any answers later by running "notmuch
setup" again or by editing the .notmuch-config file in your home
directory.
With notmuch configured you should next run "notmuch new" which will
index all of your existing mail. This can take a long time, (several
hours) if you have a lot of email, (hundreds of thousands of
files). When new mail is delivered to your mail archive in the future,
you will want to run "notmuch new" again. These runs will be much
faster as they will only index new messages.
Finally, you can prove to yourself that things are working by running
some command-line searches such as "notmuch search
from:someone@example.com" or "notmuch search subject:topic". See
"notmuch help search-terms" for more details on the available search
syntax.
The command-line search output is not expected to be particularly
friendly for day-to-day usage. Instead, it is expected that you will
use an email interface that builds on the notmuch command-line tool or
the libnotmuch library.
Notmuch installs a full-featured email interface for use within
emacs. To use this, first add the following line to your .emacs file:
(autoload 'notmuch "notmuch" "Notmuch mail" t)
Then, either run "emacs -f notmuch" or execute the command "M-x
notmuch" from within a running emacs.
If you're interested in a non-emacs-based interface to notmuch, then
please join the notmuch community. Various other interfaces are
already in progress, (an interface within vim, a curses interface,
graphical interfaces based on evolution, and various web-based
interfaces). The authors of these interfaces would love further
testing or contribution. See contact information below.
Contacting users and developers
-------------------------------
The website for Notmuch is:
https://notmuchmail.org
The mailing list address for the notmuch community is:
notmuch@notmuchmail.org
We welcome any sort of questions, comments, kudos, or code there.
Subscription is not required, (but if you do subscribe you'll avoid
any delay due to moderation). See the website for subscription
information.
There is also an IRC channel dedicated to talk about using and
developing notmuch:
IRC server: irc.libera.chat
Channel: #notmuch

11
README.rst Normal file
View file

@ -0,0 +1,11 @@
If you're reading this on https://github.com/notmuch/notmuch, this is a
read-only mirror of the notmuch project.
For more information about the project, see https://notmuchmail.org.
Please don't send us pull requests on github. If you have a feature
branch that you want us to look at, use ``git send-email`` to send it
to notmuch@notmuchmail.org.
For more information about contributing to the project, see
https://notmuchmail.org/contributing/.

7
bindings/Makefile Normal file
View file

@ -0,0 +1,7 @@
# See Makefile.local for the list of files to be compiled in this
# directory.
all:
$(MAKE) -C .. all
.DEFAULT:
$(MAKE) -C .. $@

40
bindings/Makefile.local Normal file
View file

@ -0,0 +1,40 @@
# -*- makefile-gmake -*-
dir := bindings
# force the shared library to be built
ruby-bindings: $(dir)/ruby.stamp
$(dir)/ruby.stamp: lib/$(LINKER_NAME)
ifeq ($(HAVE_RUBY_DEV),1)
cd $(dir)/ruby && \
EXTRA_LDFLAGS="$(NO_UNDEFINED_LDFLAGS)" \
LIBNOTMUCH="../../lib/$(LINKER_NAME)" \
NOTMUCH_SRCDIR='$(NOTMUCH_SRCDIR)' \
$(RUBY) extconf.rb --vendor
$(MAKE) -C $(dir)/ruby CFLAGS="$(CFLAGS) -pipe -fno-plt -fPIC" && touch $@
endif
python-cffi-bindings: $(dir)/python-cffi.stamp
$(dir)/python-cffi.stamp: lib/$(LINKER_NAME)
ifeq ($(HAVE_PYTHON3_CFFI),1)
cd $(dir)/python-cffi && \
${PYTHON} setup.py build --build-lib build/stage && \
mkdir -p build/stage/tests && cp tests/*.py build/stage/tests && \
touch ../python-cffi.stamp
endif
CLEAN += $(patsubst %,$(dir)/ruby/%, \
.RUBYARCHDIR.time \
Makefile database.o directory.o filenames.o\
init.o message.o messages.o mkmf.log notmuch.so query.o \
status.o tags.o thread.o threads.o)
CLEAN += bindings/ruby/.vendorarchdir.time $(dir)/ruby.stamp
CLEAN += bindings/python-cffi/build $(dir)/python-cffi.stamp
CLEAN += bindings/python-cffi/__pycache__
DISTCLEAN += bindings/python-cffi/_notmuch_config.py \
bindings/python-cffi/notmuch2.egg-info

View file

@ -0,0 +1,2 @@
include MANIFEST.in
include tox.ini

View file

@ -0,0 +1,62 @@
"""Pythonic API to the notmuch database.
Creating Objects
================
Only the :class:`Database` object is meant to be created by the user.
All other objects should be created from this initial object. Users
should consider their signatures implementation details.
Errors
======
All errors occurring due to errors from the underlying notmuch database
are subclasses of the :exc:`NotmuchError`. Due to memory management
it is possible to try and use an object after it has been freed. In
this case a :exc:`ObjectDestroyedError` will be raised.
Memory Management
=================
Libnotmuch uses a hierarchical memory allocator, this means all
objects have a strict parent-child relationship and when the parent is
freed all the children are freed as well. This has some implications
for these Python bindings as parent objects need to be kept alive.
This is normally schielded entirely from the user however and the
Python objects automatically make sure the right references are kept
alive. It is however the reason the :class:`BaseObject` exists as it
defines the API all Python objects need to implement to work
correctly.
Collections and Containers
==========================
Libnotmuch exposes nearly all collections of things as iterators only.
In these python bindings they have sometimes been exposed as
:class:`collections.abc.Container` instances or subclasses of this
like :class:`collections.abc.Set` or :class:`collections.abc.Mapping`
etc. This gives a more natural API to work with, e.g. being able to
treat tags as sets. However it does mean that the
:meth:`__contains__`, :meth:`__len__` and frieds methods on these are
usually more and essentially O(n) rather than O(1) as you might
usually expect from Python containers.
"""
from notmuch2 import _capi
from notmuch2._base import *
from notmuch2._database import *
from notmuch2._errors import *
from notmuch2._message import *
from notmuch2._tags import *
from notmuch2._thread import *
NOTMUCH_TAG_MAX = _capi.lib.NOTMUCH_TAG_MAX
del _capi
# Re-home all the objects to the package. This leaves __qualname__ intact.
for x in locals().copy().values():
if hasattr(x, '__module__'):
x.__module__ = __name__
del x

View file

@ -0,0 +1,238 @@
import abc
import collections.abc
from notmuch2 import _capi as capi
from notmuch2 import _errors as errors
__all__ = ['NotmuchObject', 'BinString']
class NotmuchObject(metaclass=abc.ABCMeta):
"""Base notmuch object syntax.
This base class exists to define the memory management handling
required to use the notmuch library. It is meant as an interface
definition rather than a base class, though you can use it as a
base class to ensure you don't forget part of the interface. It
only concerns you if you are implementing this package itself
rather then using it.
libnotmuch uses a hierarchical memory allocator, where freeing the
memory of a parent object also frees the memory of all child
objects. To make this work seamlessly in Python this package
keeps references to parent objects which makes them stay alive
correctly under normal circumstances. When an object finally gets
deleted the :meth:`__del__` method will be called to free the
memory.
However during some peculiar situations, e.g. interpreter
shutdown, it is possible for the :meth:`__del__` method to have
been called, whele there are still references to an object. This
could result in child objects asking their memory to be freed
after the parent has already freed the memory, making things
rather unhappy as double frees are not taken lightly in C. To
handle this case all objects need to follow the same protocol to
destroy themselves, see :meth:`destroy`.
Once an object has been destroyed trying to use it should raise
the :exc:`ObjectDestroyedError` exception. For this see also the
convenience :class:`MemoryPointer` descriptor in this module which
can be used as a pointer to libnotmuch memory.
"""
@abc.abstractmethod
def __init__(self, parent, *args, **kwargs):
"""Create a new object.
Other then for the toplevel :class:`Database` object
constructors are only ever called by internal code and not by
the user. Per convention their signature always takes the
parent object as first argument. Feel free to make the rest
of the signature match the object's requirement. The object
needs to keep a reference to the parent, so it can check the
parent is still alive.
"""
@property
@abc.abstractmethod
def alive(self):
"""Whether the object is still alive.
This indicates whether the object is still alive. The first
thing this needs to check is whether the parent object is
still alive, if it is not then this object can not be alive
either. If the parent is alive then it depends on whether the
memory for this object has been freed yet or not.
"""
def __del__(self):
self._destroy()
@abc.abstractmethod
def _destroy(self):
"""Destroy the object, freeing all memory.
This method needs to destroy the object on the
libnotmuch-level. It must ensure it's not been destroyed by
it's parent object yet before doing so. It also must be
idempotent.
"""
class MemoryPointer:
"""Data Descriptor to handle accessing libnotmuch pointers.
Most :class:`NotmuchObject` instances will have one or more CFFI
pointers to C-objects. Once an object is destroyed this pointer
should no longer be used and a :exc:`ObjectDestroyedError`
exception should be raised on trying to access it. This
descriptor simplifies implementing this, allowing the creation of
an attribute which can be assigned to, but when accessed when the
stored value is *None* it will raise the
:exc:`ObjectDestroyedError` exception::
class SomeOjb:
_ptr = MemoryPointer()
def __init__(self, ptr):
self._ptr = ptr
def destroy(self):
somehow_free(self._ptr)
self._ptr = None
def do_something(self):
return some_libnotmuch_call(self._ptr)
"""
def __get__(self, instance, owner):
try:
val = getattr(instance, self.attr_name, None)
except AttributeError:
# We're not on 3.6+ and self.attr_name does not exist
self.__set_name__(instance, 'dummy')
val = getattr(instance, self.attr_name, None)
if val is None:
raise errors.ObjectDestroyedError()
return val
def __set__(self, instance, value):
try:
setattr(instance, self.attr_name, value)
except AttributeError:
# We're not on 3.6+ and self.attr_name does not exist
self.__set_name__(instance, 'dummy')
setattr(instance, self.attr_name, value)
def __set_name__(self, instance, name):
self.attr_name = '_memptr_{}_{:x}'.format(name, id(instance))
class BinString(str):
"""A str subclass with binary data.
Most data in libnotmuch should be valid ASCII or valid UTF-8.
However since it is a C library these are represented as
bytestrings instead which means on an API level we can not
guarantee that decoding this to UTF-8 will both succeed and be
lossless. This string type converts bytes to unicode in a lossy
way, but also makes the raw bytes available.
This object is a normal unicode string for most intents and
purposes, but you can get the original bytestring back by calling
``bytes()`` on it.
"""
def __new__(cls, data, encoding='utf-8', errors='ignore'):
if not isinstance(data, bytes):
data = bytes(data, encoding=encoding)
strdata = str(data, encoding=encoding, errors=errors)
inst = super().__new__(cls, strdata)
inst._bindata = data
return inst
@classmethod
def from_cffi(cls, cdata):
"""Create a new string from a CFFI cdata pointer."""
return cls(capi.ffi.string(cdata))
def __bytes__(self):
return self._bindata
class NotmuchIter(NotmuchObject, collections.abc.Iterator):
"""An iterator for libnotmuch iterators.
It is tempting to use a generator function instead, but this would
not correctly respect the :class:`NotmuchObject` memory handling
protocol and in some unsuspecting cornercases cause memory
trouble. You probably want to sublcass this in order to wrap the
value returned by :meth:`__next__`.
:param parent: The parent object.
:type parent: NotmuchObject
:param iter_p: The CFFI pointer to the C iterator.
:type iter_p: cffi.cdata
:param fn_destory: The CFFI notmuch_*_destroy function.
:param fn_valid: The CFFI notmuch_*_valid function.
:param fn_get: The CFFI notmuch_*_get function.
:param fn_next: The CFFI notmuch_*_move_to_next function.
"""
_iter_p = MemoryPointer()
def __init__(self, parent, iter_p,
*, fn_destroy, fn_valid, fn_get, fn_next):
self._parent = parent
self._iter_p = iter_p
self._fn_destroy = fn_destroy
self._fn_valid = fn_valid
self._fn_get = fn_get
self._fn_next = fn_next
def __del__(self):
self._destroy()
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._iter_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
if self.alive:
try:
self._fn_destroy(self._iter_p)
except errors.ObjectDestroyedError:
pass
self._iter_p = None
def __iter__(self):
"""Return the iterator itself.
Note that as this is an iterator and not a container this will
not return a new iterator. Thus any elements already consumed
will not be yielded by the :meth:`__next__` method anymore.
"""
return self
def __next__(self):
if not self._fn_valid(self._iter_p):
self._destroy()
raise StopIteration()
obj_p = self._fn_get(self._iter_p)
self._fn_next(self._iter_p)
return obj_p
def __repr__(self):
try:
self._iter_p
except errors.ObjectDestroyedError:
return '<NotmuchIter (exhausted)>'
else:
return '<NotmuchIter>'

View file

@ -0,0 +1,346 @@
import cffi
from _notmuch_config import *
ffibuilder = cffi.FFI()
ffibuilder.set_source(
'notmuch2._capi',
r"""
#include <stdlib.h>
#include <time.h>
#include <notmuch.h>
#if LIBNOTMUCH_MAJOR_VERSION < 5
#error libnotmuch version not supported by notmuch2 python bindings
#endif
#if LIBNOTMUCH_MINOR_VERSION < 1
#ERROR libnotmuch version < 5.1 not supported
#endif
""",
include_dirs=[NOTMUCH_INCLUDE_DIR],
library_dirs=[NOTMUCH_LIB_DIR],
libraries=['notmuch'],
)
ffibuilder.cdef(
r"""
void free(void *ptr);
typedef int... time_t;
#define LIBNOTMUCH_MAJOR_VERSION ...
#define LIBNOTMUCH_MINOR_VERSION ...
#define LIBNOTMUCH_MICRO_VERSION ...
#define NOTMUCH_TAG_MAX ...
typedef enum _notmuch_status {
NOTMUCH_STATUS_SUCCESS = 0,
NOTMUCH_STATUS_OUT_OF_MEMORY,
NOTMUCH_STATUS_READ_ONLY_DATABASE,
NOTMUCH_STATUS_XAPIAN_EXCEPTION,
NOTMUCH_STATUS_FILE_ERROR,
NOTMUCH_STATUS_FILE_NOT_EMAIL,
NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
NOTMUCH_STATUS_NULL_POINTER,
NOTMUCH_STATUS_TAG_TOO_LONG,
NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
NOTMUCH_STATUS_UNBALANCED_ATOMIC,
NOTMUCH_STATUS_UNSUPPORTED_OPERATION,
NOTMUCH_STATUS_UPGRADE_REQUIRED,
NOTMUCH_STATUS_PATH_ERROR,
NOTMUCH_STATUS_ILLEGAL_ARGUMENT,
NOTMUCH_STATUS_MALFORMED_CRYPTO_PROTOCOL,
NOTMUCH_STATUS_FAILED_CRYPTO_CONTEXT_CREATION,
NOTMUCH_STATUS_UNKNOWN_CRYPTO_PROTOCOL,
NOTMUCH_STATUS_NO_CONFIG,
NOTMUCH_STATUS_NO_DATABASE,
NOTMUCH_STATUS_DATABASE_EXISTS,
NOTMUCH_STATUS_BAD_QUERY_SYNTAX,
NOTMUCH_STATUS_NO_MAIL_ROOT,
NOTMUCH_STATUS_CLOSED_DATABASE,
NOTMUCH_STATUS_LAST_STATUS
} notmuch_status_t;
typedef enum {
NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
NOTMUCH_DATABASE_MODE_READ_WRITE
} notmuch_database_mode_t;
typedef int notmuch_bool_t;
typedef enum _notmuch_message_flag {
NOTMUCH_MESSAGE_FLAG_MATCH,
NOTMUCH_MESSAGE_FLAG_EXCLUDED,
NOTMUCH_MESSAGE_FLAG_GHOST,
} notmuch_message_flag_t;
typedef enum {
NOTMUCH_SORT_OLDEST_FIRST,
NOTMUCH_SORT_NEWEST_FIRST,
NOTMUCH_SORT_MESSAGE_ID,
NOTMUCH_SORT_UNSORTED
} notmuch_sort_t;
typedef enum {
NOTMUCH_EXCLUDE_FLAG,
NOTMUCH_EXCLUDE_TRUE,
NOTMUCH_EXCLUDE_FALSE,
NOTMUCH_EXCLUDE_ALL
} notmuch_exclude_t;
typedef enum {
NOTMUCH_DECRYPT_FALSE,
NOTMUCH_DECRYPT_TRUE,
NOTMUCH_DECRYPT_AUTO,
NOTMUCH_DECRYPT_NOSTASH,
} notmuch_decryption_policy_t;
// These are fully opaque types for us, we only ever use pointers.
typedef struct _notmuch_database notmuch_database_t;
typedef struct _notmuch_query notmuch_query_t;
typedef struct _notmuch_threads notmuch_threads_t;
typedef struct _notmuch_thread notmuch_thread_t;
typedef struct _notmuch_messages notmuch_messages_t;
typedef struct _notmuch_message notmuch_message_t;
typedef struct _notmuch_tags notmuch_tags_t;
typedef struct _notmuch_string_map_iterator notmuch_message_properties_t;
typedef struct _notmuch_directory notmuch_directory_t;
typedef struct _notmuch_filenames notmuch_filenames_t;
typedef struct _notmuch_config_pairs notmuch_config_pairs_t;
typedef struct _notmuch_indexopts notmuch_indexopts_t;
const char *
notmuch_status_to_string (notmuch_status_t status);
notmuch_status_t
notmuch_database_create_with_config (const char *database_path,
const char *config_path,
const char *profile,
notmuch_database_t **database,
char **error_message);
notmuch_status_t
notmuch_database_open_with_config (const char *database_path,
notmuch_database_mode_t mode,
const char *config_path,
const char *profile,
notmuch_database_t **database,
char **error_message);
notmuch_status_t
notmuch_database_close (notmuch_database_t *database);
notmuch_status_t
notmuch_database_destroy (notmuch_database_t *database);
const char *
notmuch_database_get_path (notmuch_database_t *database);
unsigned int
notmuch_database_get_version (notmuch_database_t *database);
notmuch_bool_t
notmuch_database_needs_upgrade (notmuch_database_t *database);
notmuch_status_t
notmuch_database_begin_atomic (notmuch_database_t *notmuch);
notmuch_status_t
notmuch_database_end_atomic (notmuch_database_t *notmuch);
unsigned long
notmuch_database_get_revision (notmuch_database_t *notmuch,
const char **uuid);
notmuch_status_t
notmuch_database_index_file (notmuch_database_t *database,
const char *filename,
notmuch_indexopts_t *indexopts,
notmuch_message_t **message);
notmuch_status_t
notmuch_database_remove_message (notmuch_database_t *database,
const char *filename);
notmuch_status_t
notmuch_database_find_message (notmuch_database_t *database,
const char *message_id,
notmuch_message_t **message);
notmuch_status_t
notmuch_database_find_message_by_filename (notmuch_database_t *notmuch,
const char *filename,
notmuch_message_t **message);
notmuch_tags_t *
notmuch_database_get_all_tags (notmuch_database_t *db);
notmuch_query_t *
notmuch_query_create (notmuch_database_t *database,
const char *query_string);
const char *
notmuch_query_get_query_string (const notmuch_query_t *query);
notmuch_database_t *
notmuch_query_get_database (const notmuch_query_t *query);
void
notmuch_query_set_omit_excluded (notmuch_query_t *query,
notmuch_exclude_t omit_excluded);
void
notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
notmuch_sort_t
notmuch_query_get_sort (const notmuch_query_t *query);
notmuch_status_t
notmuch_query_add_tag_exclude (notmuch_query_t *query, const char *tag);
notmuch_status_t
notmuch_query_search_threads (notmuch_query_t *query,
notmuch_threads_t **out);
notmuch_status_t
notmuch_query_search_messages (notmuch_query_t *query,
notmuch_messages_t **out);
notmuch_status_t
notmuch_query_count_messages (notmuch_query_t *query, unsigned int *count);
notmuch_status_t
notmuch_query_count_threads (notmuch_query_t *query, unsigned *count);
void
notmuch_query_destroy (notmuch_query_t *query);
notmuch_bool_t
notmuch_threads_valid (notmuch_threads_t *threads);
notmuch_thread_t *
notmuch_threads_get (notmuch_threads_t *threads);
void
notmuch_threads_move_to_next (notmuch_threads_t *threads);
void
notmuch_threads_destroy (notmuch_threads_t *threads);
const char *
notmuch_thread_get_thread_id (notmuch_thread_t *thread);
notmuch_messages_t *
notmuch_message_get_replies (notmuch_message_t *message);
int
notmuch_thread_get_total_messages (notmuch_thread_t *thread);
notmuch_messages_t *
notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
notmuch_messages_t *
notmuch_thread_get_messages (notmuch_thread_t *thread);
int
notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
const char *
notmuch_thread_get_authors (notmuch_thread_t *thread);
const char *
notmuch_thread_get_subject (notmuch_thread_t *thread);
time_t
notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
time_t
notmuch_thread_get_newest_date (notmuch_thread_t *thread);
notmuch_tags_t *
notmuch_thread_get_tags (notmuch_thread_t *thread);
void
notmuch_thread_destroy (notmuch_thread_t *thread);
notmuch_bool_t
notmuch_messages_valid (notmuch_messages_t *messages);
notmuch_message_t *
notmuch_messages_get (notmuch_messages_t *messages);
void
notmuch_messages_move_to_next (notmuch_messages_t *messages);
void
notmuch_messages_destroy (notmuch_messages_t *messages);
notmuch_tags_t *
notmuch_messages_collect_tags (notmuch_messages_t *messages);
const char *
notmuch_message_get_message_id (notmuch_message_t *message);
const char *
notmuch_message_get_thread_id (notmuch_message_t *message);
const char *
notmuch_message_get_filename (notmuch_message_t *message);
notmuch_filenames_t *
notmuch_message_get_filenames (notmuch_message_t *message);
notmuch_bool_t
notmuch_message_get_flag (notmuch_message_t *message,
notmuch_message_flag_t flag);
void
notmuch_message_set_flag (notmuch_message_t *message,
notmuch_message_flag_t flag,
notmuch_bool_t value);
time_t
notmuch_message_get_date (notmuch_message_t *message);
const char *
notmuch_message_get_header (notmuch_message_t *message,
const char *header);
notmuch_tags_t *
notmuch_message_get_tags (notmuch_message_t *message);
notmuch_status_t
notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
notmuch_status_t
notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
notmuch_status_t
notmuch_message_remove_all_tags (notmuch_message_t *message);
notmuch_status_t
notmuch_message_maildir_flags_to_tags (notmuch_message_t *message);
notmuch_status_t
notmuch_message_tags_to_maildir_flags (notmuch_message_t *message);
notmuch_status_t
notmuch_message_freeze (notmuch_message_t *message);
notmuch_status_t
notmuch_message_thaw (notmuch_message_t *message);
notmuch_status_t
notmuch_message_get_property (notmuch_message_t *message,
const char *key, const char **value);
notmuch_status_t
notmuch_message_add_property (notmuch_message_t *message,
const char *key, const char *value);
notmuch_status_t
notmuch_message_remove_property (notmuch_message_t *message,
const char *key, const char *value);
notmuch_status_t
notmuch_message_remove_all_properties (notmuch_message_t *message,
const char *key);
notmuch_message_properties_t *
notmuch_message_get_properties (notmuch_message_t *message,
const char *key, notmuch_bool_t exact);
notmuch_bool_t
notmuch_message_properties_valid (notmuch_message_properties_t
*properties);
void
notmuch_message_properties_move_to_next (notmuch_message_properties_t
*properties);
const char *
notmuch_message_properties_key (notmuch_message_properties_t *properties);
const char *
notmuch_message_properties_value (notmuch_message_properties_t
*properties);
void
notmuch_message_properties_destroy (notmuch_message_properties_t
*properties);
void
notmuch_message_destroy (notmuch_message_t *message);
notmuch_bool_t
notmuch_tags_valid (notmuch_tags_t *tags);
const char *
notmuch_tags_get (notmuch_tags_t *tags);
void
notmuch_tags_move_to_next (notmuch_tags_t *tags);
void
notmuch_tags_destroy (notmuch_tags_t *tags);
notmuch_bool_t
notmuch_filenames_valid (notmuch_filenames_t *filenames);
const char *
notmuch_filenames_get (notmuch_filenames_t *filenames);
void
notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
void
notmuch_filenames_destroy (notmuch_filenames_t *filenames);
notmuch_indexopts_t *
notmuch_database_get_default_indexopts (notmuch_database_t *db);
notmuch_status_t
notmuch_indexopts_set_decrypt_policy (notmuch_indexopts_t *indexopts,
notmuch_decryption_policy_t decrypt_policy);
notmuch_decryption_policy_t
notmuch_indexopts_get_decrypt_policy (const notmuch_indexopts_t *indexopts);
void
notmuch_indexopts_destroy (notmuch_indexopts_t *options);
notmuch_status_t
notmuch_database_set_config (notmuch_database_t *db, const char *key, const char *value);
notmuch_status_t
notmuch_database_get_config (notmuch_database_t *db, const char *key, char **value);
notmuch_config_pairs_t *
notmuch_config_get_pairs (notmuch_database_t *db, const char *prefix);
notmuch_bool_t
notmuch_config_pairs_valid (notmuch_config_pairs_t *config_list);
const char *
notmuch_config_pairs_key (notmuch_config_pairs_t *config_list);
const char *
notmuch_config_pairs_value (notmuch_config_pairs_t *config_list);
void
notmuch_config_pairs_move_to_next (notmuch_config_pairs_t *config_list);
void
notmuch_config_pairs_destroy (notmuch_config_pairs_t *config_list);
"""
)
if __name__ == '__main__':
ffibuilder.compile(verbose=True)

View file

@ -0,0 +1,101 @@
import collections.abc
import notmuch2._base as base
import notmuch2._capi as capi
import notmuch2._errors as errors
__all__ = ['ConfigMapping']
class ConfigIter(base.NotmuchIter):
def __init__(self, parent, iter_p):
super().__init__(
parent, iter_p,
fn_destroy=capi.lib.notmuch_config_pairs_destroy,
fn_valid=capi.lib.notmuch_config_pairs_valid,
fn_get=capi.lib.notmuch_config_pairs_key,
fn_next=capi.lib.notmuch_config_pairs_move_to_next)
def __next__(self):
# skip pairs whose value is NULL
while capi.lib.notmuch_config_pairs_valid(super()._iter_p):
val_p = capi.lib.notmuch_config_pairs_value(super()._iter_p)
key_p = capi.lib.notmuch_config_pairs_key(super()._iter_p)
if key_p == capi.ffi.NULL:
# this should never happen
raise errors.NullPointerError
key = base.BinString.from_cffi(key_p)
capi.lib.notmuch_config_pairs_move_to_next(super()._iter_p)
if val_p != capi.ffi.NULL and base.BinString.from_cffi(val_p) != "":
return key
self._destroy()
raise StopIteration
class ConfigMapping(base.NotmuchObject, collections.abc.MutableMapping):
"""The config key/value pairs loaded from the database, config file,
and and/or defaults.
The entries are exposed as a :class:`collections.abc.MutableMapping` object.
Note that setting a value to an empty string is the same as deleting it.
Mutating (deleting or updating values) in the map persists only in
the database, which can be shadowed by config files.
:param parent: the parent object
:param ptr_name: the name of the attribute on the parent which will
return the memory pointer. This allows this object to
access the pointer via the parent's descriptor and thus
trigger :class:`MemoryPointer`'s memory safety.
"""
def __init__(self, parent, ptr_name):
self._parent = parent
self._ptr = lambda: getattr(parent, ptr_name)
@property
def alive(self):
return self._parent.alive
def _destroy(self):
pass
def __getitem__(self, key):
if isinstance(key, str):
key = key.encode('utf-8')
val_pp = capi.ffi.new('char**')
ret = capi.lib.notmuch_database_get_config(self._ptr(), key, val_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
val = base.BinString.from_cffi(val_pp[0])
capi.lib.free(val_pp[0])
if val == '':
raise KeyError
return val
def __setitem__(self, key, val):
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(val, str):
val = val.encode('utf-8')
ret = capi.lib.notmuch_database_set_config(self._ptr(), key, val)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def __delitem__(self, key):
self[key] = ""
def __iter__(self):
"""Return an iterator over the config items.
:raises NullPointerError: If the iterator can not be created.
"""
config_pairs_p = capi.lib.notmuch_config_get_pairs(self._ptr(), b'')
if config_pairs_p == capi.ffi.NULL:
raise KeyError
return ConfigIter(self._parent, config_pairs_p)
def __len__(self):
return sum(1 for t in self)

View file

@ -0,0 +1,856 @@
import collections
import configparser
import enum
import functools
import os
import pathlib
import weakref
import notmuch2._base as base
import notmuch2._config as config
import notmuch2._capi as capi
import notmuch2._errors as errors
import notmuch2._message as message
import notmuch2._query as querymod
import notmuch2._tags as tags
__all__ = ['Database', 'AtomicContext', 'DbRevision']
def _config_pathname():
"""Return the path of the configuration file.
:rtype: pathlib.Path
"""
cfgfname = os.getenv('NOTMUCH_CONFIG', '~/.notmuch-config')
return pathlib.Path(os.path.expanduser(cfgfname))
class Mode(enum.Enum):
READ_ONLY = capi.lib.NOTMUCH_DATABASE_MODE_READ_ONLY
READ_WRITE = capi.lib.NOTMUCH_DATABASE_MODE_READ_WRITE
class ConfigFile(enum.Enum):
EMPTY = b''
SEARCH = capi.ffi.NULL
class QuerySortOrder(enum.Enum):
OLDEST_FIRST = capi.lib.NOTMUCH_SORT_OLDEST_FIRST
NEWEST_FIRST = capi.lib.NOTMUCH_SORT_NEWEST_FIRST
MESSAGE_ID = capi.lib.NOTMUCH_SORT_MESSAGE_ID
UNSORTED = capi.lib.NOTMUCH_SORT_UNSORTED
class QueryExclude(enum.Enum):
TRUE = capi.lib.NOTMUCH_EXCLUDE_TRUE
FLAG = capi.lib.NOTMUCH_EXCLUDE_FLAG
FALSE = capi.lib.NOTMUCH_EXCLUDE_FALSE
ALL = capi.lib.NOTMUCH_EXCLUDE_ALL
class DecryptionPolicy(enum.Enum):
FALSE = capi.lib.NOTMUCH_DECRYPT_FALSE
TRUE = capi.lib.NOTMUCH_DECRYPT_TRUE
AUTO = capi.lib.NOTMUCH_DECRYPT_AUTO
NOSTASH = capi.lib.NOTMUCH_DECRYPT_NOSTASH
class Database(base.NotmuchObject):
"""Toplevel access to notmuch.
A :class:`Database` can be opened read-only or read-write.
Modifications are not atomic by default, use :meth:`begin_atomic`
for atomic updates. If the underlying database has been modified
outside of this class a :exc:`XapianError` will be raised and the
instance must be closed and a new one created.
You can use an instance of this class as a context-manager.
:cvar MODE: The mode a database can be opened with, an enumeration
of ``READ_ONLY`` and ``READ_WRITE``
:cvar SORT: The sort order for search results, ``OLDEST_FIRST``,
``NEWEST_FIRST``, ``MESSAGE_ID`` or ``UNSORTED``.
:cvar EXCLUDE: Which messages to exclude from queries, ``TRUE``,
``FLAG``, ``FALSE`` or ``ALL``. See the query documentation
for details.
:cvar CONFIG: Control loading of config file. Enumeration of
``EMPTY`` (don't load a config file), and ``SEARCH`` (search as
in :ref:`config_search`)
:cvar AddedMessage: A namedtuple ``(msg, dup)`` used by
:meth:`add` as return value.
:cvar STR_MODE_MAP: A map mapping strings to :attr:`MODE` items.
This is used to implement the ``ro`` and ``rw`` string
variants.
:ivar closed: Boolean indicating if the database is closed or
still open.
:param path: The directory of where the database is stored. If
``None`` the location will be searched according to
:ref:`database`
:type path: str, bytes, os.PathLike or pathlib.Path
:param mode: The mode to open the database in. One of
:attr:`MODE.READ_ONLY` OR :attr:`MODE.READ_WRITE`. For
convenience you can also use the strings ``ro`` for
:attr:`MODE.READ_ONLY` and ``rw`` for :attr:`MODE.READ_WRITE`.
:type mode: :attr:`MODE` or str.
:param config: Where to load the configuration from, if any.
:type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path
:raises KeyError: if an unknown mode string is used.
:raises OSError: or subclasses if the configuration file can not
be opened.
:raises configparser.Error: or subclasses if the configuration
file can not be parsed.
:raises NotmuchError: or subclasses for other failures.
"""
MODE = Mode
SORT = QuerySortOrder
EXCLUDE = QueryExclude
CONFIG = ConfigFile
AddedMessage = collections.namedtuple('AddedMessage', ['msg', 'dup'])
_db_p = base.MemoryPointer()
STR_MODE_MAP = {
'ro': MODE.READ_ONLY,
'rw': MODE.READ_WRITE,
}
@staticmethod
def _cfg_path_encode(path):
if isinstance(path,ConfigFile):
path = path.value
elif path is None:
path = capi.ffi.NULL
elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
path = bytes(path)
else:
path = os.fsencode(path)
return path
@staticmethod
def _db_path_encode(path):
if path is None:
path = capi.ffi.NULL
elif not hasattr(os, 'PathLike') and isinstance(path, pathlib.Path):
path = bytes(path)
else:
path = os.fsencode(path)
return path
def __init__(self, path=None, mode=MODE.READ_ONLY, config=CONFIG.SEARCH):
if isinstance(mode, str):
mode = self.STR_MODE_MAP[mode]
self.mode = mode
db_pp = capi.ffi.new('notmuch_database_t **')
cmsg = capi.ffi.new('char**')
ret = capi.lib.notmuch_database_open_with_config(self._db_path_encode(path),
mode.value,
self._cfg_path_encode(config),
capi.ffi.NULL,
db_pp, cmsg)
if cmsg[0]:
msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
capi.lib.free(cmsg[0])
else:
msg = None
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret, msg)
self._db_p = db_pp[0]
self.closed = False
@classmethod
def create(cls, path=None, config=ConfigFile.EMPTY):
"""Create and open database in READ_WRITE mode.
This is creates a new notmuch database and returns an opened
instance in :attr:`MODE.READ_WRITE` mode.
:param path: The directory of where the database is stored.
If ``None`` the location will be read searched by the
notmuch library (see notmuch(3)::notmuch_open_with_config).
:type path: str, bytes or os.PathLike
:param config: The pathname of the notmuch configuration file.
:type config: :attr:`CONFIG.EMPTY`, :attr:`CONFIG.SEARCH`, str, bytes, os.PathLike, pathlib.Path
:raises OSError: or subclasses if the configuration file can not
be opened.
:raises configparser.Error: or subclasses if the configuration
file can not be parsed.
:raises NotmuchError: if the config file does not have the
database.path setting.
:raises FileError: if the database already exists.
:returns: The newly created instance.
"""
db_pp = capi.ffi.new('notmuch_database_t **')
cmsg = capi.ffi.new('char**')
ret = capi.lib.notmuch_database_create_with_config(cls._db_path_encode(path),
cls._cfg_path_encode(config),
capi.ffi.NULL,
db_pp, cmsg)
if cmsg[0]:
msg = capi.ffi.string(cmsg[0]).decode(errors='replace')
capi.lib.free(cmsg[0])
else:
msg = None
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret, msg)
# Now close the db and let __init__ open it. Inefficient but
# creating is not a hot loop while this allows us to have a
# clean API.
ret = capi.lib.notmuch_database_destroy(db_pp[0])
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
return cls(path, cls.MODE.READ_WRITE, config=config)
@staticmethod
def default_path(cfg_path=None):
"""Return the path of the user's default database.
This reads the user's configuration file and returns the
default path of the database.
:param cfg_path: The pathname of the notmuch configuration file.
If not specified tries to use the pathname provided in the
:envvar:`NOTMUCH_CONFIG` environment variable and falls back
to :file:`~/.notmuch-config`.
:type cfg_path: str, bytes, os.PathLike or pathlib.Path.
:returns: The path of the database, which does not necessarily
exists.
:rtype: pathlib.Path
:raises OSError: or subclasses if the configuration file can not
be opened.
:raises configparser.Error: or subclasses if the configuration
file can not be parsed.
:raises NotmuchError: if the config file does not have the
database.path setting.
.. deprecated:: 0.35
Use the ``config`` parameter to :meth:`__init__` or :meth:`__create__` instead.
"""
if not cfg_path:
cfg_path = _config_pathname()
if not hasattr(os, 'PathLike') and isinstance(cfg_path, pathlib.Path):
cfg_path = bytes(cfg_path)
parser = configparser.ConfigParser()
with open(cfg_path) as fp:
parser.read_file(fp)
try:
return pathlib.Path(parser.get('database', 'path'))
except configparser.Error:
raise errors.NotmuchError(
'No database.path setting in {}'.format(cfg_path))
def __del__(self):
self._destroy()
@property
def alive(self):
try:
self._db_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
try:
ret = capi.lib.notmuch_database_destroy(self._db_p)
except errors.ObjectDestroyedError:
ret = capi.lib.NOTMUCH_STATUS_SUCCESS
else:
self._db_p = None
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def close(self):
"""Close the notmuch database.
Once closed most operations will fail. This can still be
useful however to explicitly close a database which is opened
read-write as this would otherwise stop other processes from
reading the database while it is open.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_database_close(self._db_p)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
self.closed = True
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.close()
@property
def path(self):
"""The pathname of the notmuch database.
This is returned as a :class:`pathlib.Path` instance.
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
return self._cache_path
except AttributeError:
ret = capi.lib.notmuch_database_get_path(self._db_p)
self._cache_path = pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
return self._cache_path
@property
def version(self):
"""The database format version.
This is a positive integer.
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
return self._cache_version
except AttributeError:
ret = capi.lib.notmuch_database_get_version(self._db_p)
self._cache_version = ret
return ret
@property
def needs_upgrade(self):
"""Whether the database should be upgraded.
If *True* the database can be upgraded using :meth:`upgrade`.
Not doing so may result in some operations raising
:exc:`UpgradeRequiredError`.
A read-only database will never be upgradable.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_database_needs_upgrade(self._db_p)
return bool(ret)
def upgrade(self, progress_cb=None):
"""Upgrade the database to the latest version.
Upgrade the database, optionally with a progress callback
which should be a callable which will be called with a
floating point number in the range of [0.0 .. 1.0].
"""
raise NotImplementedError
def atomic(self):
"""Return a context manager to perform atomic operations.
The returned context manager can be used to perform atomic
operations on the database.
.. note:: Unlinke a traditional RDBMS transaction this does
not imply durability, it only ensures the changes are
performed atomically.
:raises ObjectDestroyedError: if used after destroyed.
"""
ctx = AtomicContext(self, '_db_p')
return ctx
def revision(self):
"""The currently committed revision in the database.
Returned as a ``(revision, uuid)`` namedtuple.
:raises ObjectDestroyedError: if used after destroyed.
"""
raw_uuid = capi.ffi.new('char**')
rev = capi.lib.notmuch_database_get_revision(self._db_p, raw_uuid)
return DbRevision(rev, capi.ffi.string(raw_uuid[0]))
def get_directory(self, path):
raise NotImplementedError
def default_indexopts(self):
"""Returns default index options for the database.
:raises ObjectDestroyedError: if used after destroyed.
:returns: :class:`IndexOptions`.
"""
opts = capi.lib.notmuch_database_get_default_indexopts(self._db_p)
return IndexOptions(self, opts)
def add(self, filename, *, sync_flags=False, indexopts=None):
"""Add a message to the database.
Add a new message to the notmuch database. The message is
referred to by the pathname of the maildir file. If the
message ID of the new message already exists in the database,
this adds ``pathname`` to the list of list of files for the
existing message.
:param filename: The path of the file containing the message.
:type filename: str, bytes, os.PathLike or pathlib.Path.
:param sync_flags: Whether to sync the known maildir flags to
notmuch tags. See :meth:`Message.flags_to_tags` for
details.
:type sync_flags: bool
:param indexopts: The indexing options, see
:meth:`default_indexopts`. Leave as `None` to use the
default options configured in the database.
:type indexopts: :class:`IndexOptions` or `None`
:returns: A tuple where the first item is the newly inserted
messages as a :class:`Message` instance, and the second
item is a boolean indicating if the message inserted was a
duplicate. This is the namedtuple ``AddedMessage(msg,
dup)``.
:rtype: Database.AddedMessage
If an exception is raised, no message was added.
:raises XapianError: A Xapian exception occurred.
:raises FileError: The file referred to by ``pathname`` could
not be opened.
:raises FileNotEmailError: The file referreed to by
``pathname`` is not recognised as an email message.
:raises ReadOnlyDatabaseError: The database is opened in
READ_ONLY mode.
:raises UpgradeRequiredError: The database must be upgraded
first.
:raises ObjectDestroyedError: if used after destroyed.
"""
if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
filename = bytes(filename)
msg_pp = capi.ffi.new('notmuch_message_t **')
opts_p = indexopts._opts_p if indexopts else capi.ffi.NULL
ret = capi.lib.notmuch_database_index_file(
self._db_p, os.fsencode(filename), opts_p, msg_pp)
ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
if ret not in ok:
raise errors.NotmuchError(ret)
msg = message.Message(self, msg_pp[0], db=self)
if sync_flags:
msg.tags.from_maildir_flags()
return self.AddedMessage(
msg, ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
def remove(self, filename):
"""Remove a message from the notmuch database.
Removing a message which is not in the database is just a
silent nop-operation.
:param filename: The pathname of the file containing the
message to be removed.
:type filename: str, bytes, os.PathLike or pathlib.Path.
:returns: True if the message is still in the database. This
can happen when multiple files contain the same message ID.
The true/false distinction is fairly arbitrary, but think
of it as ``dup = db.remove_message(name); if dup: ...``.
:rtype: bool
:raises XapianError: A Xapian exception occurred.
:raises ReadOnlyDatabaseError: The database is opened in
READ_ONLY mode.
:raises UpgradeRequiredError: The database must be upgraded
first.
:raises ObjectDestroyedError: if used after destroyed.
"""
if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
filename = bytes(filename)
ret = capi.lib.notmuch_database_remove_message(self._db_p,
os.fsencode(filename))
ok = [capi.lib.NOTMUCH_STATUS_SUCCESS,
capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID]
if ret not in ok:
raise errors.NotmuchError(ret)
if ret == capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
return True
else:
return False
def find(self, msgid):
"""Return the message matching the given message ID.
If a message with the given message ID is found a
:class:`Message` instance is returned. Otherwise a
:exc:`LookupError` is raised.
:param msgid: The message ID to look for.
:type msgid: str
:returns: The message instance.
:rtype: Message
:raises LookupError: If no message was found.
:raises OutOfMemoryError: When there is no memory to allocate
the message instance.
:raises XapianError: A Xapian exception occurred.
:raises ObjectDestroyedError: if used after destroyed.
"""
msg_pp = capi.ffi.new('notmuch_message_t **')
ret = capi.lib.notmuch_database_find_message(self._db_p,
msgid.encode(), msg_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
msg_p = msg_pp[0]
if msg_p == capi.ffi.NULL:
raise LookupError
msg = message.Message(self, msg_p, db=self)
return msg
def get(self, filename):
"""Return the :class:`Message` given a pathname.
If a message with the given pathname exists in the database
return the :class:`Message` instance for the message.
Otherwise raise a :exc:`LookupError` exception.
:param filename: The pathname of the message.
:type filename: str, bytes, os.PathLike or pathlib.Path
:returns: The message instance.
:rtype: Message
:raises LookupError: If no message was found. This is also
a subclass of :exc:`KeyError`.
:raises OutOfMemoryError: When there is no memory to allocate
the message instance.
:raises XapianError: A Xapian exception occurred.
:raises ObjectDestroyedError: if used after destroyed.
"""
if not hasattr(os, 'PathLike') and isinstance(filename, pathlib.Path):
filename = bytes(filename)
msg_pp = capi.ffi.new('notmuch_message_t **')
ret = capi.lib.notmuch_database_find_message_by_filename(
self._db_p, os.fsencode(filename), msg_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
msg_p = msg_pp[0]
if msg_p == capi.ffi.NULL:
raise LookupError
msg = message.Message(self, msg_p, db=self)
return msg
@property
def tags(self):
"""Return an immutable set with all tags used in this database.
This returns an immutable set-like object implementing the
collections.abc.Set Abstract Base Class. Due to the
underlying libnotmuch implementation some operations have
different performance characteristics then plain set objects.
Mainly any lookup operation is O(n) rather then O(1).
Normal usage treats tags as UTF-8 encoded unicode strings so
they are exposed to Python as normal unicode string objects.
If you need to handle tags stored in libnotmuch which are not
valid unicode do check the :class:`ImmutableTagSet` docs for
how to handle this.
:rtype: ImmutableTagSet
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
ref = self._cached_tagset
except AttributeError:
tagset = None
else:
tagset = ref()
if tagset is None:
tagset = tags.ImmutableTagSet(
self, '_db_p', capi.lib.notmuch_database_get_all_tags)
self._cached_tagset = weakref.ref(tagset)
return tagset
@property
def config(self):
"""Return a mutable mapping with the settings stored in this database.
This returns an mutable dict-like object implementing the
collections.abc.MutableMapping Abstract Base Class.
:rtype: Config
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
ref = self._cached_config
except AttributeError:
config_mapping = None
else:
config_mapping = ref()
if config_mapping is None:
config_mapping = config.ConfigMapping(self, '_db_p')
self._cached_config = weakref.ref(config_mapping)
return config_mapping
def _create_query(self, query, *,
omit_excluded=EXCLUDE.TRUE,
sort=SORT.UNSORTED, # Check this default
exclude_tags=None):
"""Create an internal query object.
:raises OutOfMemoryError: if no memory is available to
allocate the query.
"""
if isinstance(query, str):
query = query.encode('utf-8')
query_p = capi.lib.notmuch_query_create(self._db_p, query)
if query_p == capi.ffi.NULL:
raise errors.OutOfMemoryError()
capi.lib.notmuch_query_set_omit_excluded(query_p, omit_excluded.value)
capi.lib.notmuch_query_set_sort(query_p, sort.value)
if exclude_tags is not None:
for tag in exclude_tags:
if isinstance(tag, str):
tag = tag.encode('utf-8')
capi.lib.notmuch_query_add_tag_exclude(query_p, tag)
return querymod.Query(self, query_p)
def messages(self, query, *,
omit_excluded=EXCLUDE.TRUE,
sort=SORT.UNSORTED, # Check this default
exclude_tags=None):
"""Search the database for messages.
:returns: An iterator over the messages found.
:rtype: MessageIter
:raises OutOfMemoryError: if no memory is available to
allocate the query.
:raises ObjectDestroyedError: if used after destroyed.
"""
query = self._create_query(query,
omit_excluded=omit_excluded,
sort=sort,
exclude_tags=exclude_tags)
return query.messages()
def count_messages(self, query, *,
omit_excluded=EXCLUDE.TRUE,
sort=SORT.UNSORTED, # Check this default
exclude_tags=None):
"""Search the database for messages.
:returns: An iterator over the messages found.
:rtype: MessageIter
:raises ObjectDestroyedError: if used after destroyed.
"""
query = self._create_query(query,
omit_excluded=omit_excluded,
sort=sort,
exclude_tags=exclude_tags)
return query.count_messages()
def threads(self, query, *,
omit_excluded=EXCLUDE.TRUE,
sort=SORT.UNSORTED, # Check this default
exclude_tags=None):
query = self._create_query(query,
omit_excluded=omit_excluded,
sort=sort,
exclude_tags=exclude_tags)
return query.threads()
def count_threads(self, query, *,
omit_excluded=EXCLUDE.TRUE,
sort=SORT.UNSORTED, # Check this default
exclude_tags=None):
query = self._create_query(query,
omit_excluded=omit_excluded,
sort=sort,
exclude_tags=exclude_tags)
return query.count_threads()
def status_string(self):
raise NotImplementedError
def __repr__(self):
return 'Database(path={self.path}, mode={self.mode})'.format(self=self)
class AtomicContext:
"""Context manager for atomic support.
This supports the notmuch_database_begin_atomic and
notmuch_database_end_atomic API calls. The object can not be
directly instantiated by the user, only via ``Database.atomic``.
It does keep a reference to the :class:`Database` instance to keep
the C memory alive.
:raises XapianError: When this is raised at enter time the atomic
section is not active. When it is raised at exit time the
atomic section is still active and you may need to try using
:meth:`force_end`.
:raises ObjectDestroyedError: if used after destroyed.
"""
def __init__(self, db, ptr_name):
self._db = db
self._ptr = lambda: getattr(db, ptr_name)
self._exit_fn = lambda: None
def __del__(self):
self._destroy()
@property
def alive(self):
return self.parent.alive
def _destroy(self):
pass
def __enter__(self):
ret = capi.lib.notmuch_database_begin_atomic(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
self._exit_fn = self._end_atomic
return self
def _end_atomic(self):
ret = capi.lib.notmuch_database_end_atomic(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def __exit__(self, exc_type, exc_value, traceback):
self._exit_fn()
def force_end(self):
"""Force ending the atomic section.
This can only be called once __exit__ has been called. It
will attempt to close the atomic section (again). This is
useful if the original exit raised an exception and the atomic
section is still open. But things are pretty ugly by now.
:raises XapianError: If exiting fails, the atomic section is
not ended.
:raises UnbalancedAtomicError: If the database was currently
not in an atomic section.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_database_end_atomic(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def abort(self):
"""Abort the transaction.
Aborting a transaction will not commit any of the changes, but
will also implicitly close the database.
"""
self._exit_fn = lambda: None
self._db.close()
@functools.total_ordering
class DbRevision:
"""A database revision.
The database revision number increases monotonically with each
commit to the database. Which means user-visible changes can be
ordered. This object is sortable with other revisions. It
carries the UUID of the database to ensure it is only ever
compared with revisions from the same database.
"""
def __init__(self, rev, uuid):
self._rev = rev
self._uuid = uuid
@property
def rev(self):
"""The revision number, a positive integer."""
return self._rev
@property
def uuid(self):
"""The UUID of the database, consider this opaque."""
return self._uuid
def __eq__(self, other):
if isinstance(other, self.__class__):
if self.uuid != other.uuid:
return False
return self.rev == other.rev
else:
return NotImplemented
def __lt__(self, other):
if self.__class__ is other.__class__:
if self.uuid != other.uuid:
return False
return self.rev < other.rev
else:
return NotImplemented
def __repr__(self):
return 'DbRevision(rev={self.rev}, uuid={self.uuid})'.format(self=self)
class IndexOptions(base.NotmuchObject):
"""Indexing options.
This represents the indexing options which can be used to index a
message. See :meth:`Database.default_indexopts` to create an
instance of this. It can be used e.g. when indexing a new message
using :meth:`Database.add`.
"""
_opts_p = base.MemoryPointer()
def __init__(self, parent, opts_p):
self._parent = parent
self._opts_p = opts_p
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._opts_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
if self.alive:
capi.lib.notmuch_indexopts_destroy(self._opts_p)
self._opts_p = None
@property
def decrypt_policy(self):
"""The decryption policy.
This is an enum from the :class:`DecryptionPolicy`. See the
`index.decrypt` section in :man:`notmuch-config` for details
on the options. **Do not set this to
:attr:`DecryptionPolicy.TRUE`** without considering the
security of your index.
You can change this policy by assigning a new
:class:`DecryptionPolicy` to this property.
:raises ObjectDestroyedError: if used after destroyed.
:returns: A :class:`DecryptionPolicy` enum instance.
"""
raw = capi.lib.notmuch_indexopts_get_decrypt_policy(self._opts_p)
return DecryptionPolicy(raw)
@decrypt_policy.setter
def decrypt_policy(self, val):
ret = capi.lib.notmuch_indexopts_set_decrypt_policy(
self._opts_p, val.value)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret, msg)

View file

@ -0,0 +1,124 @@
from notmuch2 import _capi as capi
class NotmuchError(Exception):
"""Base exception for errors originating from the notmuch library.
Usually this will have two attributes:
:status: This is a numeric status code corresponding to the error
code in the notmuch library. This is normally fairly
meaningless, it can also often be ``None``. This exists mostly
to easily create new errors from notmuch status codes and
should not normally be used by users.
:message: A user-facing message for the error. This can
occasionally also be ``None``. Usually you'll want to call
``str()`` on the error object instead to get a sensible
message.
"""
@classmethod
def exc_type(cls, status):
"""Return correct exception type for notmuch status."""
types = {
capi.lib.NOTMUCH_STATUS_OUT_OF_MEMORY:
OutOfMemoryError,
capi.lib.NOTMUCH_STATUS_READ_ONLY_DATABASE:
ReadOnlyDatabaseError,
capi.lib.NOTMUCH_STATUS_XAPIAN_EXCEPTION:
XapianError,
capi.lib.NOTMUCH_STATUS_FILE_ERROR:
FileError,
capi.lib.NOTMUCH_STATUS_FILE_NOT_EMAIL:
FileNotEmailError,
capi.lib.NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
DuplicateMessageIdError,
capi.lib.NOTMUCH_STATUS_NULL_POINTER:
NullPointerError,
capi.lib.NOTMUCH_STATUS_TAG_TOO_LONG:
TagTooLongError,
capi.lib.NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
UnbalancedFreezeThawError,
capi.lib.NOTMUCH_STATUS_UNBALANCED_ATOMIC:
UnbalancedAtomicError,
capi.lib.NOTMUCH_STATUS_UNSUPPORTED_OPERATION:
UnsupportedOperationError,
capi.lib.NOTMUCH_STATUS_UPGRADE_REQUIRED:
UpgradeRequiredError,
capi.lib.NOTMUCH_STATUS_PATH_ERROR:
PathError,
capi.lib.NOTMUCH_STATUS_ILLEGAL_ARGUMENT:
IllegalArgumentError,
capi.lib.NOTMUCH_STATUS_NO_CONFIG:
NoConfigError,
capi.lib.NOTMUCH_STATUS_NO_DATABASE:
NoDatabaseError,
capi.lib.NOTMUCH_STATUS_DATABASE_EXISTS:
DatabaseExistsError,
capi.lib.NOTMUCH_STATUS_BAD_QUERY_SYNTAX:
QuerySyntaxError,
}
return types[status]
def __new__(cls, *args, **kwargs):
"""Return the correct subclass based on status."""
# This is simplistic, but the actual __init__ will fail if the
# signature is wrong anyway.
if args:
status = args[0]
else:
status = kwargs.get('status', None)
if status and cls == NotmuchError:
exc = cls.exc_type(status)
return exc.__new__(exc, *args, **kwargs)
else:
return super().__new__(cls)
def __init__(self, status=None, message=None):
self.status = status
self.message = message
def __str__(self):
if self.message:
return self.message
elif self.status:
char_str = capi.lib.notmuch_status_to_string(self.status)
return capi.ffi.string(char_str).decode(errors='replace')
else:
return 'Unknown error'
class OutOfMemoryError(NotmuchError): pass
class ReadOnlyDatabaseError(NotmuchError): pass
class XapianError(NotmuchError): pass
class FileError(NotmuchError): pass
class FileNotEmailError(NotmuchError): pass
class DuplicateMessageIdError(NotmuchError): pass
class NullPointerError(NotmuchError): pass
class TagTooLongError(NotmuchError): pass
class UnbalancedFreezeThawError(NotmuchError): pass
class UnbalancedAtomicError(NotmuchError): pass
class UnsupportedOperationError(NotmuchError): pass
class UpgradeRequiredError(NotmuchError): pass
class PathError(NotmuchError): pass
class IllegalArgumentError(NotmuchError): pass
class NoConfigError(NotmuchError): pass
class NoDatabaseError(NotmuchError): pass
class DatabaseExistsError(NotmuchError): pass
class QuerySyntaxError(NotmuchError): pass
class ObjectDestroyedError(NotmuchError):
"""The object has already been destroyed and it's memory freed.
This occurs when :meth:`destroy` has been called on the object but
you still happen to have access to the object. This should not
normally occur since you should never call :meth:`destroy` by
hand.
"""
def __str__(self):
if self.message:
return self.message
else:
return 'Memory already freed'

View file

@ -0,0 +1,724 @@
import collections
import contextlib
import os
import pathlib
import weakref
import notmuch2._base as base
import notmuch2._capi as capi
import notmuch2._errors as errors
import notmuch2._tags as tags
__all__ = ['Message']
class Message(base.NotmuchObject):
"""An email message stored in the notmuch database retrieved via a query.
This should not be directly created, instead it will be returned
by calling methods on :class:`Database`. A message keeps a
reference to the database object since the database object can not
be released while the message is in use.
Note that this represents a message in the notmuch database. For
full email functionality you may want to use the :mod:`email`
package from Python's standard library. You could e.g. create
this as such::
notmuch_msg = db.find(msgid) # or from a query
parser = email.parser.BytesParser(policy=email.policy.default)
with notmuch_msg.path.open('rb) as fp:
email_msg = parser.parse(fp)
Most commonly the functionality provided by notmuch is sufficient
to read email however.
Messages are considered equal when they have the same message ID.
This is how libnotmuch treats messages as well, the
:meth:`pathnames` function returns multiple results for
duplicates.
:param parent: The parent object. This is probably one off a
:class:`Database`, :class:`Thread` or :class:`Query`.
:type parent: NotmuchObject
:param db: The database instance this message is associated with.
This could be the same as the parent.
:type db: Database
:param msg_p: The C pointer to the ``notmuch_message_t``.
:type msg_p: <cdata>
:param dup: Whether the message was a duplicate on insertion.
:type dup: None or bool
"""
_msg_p = base.MemoryPointer()
def __init__(self, parent, msg_p, *, db):
self._parent = parent
self._msg_p = msg_p
self._db = db
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._msg_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self):
if self.alive:
capi.lib.notmuch_message_destroy(self._msg_p)
self._msg_p = None
@property
def messageid(self):
"""The message ID as a string.
The message ID is decoded with the ignore error handler. This
is fine as long as the message ID is well formed. If it is
not valid ASCII then this will be lossy. So if you need to be
able to write the exact same message ID back you should use
:attr:`messageidb`.
Note that notmuch will decode the message ID value and thus
strip off the surrounding ``<`` and ``>`` characters. This is
different from Python's :mod:`email` package behaviour which
leaves these characters in place.
:returns: The message ID.
:rtype: :class:`BinString`, this is a normal str but calling
bytes() on it will return the original bytes used to create
it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_message_id(self._msg_p)
return base.BinString(capi.ffi.string(ret))
@property
def threadid(self):
"""The thread ID.
The thread ID is decoded with the surrogateescape error
handler so that it is possible to reconstruct the original
thread ID if it is not valid UTF-8.
:returns: The thread ID.
:rtype: :class:`BinString`, this is a normal str but calling
bytes() on it will return the original bytes used to create
it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_thread_id(self._msg_p)
return base.BinString(capi.ffi.string(ret))
@property
def path(self):
"""A pathname of the message as a pathlib.Path instance.
If multiple files in the database contain the same message ID
this will be just one of the files, chosen at random.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return pathlib.Path(os.fsdecode(capi.ffi.string(ret)))
@property
def pathb(self):
"""A pathname of the message as a bytes object.
See :attr:`path` for details, this is the same but does return
the path as a bytes object which is faster but less convenient.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_filename(self._msg_p)
return capi.ffi.string(ret)
def filenames(self):
"""Return an iterator of all files for this message.
If multiple files contained the same message ID they will all
be returned here. The files are returned as instances of
:class:`pathlib.Path`.
:returns: Iterator yielding :class:`pathlib.Path` instances.
:rtype: iter
:raises ObjectDestroyedError: if used after destroyed.
"""
fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
return PathIter(self, fnames_p)
def filenamesb(self):
"""Return an iterator of all files for this message.
This is like :meth:`pathnames` but the files are returned as
byte objects instead.
:returns: Iterator yielding :class:`bytes` instances.
:rtype: iter
:raises ObjectDestroyedError: if used after destroyed.
"""
fnames_p = capi.lib.notmuch_message_get_filenames(self._msg_p)
return FilenamesIter(self, fnames_p)
@property
def ghost(self):
"""Indicates whether this message is a ghost message.
A ghost message if a message which we know exists, but it has
no files or content associated with it. This can happen if
it was referenced by some other message. Only the
:attr:`messageid` and :attr:`threadid` attributes are valid
for it.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_flag(
self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_GHOST)
return bool(ret)
@property
def excluded(self):
"""Indicates whether this message was excluded from the query.
When a message is created from a search, sometimes messages
that where excluded by the search query could still be
returned by it, e.g. because they are part of a thread
matching the query. the :meth:`Database.query` method allows
these messages to be flagged, which results in this property
being set to *True*.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_flag(
self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_EXCLUDED)
return bool(ret)
@property
def matched(self):
"""Indicates whether this message was matched by the query.
When a thread is created from a search, some of the
messages may not match the original query. This property
is set to *True* for those that do match.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_get_flag(
self._msg_p, capi.lib.NOTMUCH_MESSAGE_FLAG_MATCH)
return bool(ret)
@property
def date(self):
"""The message date as an integer.
The time the message was sent as an integer number of seconds
since the *epoch*, 1 Jan 1970. This is derived from the
message's header, you can get the original header value with
:meth:`header`.
:raises ObjectDestroyedError: if used after destroyed.
"""
return capi.lib.notmuch_message_get_date(self._msg_p)
def header(self, name):
"""Return the value of the named header.
Returns the header from notmuch, some common headers are
stored in the database, others are read from the file.
Headers are returned with their newlines stripped and
collapsed concatenated together if they occur multiple times.
You may be better off using the standard library email
package's ``email.message_from_file(msg.path.open())`` if that
is not sufficient for you.
:param header: Case-insensitive header name to retrieve.
:type header: str or bytes
:returns: The header value, an empty string if the header is
not present.
:rtype: str
:raises LookupError: if the header is not present.
:raises NullPointerError: For unexpected notmuch errors.
:raises ObjectDestroyedError: if used after destroyed.
"""
# The returned is supposedly guaranteed to be UTF-8. Header
# names must be ASCII as per RFC x822.
if isinstance(name, str):
name = name.encode('ascii')
ret = capi.lib.notmuch_message_get_header(self._msg_p, name)
if ret == capi.ffi.NULL:
raise errors.NullPointerError()
hdr = capi.ffi.string(ret)
if not hdr:
raise LookupError
return hdr.decode(encoding='utf-8')
@property
def tags(self):
"""The tags associated with the message.
This behaves as a set. But removing and adding items to the
set removes and adds them to the message in the database.
:raises ReadOnlyDatabaseError: When manipulating tags on a
database opened in read-only mode.
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
ref = self._cached_tagset
except AttributeError:
tagset = None
else:
tagset = ref()
if tagset is None:
tagset = tags.MutableTagSet(
self, '_msg_p', capi.lib.notmuch_message_get_tags)
self._cached_tagset = weakref.ref(tagset)
return tagset
@contextlib.contextmanager
def frozen(self):
"""Context manager to freeze the message state.
This allows you to perform atomic tag updates::
with msg.frozen():
msg.tags.clear()
msg.tags.add('foo')
Using This would ensure the message never ends up with no tags
applied at all.
It is safe to nest calls to this context manager.
:raises ReadOnlyDatabaseError: if the database is opened in
read-only mode.
:raises UnbalancedFreezeThawError: if you somehow managed to
call __exit__ of this context manager more than once. Why
did you do that?
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_message_freeze(self._msg_p)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
self._frozen = True
try:
yield
except Exception:
# Only way to "rollback" these changes is to destroy
# ourselves and re-create. Behold.
msgid = self.messageid
self._destroy()
with contextlib.suppress(Exception):
new = self._db.find(msgid)
self._msg_p = new._msg_p
new._msg_p = None
del new
raise
else:
ret = capi.lib.notmuch_message_thaw(self._msg_p)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
self._frozen = False
@property
def properties(self):
"""A map of arbitrary key-value pairs associated with the message.
Be aware that properties may be used by other extensions to
store state in. So delete or modify with care.
The properties map is somewhat special. It is essentially a
multimap-like structure where each key can have multiple
values. Therefore accessing a single item using
:meth:`PropertiesMap.get` or :meth:`PropertiesMap.__getitem__`
will only return you the *first* item if there are multiple
and thus are only recommended if you know there to be only one
value.
Instead the map has an additional :meth:`PropertiesMap.all`
method which can be used to retrieve all properties of a given
key. This method also allows iterating of a a subset of the
keys starting with a given prefix.
"""
try:
ref = self._cached_props
except AttributeError:
props = None
else:
props = ref()
if props is None:
props = PropertiesMap(self, '_msg_p')
self._cached_props = weakref.ref(props)
return props
def replies(self):
"""Return an iterator of all replies to this message.
This method will only work if the message was created from a
thread. Otherwise it will yield no results.
:returns: An iterator yielding :class:`OwnedMessage` instances.
:rtype: MessageIter
"""
# The notmuch_messages_valid call accepts NULL and this will
# become an empty iterator, raising StopIteration immediately.
# Hence no return value checking here.
msgs_p = capi.lib.notmuch_message_get_replies(self._msg_p)
return MessageIter(self, msgs_p, db=self._db, msg_cls=OwnedMessage)
def __hash__(self):
return hash(self.messageid)
def __eq__(self, other):
if isinstance(other, self.__class__):
return self.messageid == other.messageid
class OwnedMessage(Message):
"""An email message owned by parent thread object.
This subclass of Message is used for messages that are retrieved
from the notmuch database via a parent :class:`notmuch2.Thread`
object, which "owns" this message. This means that when this
message object is destroyed, by calling :func:`del` or
:meth:`_destroy` directly or indirectly, the message is not freed
in the notmuch API and the parent :class:`notmuch2.Thread` object
can return the same object again when needed.
"""
@property
def alive(self):
return self._parent.alive
def _destroy(self):
pass
class FilenamesIter(base.NotmuchIter):
"""Iterator for binary filenames objects."""
def __init__(self, parent, iter_p):
super().__init__(parent, iter_p,
fn_destroy=capi.lib.notmuch_filenames_destroy,
fn_valid=capi.lib.notmuch_filenames_valid,
fn_get=capi.lib.notmuch_filenames_get,
fn_next=capi.lib.notmuch_filenames_move_to_next)
def __next__(self):
fname = super().__next__()
return capi.ffi.string(fname)
class PathIter(FilenamesIter):
"""Iterator for pathlib.Path objects."""
def __next__(self):
fname = super().__next__()
return pathlib.Path(os.fsdecode(fname))
class PropertiesMap(base.NotmuchObject, collections.abc.MutableMapping):
"""A mutable mapping to manage properties.
Both keys and values of properties are supposed to be UTF-8
strings in libnotmuch. However since the uderlying API uses
bytestrings you can use either str or bytes to represent keys and
all returned keys and values use :class:`BinString`.
Also be aware that ``iter(this_map)`` will return duplicate keys,
while the :class:`collections.abc.KeysView` returned by
:meth:`keys` is a :class:`collections.abc.Set` subclass. This
means the former will yield duplicate keys while the latter won't.
It also means ``len(list(iter(this_map)))`` could be different
than ``len(this_map.keys())``. ``len(this_map)`` will correspond
with the length of the default iterator.
Be aware that libnotmuch exposes all of this as iterators, so
quite a few operations have O(n) performance instead of the usual
O(1).
"""
Property = collections.namedtuple('Property', ['key', 'value'])
_marker = object()
def __init__(self, msg, ptr_name):
self._msg = msg
self._ptr = lambda: getattr(msg, ptr_name)
@property
def alive(self):
if not self._msg.alive:
return False
try:
self._ptr
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
pass
def __iter__(self):
"""Return an iterator which iterates over the keys.
Be aware that a single key may have multiple values associated
with it, if so it will appear multiple times here.
"""
iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
return PropertiesKeyIter(self, iter_p)
def __len__(self):
iter_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
it = base.NotmuchIter(
self, iter_p,
fn_destroy=capi.lib.notmuch_message_properties_destroy,
fn_valid=capi.lib.notmuch_message_properties_valid,
fn_get=capi.lib.notmuch_message_properties_key,
fn_next=capi.lib.notmuch_message_properties_move_to_next,
)
return len(list(it))
def __getitem__(self, key):
"""Return **the first** peroperty associated with a key."""
if isinstance(key, str):
key = key.encode('utf-8')
value_pp = capi.ffi.new('char**')
ret = capi.lib.notmuch_message_get_property(self._ptr(), key, value_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
if value_pp[0] == capi.ffi.NULL:
raise KeyError
return base.BinString.from_cffi(value_pp[0])
def keys(self):
"""Return a :class:`collections.abc.KeysView` for this map.
Even when keys occur multiple times this is a subset of set()
so will only contain them once.
"""
return collections.abc.KeysView({k: None for k in self})
def items(self):
"""Return a :class:`collections.abc.ItemsView` for this map.
The ItemsView treats a ``(key, value)`` pair as unique, so
dupcliate ``(key, value)`` pairs will be merged together.
However duplicate keys with different values will be returned.
"""
items = set()
props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
while capi.lib.notmuch_message_properties_valid(props_p):
key = capi.lib.notmuch_message_properties_key(props_p)
value = capi.lib.notmuch_message_properties_value(props_p)
items.add((base.BinString.from_cffi(key),
base.BinString.from_cffi(value)))
capi.lib.notmuch_message_properties_move_to_next(props_p)
capi.lib.notmuch_message_properties_destroy(props_p)
return PropertiesItemsView(items)
def values(self):
"""Return a :class:`collecions.abc.ValuesView` for this map.
All unique property values are included in the view.
"""
values = set()
props_p = capi.lib.notmuch_message_get_properties(self._ptr(), b'', 0)
while capi.lib.notmuch_message_properties_valid(props_p):
value = capi.lib.notmuch_message_properties_value(props_p)
values.add(base.BinString.from_cffi(value))
capi.lib.notmuch_message_properties_move_to_next(props_p)
capi.lib.notmuch_message_properties_destroy(props_p)
return PropertiesValuesView(values)
def __setitem__(self, key, value):
"""Add a key-value pair to the properties.
You may prefer to use :meth:`add` for clarity since this
method usually implies implicit overwriting of an existing key
if it exists, while for properties this is not the case.
"""
self.add(key, value)
def add(self, key, value):
"""Add a key-value pair to the properties."""
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(value, str):
value = value.encode('utf-8')
ret = capi.lib.notmuch_message_add_property(self._ptr(), key, value)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def __delitem__(self, key):
"""Remove all properties with this key."""
if isinstance(key, str):
key = key.encode('utf-8')
ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(), key)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def remove(self, key, value):
"""Remove a key-value pair from the properties."""
if isinstance(key, str):
key = key.encode('utf-8')
if isinstance(value, str):
value = value.encode('utf-8')
ret = capi.lib.notmuch_message_remove_property(self._ptr(), key, value)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def pop(self, key, default=_marker):
try:
value = self[key]
except KeyError:
if default is self._marker:
raise
else:
return default
else:
self.remove(key, value)
return value
def popitem(self):
try:
key = next(iter(self))
except StopIteration:
raise KeyError
value = self.pop(key)
return (key, value)
def clear(self):
ret = capi.lib.notmuch_message_remove_all_properties(self._ptr(),
capi.ffi.NULL)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def getall(self, prefix='', *, exact=False):
"""Return an iterator yielding all properties for a given key prefix.
The returned iterator yields all peroperties which start with
a given key prefix as ``(key, value)`` namedtuples. If called
with ``exact=True`` then only properties which exactly match
the prefix are returned, those a key longer than the prefix
will not be included.
:param prefix: The prefix of the key.
"""
if isinstance(prefix, str):
prefix = prefix.encode('utf-8')
props_p = capi.lib.notmuch_message_get_properties(self._ptr(),
prefix, exact)
return PropertiesIter(self, props_p)
class PropertiesKeyIter(base.NotmuchIter):
def __init__(self, parent, iter_p):
super().__init__(
parent,
iter_p,
fn_destroy=capi.lib.notmuch_message_properties_destroy,
fn_valid=capi.lib.notmuch_message_properties_valid,
fn_get=capi.lib.notmuch_message_properties_key,
fn_next=capi.lib.notmuch_message_properties_move_to_next)
def __next__(self):
item = super().__next__()
return base.BinString.from_cffi(item)
class PropertiesIter(base.NotmuchIter):
def __init__(self, parent, iter_p):
super().__init__(
parent,
iter_p,
fn_destroy=capi.lib.notmuch_message_properties_destroy,
fn_valid=capi.lib.notmuch_message_properties_valid,
fn_get=capi.lib.notmuch_message_properties_key,
fn_next=capi.lib.notmuch_message_properties_move_to_next,
)
def __next__(self):
if not self._fn_valid(self._iter_p):
self._destroy()
raise StopIteration
key = capi.lib.notmuch_message_properties_key(self._iter_p)
value = capi.lib.notmuch_message_properties_value(self._iter_p)
capi.lib.notmuch_message_properties_move_to_next(self._iter_p)
return PropertiesMap.Property(base.BinString.from_cffi(key),
base.BinString.from_cffi(value))
class PropertiesItemsView(collections.abc.Set):
__slots__ = ('_items',)
def __init__(self, items):
self._items = items
@classmethod
def _from_iterable(self, it):
return set(it)
def __len__(self):
return len(self._items)
def __contains__(self, item):
return item in self._items
def __iter__(self):
yield from self._items
collections.abc.ItemsView.register(PropertiesItemsView)
class PropertiesValuesView(collections.abc.Set):
__slots__ = ('_values',)
def __init__(self, values):
self._values = values
def __len__(self):
return len(self._values)
def __contains__(self, value):
return value in self._values
def __iter__(self):
yield from self._values
collections.abc.ValuesView.register(PropertiesValuesView)
class MessageIter(base.NotmuchIter):
def __init__(self, parent, msgs_p, *, db, msg_cls=Message):
self._db = db
self._msg_cls = msg_cls
super().__init__(parent, msgs_p,
fn_destroy=capi.lib.notmuch_messages_destroy,
fn_valid=capi.lib.notmuch_messages_valid,
fn_get=capi.lib.notmuch_messages_get,
fn_next=capi.lib.notmuch_messages_move_to_next)
def __next__(self):
msg_p = super().__next__()
return self._msg_cls(self, msg_p, db=self._db)

View file

@ -0,0 +1,83 @@
from notmuch2 import _base as base
from notmuch2 import _capi as capi
from notmuch2 import _errors as errors
from notmuch2 import _message as message
from notmuch2 import _thread as thread
__all__ = []
class Query(base.NotmuchObject):
"""Private, minimal query object.
This is not meant for users and is not a full implementation of
the query API. It is only an intermediate used internally to
match libnotmuch's memory management.
"""
_query_p = base.MemoryPointer()
def __init__(self, db, query_p):
self._db = db
self._query_p = query_p
@property
def alive(self):
if not self._db.alive:
return False
try:
self._query_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self):
if self.alive:
capi.lib.notmuch_query_destroy(self._query_p)
self._query_p = None
@property
def query(self):
"""The query string as seen by libnotmuch."""
q = capi.lib.notmuch_query_get_query_string(self._query_p)
return base.BinString.from_cffi(q)
def messages(self):
"""Return an iterator over all the messages found by the query.
This executes the query and returns an iterator over the
:class:`Message` objects found.
"""
msgs_pp = capi.ffi.new('notmuch_messages_t**')
ret = capi.lib.notmuch_query_search_messages(self._query_p, msgs_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
return message.MessageIter(self, msgs_pp[0], db=self._db)
def count_messages(self):
"""Return the number of messages matching this query."""
count_p = capi.ffi.new('unsigned int *')
ret = capi.lib.notmuch_query_count_messages(self._query_p, count_p)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
return count_p[0]
def threads(self):
"""Return an iterator over all the threads found by the query."""
threads_pp = capi.ffi.new('notmuch_threads_t **')
ret = capi.lib.notmuch_query_search_threads(self._query_p, threads_pp)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
return thread.ThreadIter(self, threads_pp[0], db=self._db)
def count_threads(self):
"""Return the number of threads matching this query."""
count_p = capi.ffi.new('unsigned int *')
ret = capi.lib.notmuch_query_count_threads(self._query_p, count_p)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
return count_p[0]

View file

@ -0,0 +1,359 @@
import collections.abc
import notmuch2._base as base
import notmuch2._capi as capi
import notmuch2._errors as errors
__all__ = ['ImmutableTagSet', 'MutableTagSet', 'TagsIter']
class ImmutableTagSet(base.NotmuchObject, collections.abc.Set):
"""The tags associated with a message thread or whole database.
Both a thread as well as the database expose the union of all tags
in messages associated with them. This exposes these as a
:class:`collections.abc.Set` object.
Note that due to the underlying notmuch API the performance of the
implementation is not the same as you would expect from normal
sets. E.g. the :meth:`__contains__` and :meth:`__len__` are O(n)
rather then O(1).
Tags are internally stored as bytestrings but normally exposed as
unicode strings using the UTF-8 encoding and the *ignore* decoder
error handler. However the :meth:`iter` method can be used to
return tags as bytestrings or using a different error handler.
Note that when doing arithmetic operations on tags, this class
will return a plain normal set as it is no longer associated with
the message.
:param parent: the parent object
:param ptr_name: the name of the attribute on the parent which will
return the memory pointer. This allows this object to
access the pointer via the parent's descriptor and thus
trigger :class:`MemoryPointer`'s memory safety.
:param cffi_fn: the callable CFFI wrapper to retrieve the tags
iter. This can be one of notmuch_database_get_all_tags,
notmuch_thread_get_tags or notmuch_message_get_tags.
"""
def __init__(self, parent, ptr_name, cffi_fn):
self._parent = parent
self._ptr = lambda: getattr(parent, ptr_name)
self._cffi_fn = cffi_fn
def __del__(self):
self._destroy()
@property
def alive(self):
return self._parent.alive
def _destroy(self):
pass
@classmethod
def _from_iterable(cls, it):
return set(it)
def __iter__(self):
"""Return an iterator over the tags.
Tags are yielded as unicode strings, decoded using the
"ignore" error handler.
:raises NullPointerError: If the iterator can not be created.
"""
return self.iter(encoding='utf-8', errors='ignore')
def iter(self, *, encoding=None, errors='strict'):
"""Aternate iterator constructor controlling string decoding.
Tags are stored as bytes in the notmuch database, in Python
it's easier to work with unicode strings and thus is what the
normal iterator returns. However this method allows you to
specify how you would like to get the tags, defaulting to the
bytestring representation instead of unicode strings.
:param encoding: Which codec to use. The default *None* does not
decode at all and will return the unmodified bytes.
Otherwise this is passed on to :func:`str.decode`.
:param errors: If using a codec, this is the error handler.
See :func:`str.decode` to which this is passed on.
:raises NullPointerError: When things do not go as planned.
"""
# self._cffi_fn should point either to
# notmuch_database_get_all_tags, notmuch_thread_get_tags or
# notmuch_message_get_tags. nothmuch.h suggests these never
# fail, let's handle NULL anyway.
tags_p = self._cffi_fn(self._ptr())
if tags_p == capi.ffi.NULL:
raise errors.NullPointerError()
tags = TagsIter(self, tags_p, encoding=encoding, errors=errors)
return tags
def __len__(self):
return sum(1 for t in self)
def __contains__(self, tag):
if isinstance(tag, str):
tag = tag.encode()
for msg_tag in self.iter():
if tag == msg_tag:
return True
else:
return False
def __eq__(self, other):
return tuple(sorted(self.iter())) == tuple(sorted(other.iter()))
def issubset(self, other):
return self <= other
def issuperset(self, other):
return self >= other
def union(self, other):
return self | other
def intersection(self, other):
return self & other
def difference(self, other):
return self - other
def symmetric_difference(self, other):
return self ^ other
def copy(self):
return set(self)
def __hash__(self):
return hash(tuple(self.iter()))
def __repr__(self):
return '<{name} object at 0x{addr:x} tags={{{tags}}}>'.format(
name=self.__class__.__name__,
addr=id(self),
tags=', '.join(repr(t) for t in self))
class MutableTagSet(ImmutableTagSet, collections.abc.MutableSet):
"""The tags associated with a message.
This is a :class:`collections.abc.MutableSet` object which can be
used to manipulate the tags of a message.
Note that due to the underlying notmuch API the performance of the
implementation is not the same as you would expect from normal
sets. E.g. the ``in`` operator and variants are O(n) rather then
O(1).
Tags are bytestrings and calling ``iter()`` will return an
iterator yielding bytestrings. However the :meth:`iter` method
can be used to return tags as unicode strings, while all other
operations accept either byestrings or unicode strings. In case
unicode strings are used they will be encoded using utf-8 before
being passed to notmuch.
"""
# Since we subclass ImmutableTagSet we inherit a __hash__. But we
# are mutable, setting it to None will make the Python machinery
# recognise us as unhashable.
__hash__ = None
def add(self, tag):
"""Add a tag to the message.
:param tag: The tag to add.
:type tag: str or bytes. A str will be encoded using UTF-8.
:param sync_flags: Whether to sync the maildir flags with the
new set of tags. Leaving this as *None* respects the
configuration set in the database, while *True* will always
sync and *False* will never sync.
:param sync_flags: NoneType or bool
:raises TypeError: If the tag is not a valid type.
:raises TagTooLongError: If the added tag exceeds the maximum
length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
if isinstance(tag, str):
tag = tag.encode()
if not isinstance(tag, bytes):
raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
ret = capi.lib.notmuch_message_add_tag(self._ptr(), tag)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def discard(self, tag):
"""Remove a tag from the message.
:param tag: The tag to remove.
:type tag: str of bytes. A str will be encoded using UTF-8.
:param sync_flags: Whether to sync the maildir flags with the
new set of tags. Leaving this as *None* respects the
configuration set in the database, while *True* will always
sync and *False* will never sync.
:param sync_flags: NoneType or bool
:raises TypeError: If the tag is not a valid type.
:raises TagTooLongError: If the tag exceeds the maximum
length, see ``notmuch_cffi.NOTMUCH_TAG_MAX``.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
if isinstance(tag, str):
tag = tag.encode()
if not isinstance(tag, bytes):
raise TypeError('Not a valid type for a tag: {}'.format(type(tag)))
ret = capi.lib.notmuch_message_remove_tag(self._ptr(), tag)
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def clear(self):
"""Remove all tags from the message.
:raises ReadOnlyDatabaseError: If the database is opened in
read-only mode.
"""
ret = capi.lib.notmuch_message_remove_all_tags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def from_maildir_flags(self):
"""Update the tags based on the state in the message's maildir flags.
This function examines the filenames of 'message' for maildir
flags, and adds or removes tags on 'message' as follows when
these flags are present:
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.)
"""
ret = capi.lib.notmuch_message_maildir_flags_to_tags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
def to_maildir_flags(self):
"""Update the message's maildir flags based on the notmuch tags.
If the message's filename is in a maildir directory, that is a
directory named ``new`` or ``cur``, and has a valid maildir
filename then the flags will be added as such:
'D' if the message has the "draft" tag
'F' if the message has the "flagged" tag
'P' if the message has the "passed" tag
'R' if the message has the "replied" tag
'S' if 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".
In case there are multiple files associated with the message
all filenames will get the same logic applied.
"""
ret = capi.lib.notmuch_message_tags_to_maildir_flags(self._ptr())
if ret != capi.lib.NOTMUCH_STATUS_SUCCESS:
raise errors.NotmuchError(ret)
class TagsIter(base.NotmuchObject, collections.abc.Iterator):
"""Iterator over tags.
This is only an iterator, not a container so calling
:meth:`__iter__` does not return a new, replenished iterator but
only itself.
:param parent: The parent object to keep alive.
:param tags_p: The CFFI pointer to the C-level tags iterator.
:param encoding: Which codec to use. The default *None* does not
decode at all and will return the unmodified bytes.
Otherwise this is passed on to :func:`str.decode`.
:param errors: If using a codec, this is the error handler.
See :func:`str.decode` to which this is passed on.
:raises ObjectDestroyedError: if used after destroyed.
"""
_tags_p = base.MemoryPointer()
def __init__(self, parent, tags_p, *, encoding=None, errors='strict'):
self._parent = parent
self._tags_p = tags_p
self._encoding = encoding
self._errors = errors
def __del__(self):
self._destroy()
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._tags_p
except errors.ObjectDestroyedError:
return False
else:
return True
def _destroy(self):
if self.alive:
try:
capi.lib.notmuch_tags_destroy(self._tags_p)
except errors.ObjectDestroyedError:
pass
self._tags_p = None
def __iter__(self):
"""Return the iterator itself.
Note that as this is an iterator and not a container this will
not return a new iterator. Thus any elements already consumed
will not be yielded by the :meth:`__next__` method anymore.
"""
return self
def __next__(self):
if not capi.lib.notmuch_tags_valid(self._tags_p):
self._destroy()
raise StopIteration()
tag_p = capi.lib.notmuch_tags_get(self._tags_p)
tag = capi.ffi.string(tag_p)
if self._encoding:
tag = tag.decode(encoding=self._encoding, errors=self._errors)
capi.lib.notmuch_tags_move_to_next(self._tags_p)
return tag
def __repr__(self):
try:
self._tags_p
except errors.ObjectDestroyedError:
return '<TagsIter (exhausted)>'
else:
return '<TagsIter>'

View file

@ -0,0 +1,194 @@
import collections.abc
import weakref
from notmuch2 import _base as base
from notmuch2 import _capi as capi
from notmuch2 import _errors as errors
from notmuch2 import _message as message
from notmuch2 import _tags as tags
__all__ = ['Thread']
class Thread(base.NotmuchObject, collections.abc.Iterable):
_thread_p = base.MemoryPointer()
def __init__(self, parent, thread_p, *, db):
self._parent = parent
self._thread_p = thread_p
self._db = db
@property
def alive(self):
if not self._parent.alive:
return False
try:
self._thread_p
except errors.ObjectDestroyedError:
return False
else:
return True
def __del__(self):
self._destroy()
def _destroy(self):
if self.alive:
capi.lib.notmuch_thread_destroy(self._thread_p)
self._thread_p = None
@property
def threadid(self):
"""The thread ID as a :class:`BinString`.
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_thread_get_thread_id(self._thread_p)
return base.BinString.from_cffi(ret)
def __len__(self):
"""Return the number of messages in the thread.
:raises ObjectDestroyedError: if used after destroyed.
"""
return capi.lib.notmuch_thread_get_total_messages(self._thread_p)
def toplevel(self):
"""Return an iterator of the toplevel messages.
:returns: An iterator yielding :class:`Message` instances.
:raises ObjectDestroyedError: if used after destroyed.
"""
msgs_p = capi.lib.notmuch_thread_get_toplevel_messages(self._thread_p)
return message.MessageIter(self, msgs_p,
db=self._db,
msg_cls=message.OwnedMessage)
def __iter__(self):
"""Return an iterator over all the messages in the thread.
:returns: An iterator yielding :class:`Message` instances.
:raises ObjectDestroyedError: if used after destroyed.
"""
msgs_p = capi.lib.notmuch_thread_get_messages(self._thread_p)
return message.MessageIter(self, msgs_p,
db=self._db,
msg_cls=message.OwnedMessage)
@property
def matched(self):
"""The number of messages in this thread which matched the query.
Of the messages in the thread this gives the count of messages
which did directly match the search query which this thread
originates from.
:raises ObjectDestroyedError: if used after destroyed.
"""
return capi.lib.notmuch_thread_get_matched_messages(self._thread_p)
@property
def authors(self):
"""A comma-separated string of all authors in the thread.
Authors of messages which matched the query the thread was
retrieved from will be at the head of the string, ordered by
date of their messages. Following this will be the authors of
the other messages in the thread, also ordered by date of
their messages. Both groups of authors are separated by the
``|`` character.
:returns: The stringified list of authors.
:rtype: BinString
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_thread_get_authors(self._thread_p)
return base.BinString.from_cffi(ret)
@property
def subject(self):
"""The subject of the thread, taken from the first message.
The thread's subject is taken to be the subject of the first
message according to query sort order.
:returns: The thread's subject.
:rtype: BinString
:raises ObjectDestroyedError: if used after destroyed.
"""
ret = capi.lib.notmuch_thread_get_subject(self._thread_p)
return base.BinString.from_cffi(ret)
@property
def first(self):
"""Return the date of the oldest message in the thread.
The time the first message was sent as an integer number of
seconds since the *epoch*, 1 Jan 1970.
:raises ObjectDestroyedError: if used after destroyed.
"""
return capi.lib.notmuch_thread_get_oldest_date(self._thread_p)
@property
def last(self):
"""Return the date of the newest message in the thread.
The time the last message was sent as an integer number of
seconds since the *epoch*, 1 Jan 1970.
:raises ObjectDestroyedError: if used after destroyed.
"""
return capi.lib.notmuch_thread_get_newest_date(self._thread_p)
@property
def tags(self):
"""Return an immutable set with all tags used in this thread.
This returns an immutable set-like object implementing the
collections.abc.Set Abstract Base Class. Due to the
underlying libnotmuch implementation some operations have
different performance characteristics then plain set objects.
Mainly any lookup operation is O(n) rather then O(1).
Normal usage treats tags as UTF-8 encoded unicode strings so
they are exposed to Python as normal unicode string objects.
If you need to handle tags stored in libnotmuch which are not
valid unicode do check the :class:`ImmutableTagSet` docs for
how to handle this.
:rtype: ImmutableTagSet
:raises ObjectDestroyedError: if used after destroyed.
"""
try:
ref = self._cached_tagset
except AttributeError:
tagset = None
else:
tagset = ref()
if tagset is None:
tagset = tags.ImmutableTagSet(
self, '_thread_p', capi.lib.notmuch_thread_get_tags)
self._cached_tagset = weakref.ref(tagset)
return tagset
class ThreadIter(base.NotmuchIter):
def __init__(self, parent, threads_p, *, db):
self._db = db
super().__init__(parent, threads_p,
fn_destroy=capi.lib.notmuch_threads_destroy,
fn_valid=capi.lib.notmuch_threads_valid,
fn_get=capi.lib.notmuch_threads_get,
fn_next=capi.lib.notmuch_threads_move_to_next)
def __next__(self):
thread_p = super().__next__()
return Thread(self, thread_p, db=self._db)

View file

@ -0,0 +1,25 @@
import setuptools
from _notmuch_config import *
with open(NOTMUCH_VERSION_FILE) as fp:
VERSION = fp.read().strip()
setuptools.setup(
name='notmuch2',
version=VERSION,
description='Pythonic bindings for the notmuch mail database using CFFI',
author='Floris Bruynooghe',
author_email='flub@devork.be',
setup_requires=['cffi>=1.0.0'],
install_requires=['cffi>=1.0.0'],
packages=setuptools.find_packages(exclude=['tests']),
cffi_modules=['notmuch2/_build.py:ffibuilder'],
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 3',
'Topic :: Communications :: Email',
'Topic :: Software Development :: Libraries',
],
)

View file

@ -0,0 +1,149 @@
import email.message
import mailbox
import pathlib
import shutil
import socket
import subprocess
import textwrap
import time
import os
import pytest
def pytest_report_header():
which = shutil.which('notmuch')
vers = subprocess.run(['notmuch', '--version'], stdout=subprocess.PIPE)
return ['{} ({})'.format(vers.stdout.decode(errors='replace').strip(),which)]
@pytest.fixture(scope='function')
def tmppath(tmpdir):
"""The tmpdir fixture wrapped in pathlib.Path."""
return pathlib.Path(str(tmpdir))
@pytest.fixture
def notmuch(maildir):
"""Return a function which runs notmuch commands on our test maildir.
This uses the notmuch-config file created by the ``maildir``
fixture.
"""
def run(*args):
"""Run a notmuch command.
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.
"""
cfg_fname = maildir.path / 'notmuch-config'
cmd = ['notmuch'] + list(args)
env = os.environ.copy()
env['NOTMUCH_CONFIG'] = str(cfg_fname)
proc = subprocess.run(cmd,
timeout=120,
env=env)
proc.check_returncode()
return run
@pytest.fixture
def maildir(tmppath):
"""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.
"""
cur = tmppath / 'cur'
cur.mkdir()
new = tmppath / 'new'
new.mkdir()
tmp = tmppath / 'tmp'
tmp.mkdir()
cfg_fname = tmppath/'notmuch-config'
with cfg_fname.open('w') as fp:
fp.write(textwrap.dedent("""\
[database]
path={tmppath!s}
[user]
name=Some Hacker
primary_email=dst@example.com
[new]
tags=unread;inbox;
ignore=
[search]
exclude_tags=deleted;spam;
[maildir]
synchronize_flags=true
""".format(tmppath=tmppath)))
return MailDir(tmppath)
class MailDir:
"""An interface around a correct maildir."""
def __init__(self, path):
self._path = pathlib.Path(path)
self.mailbox = mailbox.Maildir(str(path))
self._idcount = 0
@property
def path(self):
"""The pathname of the maildir."""
return self._path
def _next_msgid(self):
"""Return a new unique message ID."""
msgid = '{}@{}'.format(self._idcount, socket.getfqdn())
self._idcount += 1
return msgid
def deliver(self,
subject='Test mail',
body='This is a test mail',
to='dst@example.com',
frm='src@example.com',
headers=None,
new=False, # Move to new dir or cur dir?
keywords=None, # List of keywords or labels
seen=False, # Seen flag (cur dir only)
replied=False, # Replied flag (cur dir only)
flagged=False): # Flagged flag (cur dir only)
"""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).
"""
msgid = self._next_msgid()
when = time.time()
msg = email.message.EmailMessage()
msg.add_header('Received', 'by MailDir; {}'.format(time.ctime(when)))
msg.add_header('Message-ID', '<{}>'.format(msgid))
msg.add_header('Date', time.ctime(when))
msg.add_header('From', frm)
msg.add_header('To', to)
msg.add_header('Subject', subject)
if headers:
for h, v in headers:
msg.add_header(h, v)
msg.set_content(body)
mdmsg = mailbox.MaildirMessage(msg)
if not new:
mdmsg.set_subdir('cur')
if flagged:
mdmsg.add_flag('F')
if replied:
mdmsg.add_flag('R')
if seen:
mdmsg.add_flag('S')
boxid = self.mailbox.add(mdmsg)
basename = boxid
if mdmsg.get_info():
basename += mailbox.Maildir.colon + mdmsg.get_info()
msgpath = self.path / mdmsg.get_subdir() / basename
return (msgid, msgpath)

View file

@ -0,0 +1,116 @@
import pytest
from notmuch2 import _base as base
from notmuch2 import _errors as errors
class TestNotmuchObject:
def test_no_impl_methods(self):
class Object(base.NotmuchObject):
pass
with pytest.raises(TypeError):
Object()
def test_impl_methods(self):
class Object(base.NotmuchObject):
def __init__(self):
pass
@property
def alive(self):
pass
def _destroy(self, parent=False):
pass
Object()
def test_del(self):
destroyed = False
class Object(base.NotmuchObject):
def __init__(self):
pass
@property
def alive(self):
pass
def _destroy(self, parent=False):
nonlocal destroyed
destroyed = True
o = Object()
o.__del__()
assert destroyed
class TestMemoryPointer:
@pytest.fixture
def obj(self):
class Cls:
ptr = base.MemoryPointer()
return Cls()
def test_unset(self, obj):
with pytest.raises(errors.ObjectDestroyedError):
obj.ptr
def test_set(self, obj):
obj.ptr = 'some'
assert obj.ptr == 'some'
def test_cleared(self, obj):
obj.ptr = 'some'
obj.ptr
obj.ptr = None
with pytest.raises(errors.ObjectDestroyedError):
obj.ptr
def test_two_instances(self, obj):
obj2 = obj.__class__()
obj.ptr = 'foo'
obj2.ptr = 'bar'
assert obj.ptr != obj2.ptr
class TestBinString:
def test_type(self):
s = base.BinString(b'foo')
assert isinstance(s, str)
def test_init_bytes(self):
s = base.BinString(b'foo')
assert s == 'foo'
def test_init_str(self):
s = base.BinString('foo')
assert s == 'foo'
def test_bytes(self):
s = base.BinString(b'foo')
assert bytes(s) == b'foo'
def test_invalid_utf8(self):
s = base.BinString(b'\x80foo')
assert s == 'foo'
assert bytes(s) == b'\x80foo'
def test_errors(self):
s = base.BinString(b'\x80foo', errors='replace')
assert s == '<EFBFBD>foo'
assert bytes(s) == b'\x80foo'
def test_encoding(self):
# pound sign: '£' == '\u00a3' latin-1: b'\xa3', utf-8: b'\xc2\xa3'
with pytest.raises(UnicodeDecodeError):
base.BinString(b'\xa3', errors='strict')
s = base.BinString(b'\xa3', encoding='latin-1', errors='strict')
assert s == '£'
assert bytes(s) == b'\xa3'

View file

@ -0,0 +1,60 @@
import collections.abc
import pytest
import notmuch2._database as dbmod
import notmuch2._config as config
class TestIter:
@pytest.fixture
def db(self, maildir):
with dbmod.Database.create(maildir.path) as db:
yield db
def test_type(self, db):
assert isinstance(db.config, collections.abc.MutableMapping)
assert isinstance(db.config, config.ConfigMapping)
def test_alive(self, db):
assert db.config.alive
def test_set_get(self, maildir):
# Ensure get-set works from different db objects
with dbmod.Database.create(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db0:
db0.config['spam'] = 'ham'
with dbmod.Database(maildir.path, config=dbmod.Database.CONFIG.EMPTY) as db1:
assert db1.config['spam'] == 'ham'
def test_get_keyerror(self, db):
with pytest.raises(KeyError):
val = db.config['not-a-key']
print(repr(val))
def test_iter(self, db):
def has_prefix(x):
return x.startswith('TEST.')
assert [ x for x in db.config if has_prefix(x) ] == []
db.config['TEST.spam'] = 'TEST.ham'
db.config['TEST.eggs'] = 'TEST.bacon'
assert { x for x in db.config if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'}
assert { x for x in db.config.keys() if has_prefix(x) } == {'TEST.spam', 'TEST.eggs'}
assert { x for x in db.config.values() if has_prefix(x) } == {'TEST.ham', 'TEST.bacon'}
assert { (x, y) for (x,y) in db.config.items() if has_prefix(x) } == \
{('TEST.spam', 'TEST.ham'), ('TEST.eggs', 'TEST.bacon')}
def test_len(self, db):
defaults = len(db.config)
db.config['spam'] = 'ham'
assert len(db.config) == defaults + 1
db.config['eggs'] = 'bacon'
assert len(db.config) == defaults + 2
def test_del(self, db):
db.config['spam'] = 'ham'
assert db.config.get('spam') == 'ham'
del db.config['spam']
assert db.config.get('spam') is None

View file

@ -0,0 +1,342 @@
import collections
import configparser
import os
import pathlib
import pytest
import notmuch2
import notmuch2._errors as errors
import notmuch2._database as dbmod
import notmuch2._message as message
@pytest.fixture
def db(maildir):
with dbmod.Database.create(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db:
yield db
class TestDefaultDb:
"""Tests for reading the default database.
The error cases are fairly undefined, some relevant Python error
will come out if you give it a bad filename or if the file does
not parse correctly. So we're not testing this too deeply.
"""
def test_config_pathname_default(self, monkeypatch):
monkeypatch.delenv('NOTMUCH_CONFIG', raising=False)
user = pathlib.Path('~/.notmuch-config').expanduser()
assert dbmod._config_pathname() == user
def test_config_pathname_env(self, monkeypatch):
monkeypatch.setenv('NOTMUCH_CONFIG', '/some/random/path')
assert dbmod._config_pathname() == pathlib.Path('/some/random/path')
def test_default_path_nocfg(self, monkeypatch, tmppath):
monkeypatch.setenv('NOTMUCH_CONFIG', str(tmppath/'foo'))
with pytest.raises(FileNotFoundError):
dbmod.Database.default_path()
def test_default_path_cfg_is_dir(self, monkeypatch, tmppath):
monkeypatch.setenv('NOTMUCH_CONFIG', str(tmppath))
with pytest.raises(IsADirectoryError):
dbmod.Database.default_path()
def test_default_path_parseerr(self, monkeypatch, tmppath):
cfg = tmppath / 'notmuch-config'
with cfg.open('w') as fp:
fp.write('invalid')
monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg))
with pytest.raises(configparser.Error):
dbmod.Database.default_path()
def test_default_path_parse(self, monkeypatch, tmppath):
cfg = tmppath / 'notmuch-config'
with cfg.open('w') as fp:
fp.write('[database]\n')
fp.write('path={!s}'.format(tmppath))
monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg))
assert dbmod.Database.default_path() == tmppath
def test_default_path_param(self, monkeypatch, tmppath):
cfg_dummy = tmppath / 'dummy'
monkeypatch.setenv('NOTMUCH_CONFIG', str(cfg_dummy))
cfg_real = tmppath / 'notmuch_config'
with cfg_real.open('w') as fp:
fp.write('[database]\n')
fp.write('path={!s}'.format(cfg_real/'mail'))
assert dbmod.Database.default_path(cfg_real) == cfg_real/'mail'
class TestCreate:
def test_create(self, tmppath, db):
assert tmppath.joinpath('.notmuch/xapian/').exists()
def test_create_already_open(self, tmppath, db):
with pytest.raises(errors.NotmuchError):
db.create(tmppath)
def test_create_existing(self, tmppath, db):
with pytest.raises(errors.DatabaseExistsError):
dbmod.Database.create(path=tmppath)
def test_close(self, db):
db.close()
def test_del_noclose(self, db):
del db
def test_close_del(self, db):
db.close()
del db
def test_closed_attr(self, db):
assert not db.closed
db.close()
assert db.closed
def test_ctx(self, db):
with db as ctx:
assert ctx is db
assert not db.closed
assert db.closed
def test_path(self, db, tmppath):
assert db.path == tmppath
def test_version(self, db):
assert db.version > 0
def test_needs_upgrade(self, db):
assert db.needs_upgrade in (True, False)
class TestAtomic:
def test_exit_early(self, db):
with pytest.raises(errors.UnbalancedAtomicError):
with db.atomic() as ctx:
ctx.force_end()
def test_exit_late(self, db):
with db.atomic() as ctx:
pass
with pytest.raises(errors.UnbalancedAtomicError):
ctx.force_end()
def test_abort(self, db):
with db.atomic() as txn:
txn.abort()
assert db.closed
class TestRevision:
def test_single_rev(self, db):
r = db.revision()
assert isinstance(r, dbmod.DbRevision)
assert isinstance(r.rev, int)
assert isinstance(r.uuid, bytes)
assert r is r
assert r == r
assert r <= r
assert r >= r
assert not r < r
assert not r > r
def test_diff_db(self, tmppath):
dbpath0 = tmppath.joinpath('db0')
dbpath0.mkdir()
dbpath1 = tmppath.joinpath('db1')
dbpath1.mkdir()
db0 = dbmod.Database.create(path=dbpath0)
db1 = dbmod.Database.create(path=dbpath1)
r_db0 = db0.revision()
r_db1 = db1.revision()
assert r_db0 != r_db1
assert r_db0.uuid != r_db1.uuid
def test_cmp(self, db, maildir):
rev0 = db.revision()
_, pathname = maildir.deliver()
db.add(pathname, sync_flags=False)
rev1 = db.revision()
assert rev0 < rev1
assert rev0 <= rev1
assert not rev0 > rev1
assert not rev0 >= rev1
assert not rev0 == rev1
assert rev0 != rev1
# XXX add tests for revisions comparisons
class TestMessages:
def test_add_message(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(pathname, sync_flags=False)
assert isinstance(msg, message.Message)
assert msg.path == pathname
assert msg.messageid == msgid
def test_add_message_str(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(str(pathname), sync_flags=False)
def test_add_message_bytes(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(os.fsencode(bytes(pathname)), sync_flags=False)
def test_remove_message(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(pathname, sync_flags=False)
assert db.find(msgid)
dup = db.remove(pathname)
with pytest.raises(LookupError):
db.find(msgid)
def test_remove_message_str(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(pathname, sync_flags=False)
assert db.find(msgid)
dup = db.remove(str(pathname))
with pytest.raises(LookupError):
db.find(msgid)
def test_remove_message_bytes(self, db, maildir):
msgid, pathname = maildir.deliver()
msg, dup = db.add(pathname, sync_flags=False)
assert db.find(msgid)
dup = db.remove(os.fsencode(bytes(pathname)))
with pytest.raises(LookupError):
db.find(msgid)
def test_find_message(self, db, maildir):
msgid, pathname = maildir.deliver()
msg0, dup = db.add(pathname, sync_flags=False)
msg1 = db.find(msgid)
assert isinstance(msg1, message.Message)
assert msg1.messageid == msgid == msg0.messageid
assert msg1.path == pathname == msg0.path
def test_find_message_notfound(self, db):
with pytest.raises(LookupError):
db.find('foo')
def test_get_message(self, db, maildir):
msgid, pathname = maildir.deliver()
msg0, _ = db.add(pathname, sync_flags=False)
msg1 = db.get(pathname)
assert isinstance(msg1, message.Message)
assert msg1.messageid == msgid == msg0.messageid
assert msg1.path == pathname == msg0.path
def test_get_message_str(self, db, maildir):
msgid, pathname = maildir.deliver()
db.add(pathname, sync_flags=False)
msg = db.get(str(pathname))
assert msg.messageid == msgid
def test_get_message_bytes(self, db, maildir):
msgid, pathname = maildir.deliver()
db.add(pathname, sync_flags=False)
msg = db.get(os.fsencode(bytes(pathname)))
assert msg.messageid == msgid
class TestTags:
# We just want to test this behaves like a set at a hight level.
# The set semantics are tested in detail in the test_tags module.
def test_type(self, db):
assert isinstance(db.tags, collections.abc.Set)
def test_none(self, db):
itags = iter(db.tags)
with pytest.raises(StopIteration):
next(itags)
assert len(db.tags) == 0
assert not db.tags
def test_some(self, db, maildir):
_, pathname = maildir.deliver()
msg, _ = db.add(pathname, sync_flags=False)
msg.tags.add('hello')
itags = iter(db.tags)
assert next(itags) == 'hello'
with pytest.raises(StopIteration):
next(itags)
assert 'hello' in msg.tags
def test_cache(self, db):
assert db.tags is db.tags
def test_iters(self, db):
i1 = iter(db.tags)
i2 = iter(db.tags)
assert i1 is not i2
class TestQuery:
@pytest.fixture
def db(self, maildir, notmuch):
"""Return a read-only notmuch2.Database.
The database will have 3 messages, 2 threads.
"""
msgid, _ = maildir.deliver(body='foo')
maildir.deliver(body='bar')
maildir.deliver(body='baz',
headers=[('In-Reply-To', '<{}>'.format(msgid))])
notmuch('new')
with dbmod.Database(maildir.path, 'rw', config=notmuch2.Database.CONFIG.EMPTY) as db:
yield db
def test_count_messages(self, db):
assert db.count_messages('*') == 3
def test_messages_type(self, db):
msgs = db.messages('*')
assert isinstance(msgs, collections.abc.Iterator)
def test_message_no_results(self, db):
msgs = db.messages('not_a_matching_query')
with pytest.raises(StopIteration):
next(msgs)
def test_message_match(self, db):
msgs = db.messages('*')
msg = next(msgs)
assert isinstance(msg, notmuch2.Message)
def test_count_threads(self, db):
assert db.count_threads('*') == 2
def test_threads_type(self, db):
threads = db.threads('*')
assert isinstance(threads, collections.abc.Iterator)
def test_threads_no_match(self, db):
threads = db.threads('not_a_matching_query')
with pytest.raises(StopIteration):
next(threads)
def test_threads_match(self, db):
threads = db.threads('*')
thread = next(threads)
assert isinstance(thread, notmuch2.Thread)
def test_use_threaded_message_twice(self, db):
thread = next(db.threads('*'))
for msg in thread.toplevel():
assert isinstance(msg, notmuch2.Message)
assert msg.alive
del msg
for msg in thread:
assert isinstance(msg, notmuch2.Message)
assert msg.alive
del msg

View file

@ -0,0 +1,8 @@
from notmuch2 import _capi as capi
from notmuch2 import _errors as errors
def test_status_no_message():
exc = errors.NotmuchError(capi.lib.NOTMUCH_STATUS_PATH_ERROR)
assert exc.status == capi.lib.NOTMUCH_STATUS_PATH_ERROR
assert exc.message is None
assert str(exc) == 'Path supplied is illegal for this function'

View file

@ -0,0 +1,229 @@
import collections.abc
import time
import pathlib
import pytest
import notmuch2
class TestMessage:
MaildirMsg = collections.namedtuple('MaildirMsg', ['msgid', 'path'])
@pytest.fixture
def maildir_msg(self, maildir):
msgid, path = maildir.deliver()
return self.MaildirMsg(msgid, path)
@pytest.fixture
def db(self, maildir):
with notmuch2.Database.create(maildir.path) as db:
yield db
@pytest.fixture
def msg(self, db, maildir_msg):
msg, dup = db.add(maildir_msg.path, sync_flags=False)
yield msg
def test_type(self, msg):
assert isinstance(msg, notmuch2.NotmuchObject)
assert isinstance(msg, notmuch2.Message)
def test_alive(self, msg):
assert msg.alive
def test_hash(self, msg):
assert hash(msg)
def test_eq(self, db, msg):
copy = db.get(msg.path)
assert msg == copy
def test_messageid_type(self, msg):
assert isinstance(msg.messageid, str)
assert isinstance(msg.messageid, notmuch2.BinString)
assert isinstance(bytes(msg.messageid), bytes)
def test_messageid(self, msg, maildir_msg):
assert msg.messageid == maildir_msg.msgid
def test_messageid_find(self, db, msg):
copy = db.find(msg.messageid)
assert msg.messageid == copy.messageid
def test_threadid_type(self, msg):
assert isinstance(msg.threadid, str)
assert isinstance(msg.threadid, notmuch2.BinString)
assert isinstance(bytes(msg.threadid), bytes)
def test_path_type(self, msg):
assert isinstance(msg.path, pathlib.Path)
def test_path(self, msg, maildir_msg):
assert msg.path == maildir_msg.path
def test_pathb_type(self, msg):
assert isinstance(msg.pathb, bytes)
def test_pathb(self, msg, maildir_msg):
assert msg.path == maildir_msg.path
def test_filenames_type(self, msg):
ifn = msg.filenames()
assert isinstance(ifn, collections.abc.Iterator)
def test_filenames(self, msg):
ifn = msg.filenames()
fn = next(ifn)
assert fn == msg.path
assert isinstance(fn, pathlib.Path)
with pytest.raises(StopIteration):
next(ifn)
assert list(msg.filenames()) == [msg.path]
def test_filenamesb_type(self, msg):
ifn = msg.filenamesb()
assert isinstance(ifn, collections.abc.Iterator)
def test_filenamesb(self, msg):
ifn = msg.filenamesb()
fn = next(ifn)
assert fn == msg.pathb
assert isinstance(fn, bytes)
with pytest.raises(StopIteration):
next(ifn)
assert list(msg.filenamesb()) == [msg.pathb]
def test_ghost_no(self, msg):
assert not msg.ghost
def test_matched_no(self,msg):
assert not msg.matched
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
def test_header(self, msg):
assert msg.header('from') == 'src@example.com'
def test_header_not_present(self, msg):
with pytest.raises(LookupError):
msg.header('foo')
def test_freeze(self, msg):
with msg.frozen():
msg.tags.add('foo')
msg.tags.add('bar')
msg.tags.discard('foo')
assert 'foo' not in msg.tags
assert 'bar' in msg.tags
def test_freeze_err(self, msg):
msg.tags.add('foo')
try:
with msg.frozen():
msg.tags.clear()
raise Exception('oops')
except Exception:
assert 'foo' in msg.tags
else:
pytest.fail('Context manager did not raise')
def test_replies_type(self, msg):
assert isinstance(msg.replies(), collections.abc.Iterator)
def test_replies(self, msg):
with pytest.raises(StopIteration):
next(msg.replies())
class TestProperties:
@pytest.fixture
def props(self, maildir):
msgid, path = maildir.deliver()
with notmuch2.Database.create(maildir.path) as db:
msg, dup = db.add(path, sync_flags=False)
yield msg.properties
def test_type(self, props):
assert isinstance(props, collections.abc.MutableMapping)
def test_add_single(self, props):
props['foo'] = 'bar'
assert props['foo'] == 'bar'
props.add('bar', 'baz')
assert props['bar'] == 'baz'
def test_add_dup(self, props):
props.add('foo', 'bar')
props.add('foo', 'baz')
assert props['foo'] == 'bar'
assert (set(props.getall('foo', exact=True))
== {('foo', 'bar'), ('foo', 'baz')})
def test_len(self, props):
props.add('foo', 'a')
props.add('foo', 'b')
props.add('bar', 'a')
assert len(props) == 3
assert len(props.keys()) == 2
assert len(props.values()) == 2
assert len(props.items()) == 3
def test_del(self, props):
props.add('foo', 'a')
props.add('foo', 'b')
del props['foo']
with pytest.raises(KeyError):
props['foo']
def test_remove(self, props):
props.add('foo', 'a')
props.add('foo', 'b')
props.remove('foo', 'a')
assert props['foo'] == 'b'
def test_view_abcs(self, props):
assert isinstance(props.keys(), collections.abc.KeysView)
assert isinstance(props.values(), collections.abc.ValuesView)
assert isinstance(props.items(), collections.abc.ItemsView)
def test_pop(self, props):
props.add('foo', 'a')
props.add('foo', 'b')
val = props.pop('foo')
assert val == 'a'
def test_pop_default(self, props):
with pytest.raises(KeyError):
props.pop('foo')
assert props.pop('foo', 'default') == 'default'
def test_popitem(self, props):
props.add('foo', 'a')
assert props.popitem() == ('foo', 'a')
with pytest.raises(KeyError):
props.popitem()
def test_clear(self, props):
props.add('foo', 'a')
props.clear()
assert len(props) == 0
def test_getall(self, props):
props.add('foo', 'a')
assert set(props.getall('foo')) == {('foo', 'a')}
def test_getall_prefix(self, props):
props.add('foo', 'a')
props.add('foobar', 'b')
assert set(props.getall('foo')) == {('foo', 'a'), ('foobar', 'b')}
def test_getall_exact(self, props):
props.add('foo', 'a')
props.add('foobar', 'b')
assert set(props.getall('foo', exact=True)) == {('foo', 'a')}

View file

@ -0,0 +1,242 @@
"""Tests for the behaviour of immutable and mutable tagsets.
This module tests the Pythonic behaviour of the sets.
"""
import collections
import subprocess
import textwrap
import pytest
from notmuch2 import _database as database
from notmuch2 import _tags as tags
class TestImmutable:
@pytest.fixture
def tagset(self, maildir, notmuch):
"""An non-empty immutable tagset.
This will have the default new mail tags: inbox, unread.
"""
maildir.deliver()
notmuch('new')
with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
yield db.tags
def test_type(self, tagset):
assert isinstance(tagset, tags.ImmutableTagSet)
assert isinstance(tagset, collections.abc.Set)
def test_hash(self, tagset, maildir, notmuch):
h0 = hash(tagset)
notmuch('tag', '+foo', '*')
with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
h1 = hash(db.tags)
assert h0 != h1
def test_eq(self, tagset):
assert tagset == tagset
def test_neq(self, tagset, maildir, notmuch):
notmuch('tag', '+foo', '*')
with database.Database(maildir.path, config=database.Database.CONFIG.EMPTY) as db:
assert tagset != db.tags
def test_contains(self, tagset):
print(tuple(tagset))
assert 'unread' in tagset
assert 'foo' not in tagset
def test_isdisjoint(self, tagset):
assert tagset.isdisjoint(set(['spam', 'ham']))
assert not tagset.isdisjoint(set(['inbox']))
def test_issubset(self, tagset):
assert {'inbox'} <= tagset
assert {'inbox'}.issubset(tagset)
assert tagset <= {'inbox', 'unread', 'spam'}
assert tagset.issubset({'inbox', 'unread', 'spam'})
def test_issuperset(self, tagset):
assert {'inbox', 'unread', 'spam'} >= tagset
assert {'inbox', 'unread', 'spam'}.issuperset(tagset)
assert tagset >= {'inbox'}
assert tagset.issuperset({'inbox'})
def test_iter(self, tagset):
expected = sorted(['unread', 'inbox'])
found = []
for tag in tagset:
assert isinstance(tag, str)
found.append(tag)
assert expected == sorted(found)
def test_special_iter(self, tagset):
expected = sorted([b'unread', b'inbox'])
found = []
for tag in tagset.iter():
assert isinstance(tag, bytes)
found.append(tag)
assert expected == sorted(found)
def test_special_iter_codec(self, tagset):
for tag in tagset.iter(encoding='ascii', errors='surrogateescape'):
assert isinstance(tag, str)
def test_len(self, tagset):
assert len(tagset) == 2
def test_and(self, tagset):
common = tagset & {'unread'}
assert isinstance(common, set)
assert isinstance(common, collections.abc.Set)
assert common == {'unread'}
common = tagset.intersection({'unread'})
assert isinstance(common, set)
assert isinstance(common, collections.abc.Set)
assert common == {'unread'}
def test_or(self, tagset):
res = tagset | {'foo'}
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'unread', 'inbox', 'foo'}
res = tagset.union({'foo'})
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'unread', 'inbox', 'foo'}
def test_sub(self, tagset):
res = tagset - {'unread'}
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox'}
res = tagset.difference({'unread'})
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox'}
def test_rsub(self, tagset):
res = {'foo', 'unread'} - tagset
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'foo'}
def test_xor(self, tagset):
res = tagset ^ {'unread', 'foo'}
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox', 'foo'}
res = tagset.symmetric_difference({'unread', 'foo'})
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox', 'foo'}
def test_rxor(self, tagset):
res = {'unread', 'foo'} ^ tagset
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox', 'foo'}
def test_copy(self, tagset):
res = tagset.copy()
assert isinstance(res, set)
assert isinstance(res, collections.abc.Set)
assert res == {'inbox', 'unread'}
class TestMutableTagset:
@pytest.fixture
def tagset(self, maildir, notmuch):
"""An non-empty mutable tagset.
This will have the default new mail tags: inbox, unread.
"""
_, pathname = maildir.deliver()
notmuch('new')
with database.Database(maildir.path,
mode=database.Mode.READ_WRITE,
config=database.Database.CONFIG.EMPTY) as db:
msg = db.get(pathname)
yield msg.tags
def test_type(self, tagset):
assert isinstance(tagset, collections.abc.MutableSet)
assert isinstance(tagset, tags.MutableTagSet)
def test_hash(self, tagset):
assert not isinstance(tagset, collections.abc.Hashable)
with pytest.raises(TypeError):
hash(tagset)
def test_add(self, tagset):
assert 'foo' not in tagset
tagset.add('foo')
assert 'foo' in tagset
def test_discard(self, tagset):
assert 'inbox' in tagset
tagset.discard('inbox')
assert 'inbox' not in tagset
def test_discard_not_present(self, tagset):
assert 'foo' not in tagset
tagset.discard('foo')
def test_clear(self, tagset):
assert len(tagset) > 0
tagset.clear()
assert len(tagset) == 0
def test_from_maildir_flags(self, maildir, notmuch):
_, pathname = maildir.deliver(flagged=True)
notmuch('new')
with database.Database(maildir.path,
mode=database.Mode.READ_WRITE,
config=database.Database.CONFIG.EMPTY) as db:
msg = db.get(pathname)
msg.tags.discard('flagged')
msg.tags.from_maildir_flags()
assert 'flagged' in msg.tags
def test_to_maildir_flags(self, maildir, notmuch):
_, pathname = maildir.deliver(flagged=True)
notmuch('new')
with database.Database(maildir.path,
mode=database.Mode.READ_WRITE,
config=database.Database.CONFIG.EMPTY) as db:
msg = db.get(pathname)
flags = msg.path.name.split(',')[-1]
assert 'F' in flags
msg.tags.discard('flagged')
msg.tags.to_maildir_flags()
flags = msg.path.name.split(',')[-1]
assert 'F' not in flags
def test_isdisjoint(self, tagset):
assert tagset.isdisjoint(set(['spam', 'ham']))
assert not tagset.isdisjoint(set(['inbox']))
def test_issubset(self, tagset):
assert {'inbox'} <= tagset
assert {'inbox'}.issubset(tagset)
assert not {'spam'} <= tagset
assert not {'spam'}.issubset(tagset)
assert tagset <= {'inbox', 'unread', 'spam'}
assert tagset.issubset({'inbox', 'unread', 'spam'})
assert not {'inbox', 'unread', 'spam'} <= tagset
assert not {'inbox', 'unread', 'spam'}.issubset(tagset)
def test_issuperset(self, tagset):
assert {'inbox', 'unread', 'spam'} >= tagset
assert {'inbox', 'unread', 'spam'}.issuperset(tagset)
assert tagset >= {'inbox'}
assert tagset.issuperset({'inbox'})
def test_union(self, tagset):
assert {'spam'}.union(tagset) == {'inbox', 'unread', 'spam'}
assert tagset.union({'spam'}) == {'inbox', 'unread', 'spam'}

View file

@ -0,0 +1,109 @@
import collections.abc
import time
import pytest
import notmuch2
@pytest.fixture
def thread(maildir, notmuch):
"""Return a single thread with one matched message."""
msgid, _ = maildir.deliver(body='foo')
maildir.deliver(body='bar',
headers=[('In-Reply-To', '<{}>'.format(msgid))])
notmuch('new')
with notmuch2.Database(maildir.path, config=notmuch2.Database.CONFIG.EMPTY) as db:
yield next(db.threads('foo'))
def test_type(thread):
assert isinstance(thread, notmuch2.Thread)
assert isinstance(thread, collections.abc.Iterable)
def test_threadid(thread):
assert isinstance(thread.threadid, notmuch2.BinString)
assert thread.threadid
def test_len(thread):
assert len(thread) == 2
def test_toplevel_type(thread):
assert isinstance(thread.toplevel(), collections.abc.Iterator)
def test_toplevel(thread):
msgs = thread.toplevel()
assert isinstance(next(msgs), notmuch2.Message)
with pytest.raises(StopIteration):
next(msgs)
def test_toplevel_reply(thread):
msg = next(thread.toplevel())
assert isinstance(next(msg.replies()), notmuch2.Message)
def test_iter(thread):
msgs = list(iter(thread))
assert len(msgs) == len(thread)
for msg in msgs:
assert isinstance(msg, notmuch2.Message)
def test_matched(thread):
assert thread.matched == 1
def test_matched_iter(thread):
count = 0
msgs = list(iter(thread))
for msg in msgs:
if msg.matched:
count += 1
assert count == thread.matched
def test_authors_type(thread):
assert isinstance(thread.authors, notmuch2.BinString)
def test_authors(thread):
assert thread.authors == 'src@example.com'
def test_subject(thread):
assert thread.subject == 'Test mail'
def test_first(thread):
# XXX Someone seems to treat things as local time instead of
# UTC or the other way around.
now = int(time.time())
assert abs(now - thread.first) < 3600*24
def test_last(thread):
# XXX Someone seems to treat things as local time instead of
# UTC or the other way around.
now = int(time.time())
assert abs(now - thread.last) < 3600*24
def test_first_last(thread):
# Sadly we only have second resolution so these will always be the
# same time in our tests.
assert thread.first <= thread.last
def test_tags_type(thread):
assert isinstance(thread.tags, notmuch2.ImmutableTagSet)
def test_tags_cache(thread):
assert thread.tags is thread.tags
def test_tags(thread):
assert 'inbox' in thread.tags

View file

@ -0,0 +1,19 @@
[pytest]
minversion = 3.0
addopts = -ra --cov=notmuch2 --cov=tests
[tox]
envlist = py35,py36,py37,py38,pypy35,pypy36
[testenv]
deps =
cffi
pytest
pytest-cov
commands = pytest --cov={envsitepackagesdir}/notmuch2 {posargs}
[testenv:pypy35]
basepython = pypy3.5
[testenv:pypy36]
basepython = pypy3.6

4
bindings/python/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
*.py[co]
/docs/build
/docs/html
/build/

View file

@ -0,0 +1,2 @@
include notmuch
#recursive-include docs/html *

17
bindings/python/README Normal file
View file

@ -0,0 +1,17 @@
notmuch -- The python interface to notmuch
==========================================
This module makes the functionality of the notmuch library
(`https://notmuchmail.org`_) available to python. Successful import of
this module depends on a libnotmuch.so|dll being available on the
user's system.
If you have downloaded the full source tarball, you can create the
documentation with sphinx installed, go to the docs directory and
"make html". A static version of the documentation is available at:
https://notmuch.readthedocs.io/projects/notmuch-python/
To build the python bindings, do
python setup.py install --prefix=path/to/your/preferred/location

View file

@ -0,0 +1,674 @@
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/philosophy/why-not-lgpl.html>.

View file

@ -0,0 +1,88 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf build/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) html
@echo
@echo "Build finished. The HTML pages are in html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) build/dirhtml
@echo
@echo "Build finished. The HTML pages are in build/dirhtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) build/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) build/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) build/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in build/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) build/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in build/qthelp, like this:"
@echo "# qcollectiongenerator build/qthelp/pyDNS.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile build/qthelp/pyDNS.qhc"
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex
@echo
@echo "Build finished; the LaTeX files are in build/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) build/changes
@echo
@echo "The overview file is in build/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) build/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in build/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) build/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in build/doctest/output.txt."

View file

@ -0,0 +1,209 @@
# -*- coding: utf-8 -*-
#
# pyDNS documentation build configuration file, created by
# sphinx-quickstart on Tue Feb 2 10:00:47 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys, os
from unittest.mock import Mock
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0,os.path.abspath('../..'))
MOCK_MODULES = [
'ctypes',
]
for mod_name in MOCK_MODULES:
sys.modules[mod_name] = Mock()
from notmuch import __VERSION__,__AUTHOR__
# -- General configuration -----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo']
autoclass_content = "both"
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'notmuch'
copyright = u'2010-2012, ' + __AUTHOR__
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = __VERSION__
# The full version, including alpha/beta/rc tags.
release = __VERSION__
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
exclude_trees = []
# The reST default role (used for this markup: `text`) to use for all documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
add_module_names = False
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output ---------------------------------------------------
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
html_use_modindex = False
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'notmuchdoc'
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
('index', 'notmuch.tex', u'notmuch Documentation',
u'notmuch contributors', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'https://docs.python.org/': None}

View file

@ -0,0 +1,54 @@
:class:`Database` -- The underlying notmuch database
====================================================
.. currentmodule:: notmuch
.. autoclass:: Database([path=None[, create=False[, mode=MODE.READ_ONLY]]])
.. automethod:: create
.. automethod:: open(path, status=MODE.READ_ONLY)
.. automethod:: close
.. automethod:: get_path
.. automethod:: get_version
.. automethod:: needs_upgrade
.. automethod:: upgrade
.. automethod:: begin_atomic
.. automethod:: end_atomic
.. automethod:: get_directory
.. automethod:: index_file
.. automethod:: remove_message
.. automethod:: find_message
.. automethod:: find_message_by_filename
.. automethod:: get_all_tags
.. automethod:: create_query
.. automethod:: get_config
.. automethod:: get_configs
.. automethod:: set_config
.. attribute:: Database.MODE
Defines constants that are used as the mode in which to open a database.
MODE.READ_ONLY
Open the database in read-only mode
MODE.READ_WRITE
Open the database in read-write mode

View file

@ -0,0 +1,32 @@
Files and directories
=====================
.. currentmodule:: notmuch
:class:`Filenames` -- An iterator over filenames
------------------------------------------------
.. autoclass:: Filenames
.. method:: Filenames.__len__
.. warning::
:meth:`__len__` was removed in version 0.22 as it exhausted the
iterator and broke list(Filenames()). Use `len(list(names))`
instead.
:class:`Directory` -- A directory entry in the database
-------------------------------------------------------
.. autoclass:: Directory
.. automethod:: Directory.get_child_files
.. automethod:: Directory.get_child_directories
.. automethod:: Directory.get_mtime
.. automethod:: Directory.set_mtime
.. autoattribute:: Directory.mtime
.. autoattribute:: Directory.path

View file

@ -0,0 +1,36 @@
Welcome to :mod:`notmuch`'s documentation
=========================================
.. currentmodule:: notmuch
The :mod:`notmuch` module provides an interface to the `notmuch
<https://notmuchmail.org>`_ functionality, directly interfacing to a
shared notmuch library. Within :mod:`notmuch`, the classes
:class:`Database`, :class:`Query` provide most of the core
functionality, returning :class:`Threads`, :class:`Messages` and
:class:`Tags`.
.. moduleauthor:: Sebastian Spaeth <Sebastian@SSpaeth.de>
:License: This module is covered under the GNU GPL v3 (or later).
.. toctree::
:maxdepth: 1
quickstart
notes
status_and_errors
database
query
messages
message
tags
threads
thread
filesystem
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

View file

@ -0,0 +1,54 @@
:class:`Message` -- A single message
====================================
.. currentmodule:: notmuch
.. autoclass:: Message
.. automethod:: get_message_id
.. automethod:: get_thread_id
.. automethod:: get_replies
.. automethod:: get_filename
.. automethod:: get_filenames
.. attribute:: FLAG
FLAG.MATCH
This flag is automatically set by a
Query.search_threads on those messages that match the
query. This allows us to distinguish matches from the rest
of the messages in that thread.
.. automethod:: get_flag
.. automethod:: set_flag
.. automethod:: get_date
.. automethod:: get_header
.. automethod:: get_tags
.. automethod:: get_property
.. automethod:: get_properties
.. automethod:: maildir_flags_to_tags
.. automethod:: tags_to_maildir_flags
.. automethod:: remove_tag
.. automethod:: add_tag
.. automethod:: remove_all_tags
.. automethod:: freeze
.. automethod:: thaw
.. automethod:: __str__

View file

@ -0,0 +1,15 @@
:class:`Messages` -- A bunch of messages
========================================
.. currentmodule:: notmuch
.. autoclass:: Messages
.. automethod:: collect_tags
.. method:: __len__()
.. warning::
:meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke
list(Messages()). Use the :meth:`Query.count_messages` function or use `len(list(msgs))`.

View file

@ -0,0 +1,6 @@
Interfacing with notmuch
========================
.. todo:: move the note about talloc out of the code
.. automodule:: notmuch

View file

@ -0,0 +1,43 @@
:class:`Query` -- A search query
================================
.. currentmodule:: notmuch
.. autoclass:: Query
.. automethod:: create
.. attribute:: Query.SORT
Defines constants that are used as the mode in which to open a database.
SORT.OLDEST_FIRST
Sort by message date, oldest first.
SORT.NEWEST_FIRST
Sort by message date, newest first.
SORT.MESSAGE_ID
Sort by email message ID.
SORT.UNSORTED
Do not apply a special sort order (returns results in document id
order).
.. automethod:: set_sort
.. attribute:: sort
Instance attribute :attr:`sort` contains the sort order (see
:attr:`Query.SORT`) if explicitly specified via
:meth:`set_sort`. By default it is set to `None`.
.. automethod:: exclude_tag
.. automethod:: search_threads
.. automethod:: search_messages
.. automethod:: count_messages
.. automethod:: count_threads

View file

@ -0,0 +1,19 @@
Quickstart and examples
=======================
.. todo:: write a nice introduction
.. todo:: improve the examples
Notmuch can be imported as::
import notmuch
or::
from notmuch import Query, Database
db = Database('path', create=True)
msgs = Query(db, 'from:myself').search_messages()
for msg in msgs:
print(msg)

View file

@ -0,0 +1,57 @@
.. currentmodule:: notmuch
Status and Errors
=================
Some methods return a status, indicating if an operation was successful and what the error was. Most of these status codes are expressed as a specific value, the :class:`notmuch.STATUS`.
.. note::
Prior to version 0.12 the exception classes and the enumeration
:class:`notmuch.STATUS` were defined in `notmuch.globals`. They
have since then been moved into `notmuch.errors`.
:class:`STATUS` -- Notmuch operation return value
--------------------------------------------------
.. autoclass:: notmuch.STATUS
:inherited-members:
.. automethod:: notmuch.STATUS.status2str
:exc:`NotmuchError` -- A Notmuch execution error
------------------------------------------------
Whenever an error occurs, we throw a special Exception :exc:`NotmuchError`, or a more fine grained Exception which is derived from it. This means it is always safe to check for NotmuchErrors if you want to catch all errors. If you are interested in more fine grained exceptions, you can use those below.
.. autoexception:: NotmuchError
The following exceptions are all directly derived from NotmuchError. Each of them corresponds to a specific :class:`notmuch.STATUS` value. You can either check the :attr:`status` attribute of a NotmuchError to see if a specific error has occurred, or you can directly check for the following Exception types:
.. autoexception:: OutOfMemoryError(message=None)
:members:
.. autoexception:: ReadOnlyDatabaseError(message=None)
:members:
.. autoexception:: XapianError(message=None)
:members:
.. autoexception:: FileError(message=None)
:members:
.. autoexception:: FileNotEmailError(message=None)
:members:
.. autoexception:: DuplicateMessageIdError(message=None)
:members:
.. autoexception:: NullPointerError(message=None)
:members:
.. autoexception:: TagTooLongError(message=None)
:members:
.. autoexception:: UnbalancedFreezeThawError(message=None)
:members:
.. autoexception:: UnbalancedAtomicError(message=None)
:members:
.. autoexception:: UnsupportedOperationError(message=None)
:members:
.. autoexception:: UpgradeRequiredError(message=None)
:members:
.. autoexception:: PathError(message=None)
:members:
.. autoexception:: NotInitializedError(message=None)
:members:

View file

@ -0,0 +1,17 @@
:class:`Tags` -- Notmuch tags
-----------------------------
.. currentmodule:: notmuch
.. autoclass:: Tags
:members:
.. method:: __len__
.. warning::
:meth:`__len__` was removed in version 0.6 as it exhausted the iterator and broke
list(Tags()). Use :meth:`len(list(msgs))` instead if you need to know the number of
tags.
.. automethod:: __str__

View file

@ -0,0 +1,26 @@
:class:`Thread` -- A single thread
==================================
.. currentmodule:: notmuch
.. autoclass:: Thread
.. automethod:: get_thread_id
.. automethod:: get_total_messages
.. automethod:: get_toplevel_messages
.. automethod:: get_matched_messages
.. automethod:: get_authors
.. automethod:: get_subject
.. automethod:: get_oldest_date
.. automethod:: get_newest_date
.. automethod:: get_tags
.. automethod:: __str__

View file

@ -0,0 +1,14 @@
:class:`Threads` -- Threads iterator
====================================
.. currentmodule:: notmuch
.. autoclass:: Threads
.. method:: __len__
.. warning::
:meth:`__len__` was removed in version 0.22 as it exhausted the
iterator and broke list(Threads()). Use `len(list(msgs))`
instead.
.. automethod:: __str__

View file

@ -0,0 +1,84 @@
"""The :mod:`notmuch` module provides most of the functionality that a user is
likely to need.
.. note:: The underlying notmuch library is build on a hierarchical
memory allocator called talloc. All objects derive from a
top-level :class:`Database` object.
This means that as soon as an object is deleted, all underlying
derived objects such as Queries, Messages, Message, and Tags will
be freed by the underlying library as well. Accessing these
objects will then lead to segfaults and other unexpected behavior.
We implement reference counting, so that parent objects can be
automatically freed when they are not needed anymore. For
example::
db = Database('path',create=True)
msgs = Query(db,'from:myself').search_messages()
This returns :class:`Messages` which internally contains a
reference to its parent :class:`Query` object. Otherwise the
Query() would be immediately freed, taking our *msgs* down with
it.
In this case, the above Query() object will be automatically freed
whenever we delete all derived objects, ie in our case:
`del(msgs)` would also delete the parent Query. It would not
delete the parent Database() though, as that is still referenced
from the variable *db* in which it is stored.
Pretty much the same is valid for all other objects in the
hierarchy, such as :class:`Query`, :class:`Messages`,
:class:`Message`, and :class:`Tags`.
"""
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010-2011 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from .database import Database
from .directory import Directory
from .filenames import Filenames
from .message import Message
from .messages import Messages
from .query import Query
from .tag import Tags
from .thread import Thread
from .threads import Threads
from .globals import nmlib
from .errors import (
STATUS,
NotmuchError,
OutOfMemoryError,
ReadOnlyDatabaseError,
XapianError,
FileError,
FileNotEmailError,
DuplicateMessageIdError,
NullPointerError,
TagTooLongError,
UnbalancedFreezeThawError,
UnbalancedAtomicError,
NotInitializedError,
UnsupportedOperationError,
UpgradeRequiredError,
PathError,
)
from .version import __VERSION__
__LICENSE__ = "GPL v3+"
__AUTHOR__ = 'Sebastian Spaeth <Sebastian@SSpaeth.de>'

View file

@ -0,0 +1,74 @@
'''
This file is part of notmuch.
This module handles differences between python2.x and python3.x and
allows the notmuch bindings to support both version families with one
source tree.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
Copyright 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
'''
import sys
if sys.version_info[0] == 2:
from ConfigParser import SafeConfigParser
class Python3StringMixIn(object):
def __str__(self):
return unicode(self).encode('utf-8')
def encode_utf8(value):
'''
Ensure a nicely utf-8 encoded string to pass to wrapped
libnotmuch functions.
C++ code expects strings to be well formatted and unicode
strings to have no null bytes.
'''
if not isinstance(value, basestring):
raise TypeError('Expected str or unicode, got %s' % type(value))
if isinstance(value, unicode):
return value.encode('utf-8', 'replace')
return value
else:
from configparser import ConfigParser as SafeConfigParser
if not hasattr(SafeConfigParser, 'readfp'): # py >= 3.12
SafeConfigParser.readfp = SafeConfigParser.read_file
class Python3StringMixIn(object):
def __str__(self):
return self.__unicode__()
def encode_utf8(value):
'''
Ensure a nicely utf-8 encoded string to pass to wrapped
libnotmuch functions.
C++ code expects strings to be well formatted and unicode
strings to have no null bytes.
'''
if not isinstance(value, str):
raise TypeError('Expected str, got %s' % type(value))
return value.encode('utf-8', 'replace')
# We import the SafeConfigParser class on behalf of other code to cope
# with the differences between Python 2 and 3.
SafeConfigParser # avoid warning about unused import

View file

@ -0,0 +1,789 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
import os
import codecs
import warnings
from ctypes import c_char_p, c_void_p, c_uint, byref, POINTER
from .compat import SafeConfigParser
from .globals import (
nmlib,
Enum,
_str,
NotmuchConfigListP,
NotmuchDatabaseP,
NotmuchDirectoryP,
NotmuchIndexoptsP,
NotmuchMessageP,
NotmuchTagsP,
)
from .errors import (
STATUS,
FileError,
NotmuchError,
NullPointerError,
NotInitializedError,
)
from .message import Message
from .tag import Tags
from .query import Query
from .directory import Directory
class Database(object):
"""The :class:`Database` is the highest-level object that notmuch
provides. It references a notmuch database, and can be opened in
read-only or read-write mode. A :class:`Query` can be derived from
or be applied to a specific database to find messages. Also adding
and removing messages to the database happens via this
object. Modifications to the database are not atmic by default (see
:meth:`begin_atomic`) and once a database has been modified, all
other database objects pointing to the same data-base will throw an
:exc:`XapianError` as the underlying database has been
modified. Close and reopen the database to continue working with it.
:class:`Database` objects implement the context manager protocol
so you can use the :keyword:`with` statement to ensure that the
database is properly closed. See :meth:`close` for more
information.
.. note::
Any function in this class can and will throw an
:exc:`NotInitializedError` if the database was not initialized
properly.
"""
_std_db_path = None
"""Class attribute to cache user's default database"""
MODE = Enum(['READ_ONLY', 'READ_WRITE'])
"""Constants: Mode in which to open the database"""
DECRYPTION_POLICY = Enum(['FALSE', 'TRUE', 'AUTO', 'NOSTASH'])
"""Constants: policies for decrypting messages during indexing"""
"""notmuch_database_get_directory"""
_get_directory = nmlib.notmuch_database_get_directory
_get_directory.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(NotmuchDirectoryP)]
_get_directory.restype = c_uint
"""notmuch_database_get_path"""
_get_path = nmlib.notmuch_database_get_path
_get_path.argtypes = [NotmuchDatabaseP]
_get_path.restype = c_char_p
"""notmuch_database_get_version"""
_get_version = nmlib.notmuch_database_get_version
_get_version.argtypes = [NotmuchDatabaseP]
_get_version.restype = c_uint
"""notmuch_database_get_revision"""
_get_revision = nmlib.notmuch_database_get_revision
_get_revision.argtypes = [NotmuchDatabaseP, POINTER(c_char_p)]
_get_revision.restype = c_uint
"""notmuch_database_open"""
_open = nmlib.notmuch_database_open
_open.argtypes = [c_char_p, c_uint, POINTER(NotmuchDatabaseP)]
_open.restype = c_uint
"""notmuch_database_upgrade"""
_upgrade = nmlib.notmuch_database_upgrade
_upgrade.argtypes = [NotmuchDatabaseP, c_void_p, c_void_p]
_upgrade.restype = c_uint
""" notmuch_database_find_message"""
_find_message = nmlib.notmuch_database_find_message
_find_message.argtypes = [NotmuchDatabaseP, c_char_p,
POINTER(NotmuchMessageP)]
_find_message.restype = c_uint
"""notmuch_database_find_message_by_filename"""
_find_message_by_filename = nmlib.notmuch_database_find_message_by_filename
_find_message_by_filename.argtypes = [NotmuchDatabaseP, c_char_p,
POINTER(NotmuchMessageP)]
_find_message_by_filename.restype = c_uint
"""notmuch_database_get_all_tags"""
_get_all_tags = nmlib.notmuch_database_get_all_tags
_get_all_tags.argtypes = [NotmuchDatabaseP]
_get_all_tags.restype = NotmuchTagsP
"""notmuch_database_create"""
_create = nmlib.notmuch_database_create
_create.argtypes = [c_char_p, POINTER(NotmuchDatabaseP)]
_create.restype = c_uint
def __init__(self, path = None, create = False,
mode = MODE.READ_ONLY):
"""If *path* is `None`, we will try to read a users notmuch
configuration and use his configured database. The location of the
configuration file can be specified through the environment variable
*NOTMUCH_CONFIG*, falling back to the default `~/.notmuch-config`.
If *create* is `True`, the database will always be created in
:attr:`MODE`.READ_WRITE mode. Default mode for opening is READ_ONLY.
:param path: Directory to open/create the database in (see
above for behavior if `None`)
:type path: `str` or `None`
:param create: Pass `False` to open an existing, `True` to create a new
database.
:type create: bool
:param mode: Mode to open a database in. Is always
:attr:`MODE`.READ_WRITE when creating a new one.
:type mode: :attr:`MODE`
:raises: :exc:`NotmuchError` or derived exception in case of
failure.
"""
self._db = None
self.mode = mode
if path is None:
# no path specified. use a user's default database
if Database._std_db_path is None:
#the following line throws a NotmuchError if it fails
Database._std_db_path = self._get_user_default_db()
path = Database._std_db_path
if create == False:
self.open(path, mode)
else:
self.create(path)
_destroy = nmlib.notmuch_database_destroy
_destroy.argtypes = [NotmuchDatabaseP]
_destroy.restype = c_uint
def __del__(self):
if self._db:
status = self._destroy(self._db)
if status != STATUS.SUCCESS:
raise NotmuchError(status)
def _assert_db_is_initialized(self):
"""Raises :exc:`NotInitializedError` if self._db is `None`"""
if not self._db:
raise NotInitializedError()
def create(self, path):
"""Creates a new notmuch database
This function is used by __init__() and usually does not need
to be called directly. It wraps the underlying
*notmuch_database_create* function and creates a new notmuch
database at *path*. It will always return a database in :attr:`MODE`
.READ_WRITE mode as creating an empty database for
reading only does not make a great deal of sense.
:param path: A directory in which we should create the database.
:type path: str
:raises: :exc:`NotmuchError` in case of any failure
(possibly after printing an error message on stderr).
"""
if self._db:
raise NotmuchError(message="Cannot create db, this Database() "
"already has an open one.")
db = NotmuchDatabaseP()
status = Database._create(_str(path), byref(db))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
self._db = db
return status
def open(self, path, mode=0):
"""Opens an existing database
This function is used by __init__() and usually does not need
to be called directly. It wraps the underlying
*notmuch_database_open* function.
:param status: Open the database in read-only or read-write mode
:type status: :attr:`MODE`
:raises: Raises :exc:`NotmuchError` in case of any failure
(possibly after printing an error message on stderr).
"""
db = NotmuchDatabaseP()
status = Database._open(_str(path), mode, byref(db))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
self._db = db
return status
_close = nmlib.notmuch_database_close
_close.argtypes = [NotmuchDatabaseP]
_close.restype = c_uint
def close(self):
'''
Closes the notmuch database.
.. warning::
This function closes the notmuch database. From that point
on every method invoked on any object ever derived from
the closed database may cease to function and raise a
NotmuchError.
'''
if self._db:
status = self._close(self._db)
if status != STATUS.SUCCESS:
raise NotmuchError(status)
def __enter__(self):
'''
Implements the context manager protocol.
'''
return self
def __exit__(self, exc_type, exc_value, traceback):
'''
Implements the context manager protocol.
'''
self.close()
def get_path(self):
"""Returns the file path of an open database"""
self._assert_db_is_initialized()
return Database._get_path(self._db).decode('utf-8')
def get_version(self):
"""Returns the database format version
:returns: The database version as positive integer
"""
self._assert_db_is_initialized()
return Database._get_version(self._db)
def get_revision (self):
"""Returns the committed database revision and UUID
:returns: (revision, uuid) The database revision as a positive integer
and the UUID of the database.
"""
self._assert_db_is_initialized()
uuid = c_char_p ()
revision = Database._get_revision(self._db, byref (uuid))
return (revision, uuid.value.decode ('utf-8'))
_needs_upgrade = nmlib.notmuch_database_needs_upgrade
_needs_upgrade.argtypes = [NotmuchDatabaseP]
_needs_upgrade.restype = bool
def needs_upgrade(self):
"""Does this database need to be upgraded before writing to it?
If this function returns `True` then no functions that modify the
database (:meth:`index_file`,
:meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
etc.) will work unless :meth:`upgrade` is called successfully first.
:returns: `True` or `False`
"""
self._assert_db_is_initialized()
return self._needs_upgrade(self._db)
def upgrade(self):
"""Upgrades the current database
After opening a database in read-write mode, the client should
check if an upgrade is needed (notmuch_database_needs_upgrade) and
if so, upgrade with this function before making any modifications.
NOT IMPLEMENTED: 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.
:TODO: catch exceptions, document return values and etc...
"""
self._assert_db_is_initialized()
status = Database._upgrade(self._db, None, None)
# TODO: catch exceptions, document return values and etc
return status
_begin_atomic = nmlib.notmuch_database_begin_atomic
_begin_atomic.argtypes = [NotmuchDatabaseP]
_begin_atomic.restype = c_uint
def begin_atomic(self):
"""Begin an atomic database operation
Any modifications performed between a successful
:meth:`begin_atomic` and a :meth:`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.
:returns: :attr:`STATUS`.SUCCESS or raises
:raises: :exc:`NotmuchError`: :attr:`STATUS`.XAPIAN_EXCEPTION
Xapian exception occurred; atomic section not entered.
*Added in notmuch 0.9*"""
self._assert_db_is_initialized()
status = self._begin_atomic(self._db)
if status != STATUS.SUCCESS:
raise NotmuchError(status)
return status
_end_atomic = nmlib.notmuch_database_end_atomic
_end_atomic.argtypes = [NotmuchDatabaseP]
_end_atomic.restype = c_uint
def end_atomic(self):
"""Indicate the end of an atomic database operation
See :meth:`begin_atomic` for details.
:returns: :attr:`STATUS`.SUCCESS or raises
:raises:
:exc:`NotmuchError`:
:attr:`STATUS`.XAPIAN_EXCEPTION
A Xapian exception occurred; atomic section not
ended.
:attr:`STATUS`.UNBALANCED_ATOMIC:
end_atomic has been called more times than begin_atomic.
*Added in notmuch 0.9*"""
self._assert_db_is_initialized()
status = self._end_atomic(self._db)
if status != STATUS.SUCCESS:
raise NotmuchError(status)
return status
def get_directory(self, path):
"""Returns a :class:`Directory` of path,
:param path: An unicode string containing the path relative to the path
of database (see :meth:`get_path`), or else should be an absolute
path with initial components that match the path of 'database'.
:returns: :class:`Directory` or raises an exception.
:raises: :exc:`FileError` if path is not relative database or absolute
with initial components same as database.
"""
self._assert_db_is_initialized()
# sanity checking if path is valid, and make path absolute
if path and path[0] == os.sep:
# we got an absolute path
if not path.startswith(self.get_path()):
# but its initial components are not equal to the db path
raise FileError('Database().get_directory() called '
'with a wrong absolute path')
abs_dirpath = path
else:
#we got a relative path, make it absolute
abs_dirpath = os.path.abspath(os.path.join(self.get_path(), path))
dir_p = NotmuchDirectoryP()
status = Database._get_directory(self._db, _str(path), byref(dir_p))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
if not dir_p:
return None
# return the Directory, init it with the absolute path
return Directory(abs_dirpath, dir_p, self)
_get_default_indexopts = nmlib.notmuch_database_get_default_indexopts
_get_default_indexopts.argtypes = [NotmuchDatabaseP]
_get_default_indexopts.restype = NotmuchIndexoptsP
_indexopts_set_decrypt_policy = nmlib.notmuch_indexopts_set_decrypt_policy
_indexopts_set_decrypt_policy.argtypes = [NotmuchIndexoptsP, c_uint]
_indexopts_set_decrypt_policy.restype = None
_indexopts_destroy = nmlib.notmuch_indexopts_destroy
_indexopts_destroy.argtypes = [NotmuchIndexoptsP]
_indexopts_destroy.restype = None
_index_file = nmlib.notmuch_database_index_file
_index_file.argtypes = [NotmuchDatabaseP, c_char_p,
c_void_p,
POINTER(NotmuchMessageP)]
_index_file.restype = c_uint
def index_file(self, filename, sync_maildir_flags=False, decrypt_policy=None):
"""Adds a new message to the database
:param filename: should be a path relative to the path of the
open database (see :meth:`get_path`), or else should be an
absolute filename with initial components that match the
path of the 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.
:param sync_maildir_flags: If the message contains Maildir
flags, we will -depending on the notmuch configuration- sync
those tags to initial notmuch tags, if set to `True`. It is
`False` by default to remain consistent with the libnotmuch
API. You might want to look into the underlying method
:meth:`Message.maildir_flags_to_tags`.
:param decrypt_policy: If the message contains any encrypted
parts, and decrypt_policy is set to
:attr:`DECRYPTION_POLICY`.TRUE, notmuch will try to
decrypt the message and index the cleartext, stashing any
discovered session keys. If it is set to
:attr:`DECRYPTION_POLICY`.FALSE, it will never try to
decrypt during indexing. If it is set to
:attr:`DECRYPTION_POLICY`.AUTO, then it will try to use
any stashed session keys it knows about, but will not try
to access the user's secret keys.
:attr:`DECRYPTION_POLICY`.NOSTASH behaves the same as
:attr:`DECRYPTION_POLICY`.TRUE except that no session keys
are stashed in the database. If decrypt_policy is set to
None (the default), then the database itself will decide
whether to decrypt, based on the `index.decrypt`
configuration setting (see notmuch-config(1)).
:returns: On success, we return
1) a :class:`Message` object that can be used for things
such as adding tags to the just-added message.
2) one of the following :attr:`STATUS` values:
:attr:`STATUS`.SUCCESS
Message successfully added to database.
:attr:`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 list of the filenames for the existing message.
:rtype: 2-tuple(:class:`Message`, :attr:`STATUS`)
:raises: Raises a :exc:`NotmuchError` with the following meaning.
If such an exception occurs, nothing was added to the database.
:attr:`STATUS`.FILE_ERROR
An error occurred trying to open the file, (such as
permission denied, or file not found, etc.).
:attr:`STATUS`.FILE_NOT_EMAIL
The contents of filename don't look like an email
message.
:attr:`STATUS`.READ_ONLY_DATABASE
Database was opened in read-only mode so no message can
be added.
"""
self._assert_db_is_initialized()
msg_p = NotmuchMessageP()
indexopts = c_void_p(None)
if decrypt_policy is not None:
indexopts = self._get_default_indexopts(self._db)
self._indexopts_set_decrypt_policy(indexopts, decrypt_policy)
status = self._index_file(self._db, _str(filename), indexopts, byref(msg_p))
if indexopts:
self._indexopts_destroy(indexopts)
if not status in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
raise NotmuchError(status)
#construct Message() and return
msg = Message(msg_p, self)
#automatic sync initial tags from Maildir flags
if sync_maildir_flags:
msg.maildir_flags_to_tags()
return (msg, status)
def add_message(self, filename, sync_maildir_flags=False):
"""Deprecated alias for :meth:`index_file`
"""
warnings.warn(
"This function is deprecated and will be removed in the future, use index_file.", DeprecationWarning)
return self.index_file(filename, sync_maildir_flags=sync_maildir_flags)
_remove_message = nmlib.notmuch_database_remove_message
_remove_message.argtypes = [NotmuchDatabaseP, c_char_p]
_remove_message.restype = c_uint
def remove_message(self, filename):
"""Removes a message (filename) from the given notmuch database
Note that only this particular filename association is removed from
the database. 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.
:returns: A :attr:`STATUS` value with the following meaning:
:attr:`STATUS`.SUCCESS
The last filename was removed and the message was removed
from the database.
:attr:`STATUS`.DUPLICATE_MESSAGE_ID
This filename was removed but the message persists in the
database with at least one other filename.
:raises: Raises a :exc:`NotmuchError` with the following meaning.
If such an exception occurs, nothing was removed from the
database.
:attr:`STATUS`.READ_ONLY_DATABASE
Database was opened in read-only mode so no message can be
removed.
"""
self._assert_db_is_initialized()
status = self._remove_message(self._db, _str(filename))
if status not in [STATUS.SUCCESS, STATUS.DUPLICATE_MESSAGE_ID]:
raise NotmuchError(status)
return status
def find_message(self, msgid):
"""Returns a :class:`Message` as identified by its message ID
Wraps the underlying *notmuch_database_find_message* function.
:param msgid: The message ID
:type msgid: unicode or str
:returns: :class:`Message` or `None` if no message is found.
:raises:
:exc:`OutOfMemoryError`
If an Out-of-memory occurred while constructing the message.
:exc:`XapianError`
In case of a Xapian Exception. These exceptions
include "Database modified" situations, e.g. when the
notmuch database has been modified by another program
in the meantime. In this case, you should close and
reopen the database and retry.
:exc:`NotInitializedError` if
the database was not initialized.
"""
self._assert_db_is_initialized()
msg_p = NotmuchMessageP()
status = Database._find_message(self._db, _str(msgid), byref(msg_p))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
return msg_p and Message(msg_p, self) or None
def find_message_by_filename(self, filename):
"""Find a message with the given filename
:returns: If the database contains a message with the given
filename, then a class:`Message:` is returned. This
function returns None if no message is found with the given
filename.
:raises: :exc:`OutOfMemoryError` if an Out-of-memory occurred while
constructing the message.
:raises: :exc:`XapianError` in case of a Xapian Exception.
These exceptions include "Database modified"
situations, e.g. when the notmuch database has been
modified by another program in the meantime. In this
case, you should close and reopen the database and
retry.
:raises: :exc:`NotInitializedError` if the database was not
initialized.
*Added in notmuch 0.9*"""
self._assert_db_is_initialized()
msg_p = NotmuchMessageP()
status = Database._find_message_by_filename(self._db, _str(filename),
byref(msg_p))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
return msg_p and Message(msg_p, self) or None
def get_all_tags(self):
"""Returns :class:`Tags` with a list of all tags found in the database
:returns: :class:`Tags`
:exception: :exc:`NotmuchError` with :attr:`STATUS`.NULL_POINTER
on error
"""
self._assert_db_is_initialized()
tags_p = Database._get_all_tags(self._db)
if not tags_p:
raise NullPointerError()
return Tags(tags_p, self)
def create_query(self, querystring):
"""Returns a :class:`Query` derived from this database
This is a shorthand method for doing::
# short version
# Automatically frees the Database() when 'q' is deleted
q = Database(dbpath).create_query('from:"Biene Maja"')
# long version, which is functionally equivalent but will keep the
# Database in the 'db' variable around after we delete 'q':
db = Database(dbpath)
q = Query(db,'from:"Biene Maja"')
This function is a python extension and not in the underlying C API.
"""
return Query(self, querystring)
"""notmuch_database_status_string"""
_status_string = nmlib.notmuch_database_status_string
_status_string.argtypes = [NotmuchDatabaseP]
_status_string.restype = c_char_p
def status_string(self):
"""Returns the status string of the database
This is sometimes used for additional error reporting
"""
self._assert_db_is_initialized()
s = Database._status_string(self._db)
if s:
return s.decode('utf-8', 'ignore')
return s
def __repr__(self):
return "'Notmuch DB " + self.get_path() + "'"
def _get_user_default_db(self):
""" Reads a user's notmuch config and returns his db location
Throws a NotmuchError if it cannot find it"""
config = SafeConfigParser()
conf_f = os.getenv('NOTMUCH_CONFIG',
os.path.expanduser('~/.notmuch-config'))
config.readfp(codecs.open(conf_f, 'r', 'utf-8'))
if not config.has_option('database', 'path'):
raise NotmuchError(message="No DB path specified"
" and no user default found")
db_path = config.get('database', 'path')
if not os.path.isabs(db_path):
db_path = os.path.expanduser(os.path.join("~", db_path))
return db_path
"""notmuch_database_get_config"""
_get_config = nmlib.notmuch_database_get_config
_get_config.argtypes = [NotmuchDatabaseP, c_char_p, POINTER(c_char_p)]
_get_config.restype = c_uint
def get_config(self, key):
"""Return the value of the given config key.
Note that only config values that are stored in the database are
searched and returned. The config file is not read.
:param key: the config key under which a value should be looked up, it
should probably be in the form "section.key"
:type key: str
:returns: the config value or the empty string if no value is present
for that key
:rtype: str
:raises: :exc:`NotmuchError` in case of failure.
"""
self._assert_db_is_initialized()
return_string = c_char_p()
status = self._get_config(self._db, _str(key), byref(return_string))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
return return_string.value.decode('utf-8')
"""notmuch_database_get_config_list"""
_get_config_list = nmlib.notmuch_database_get_config_list
_get_config_list.argtypes = [NotmuchDatabaseP, c_char_p,
POINTER(NotmuchConfigListP)]
_get_config_list.restype = c_uint
_config_list_valid = nmlib.notmuch_config_list_valid
_config_list_valid.argtypes = [NotmuchConfigListP]
_config_list_valid.restype = bool
_config_list_key = nmlib.notmuch_config_list_key
_config_list_key.argtypes = [NotmuchConfigListP]
_config_list_key.restype = c_char_p
_config_list_value = nmlib.notmuch_config_list_value
_config_list_value.argtypes = [NotmuchConfigListP]
_config_list_value.restype = c_char_p
_config_list_move_to_next = nmlib.notmuch_config_list_move_to_next
_config_list_move_to_next.argtypes = [NotmuchConfigListP]
_config_list_move_to_next.restype = None
_config_list_destroy = nmlib.notmuch_config_list_destroy
_config_list_destroy.argtypes = [NotmuchConfigListP]
_config_list_destroy.restype = None
def get_configs(self, prefix=''):
"""Return a generator of key, value pairs where the start of key
matches the given prefix
Note that only config values that are stored in the database are
searched and returned. The config file is not read. If no `prefix` is
given all config values are returned.
This could be used to get all named queries into a dict for example::
queries = {k[6:]: v for k, v in db.get_configs('query.')}
:param prefix: a string by which the keys should be selected
:type prefix: str
:yields: all key-value pairs where `prefix` matches the beginning
of the key
:ytype: pairs of str
:raises: :exc:`NotmuchError` in case of failure.
"""
self._assert_db_is_initialized()
config_list_p = NotmuchConfigListP()
status = self._get_config_list(self._db, _str(prefix),
byref(config_list_p))
if status != STATUS.SUCCESS:
raise NotmuchError(status)
while self._config_list_valid(config_list_p):
key = self._config_list_key(config_list_p).decode('utf-8')
value = self._config_list_value(config_list_p).decode('utf-8')
yield key, value
self._config_list_move_to_next(config_list_p)
"""notmuch_database_set_config"""
_set_config = nmlib.notmuch_database_set_config
_set_config.argtypes = [NotmuchDatabaseP, c_char_p, c_char_p]
_set_config.restype = c_uint
def set_config(self, key, value):
"""Set a config value in the notmuch database.
If an empty string is provided as `value` the `key` is unset!
:param key: the key to set
:type key: str
:param value: the value to store under `key`
:type value: str
:returns: None
:raises: :exc:`NotmuchError` in case of failure.
"""
self._assert_db_is_initialized()
status = self._set_config(self._db, _str(key), _str(value))
if status != STATUS.SUCCESS:
raise NotmuchError(status)

View file

@ -0,0 +1,185 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_uint, c_long
from .globals import (
nmlib,
NotmuchDirectoryP,
NotmuchFilenamesP
)
from .errors import (
STATUS,
NotmuchError,
NotInitializedError,
)
from .filenames import Filenames
class Directory(object):
"""Represents a directory entry in the notmuch directory
Modifying attributes of this object will modify the
database, not the real directory attributes.
The Directory object is usually derived from another object
e.g. via :meth:`Database.get_directory`, and will automatically be
become invalid whenever that parent is deleted. You should
therefore initialized this object handing it a reference to the
parent, preventing the parent from automatically being garbage
collected.
"""
"""notmuch_directory_get_mtime"""
_get_mtime = nmlib.notmuch_directory_get_mtime
_get_mtime.argtypes = [NotmuchDirectoryP]
_get_mtime.restype = c_long
"""notmuch_directory_set_mtime"""
_set_mtime = nmlib.notmuch_directory_set_mtime
_set_mtime.argtypes = [NotmuchDirectoryP, c_long]
_set_mtime.restype = c_uint
"""notmuch_directory_get_child_files"""
_get_child_files = nmlib.notmuch_directory_get_child_files
_get_child_files.argtypes = [NotmuchDirectoryP]
_get_child_files.restype = NotmuchFilenamesP
"""notmuch_directory_get_child_directories"""
_get_child_directories = nmlib.notmuch_directory_get_child_directories
_get_child_directories.argtypes = [NotmuchDirectoryP]
_get_child_directories.restype = NotmuchFilenamesP
def _assert_dir_is_initialized(self):
"""Raises a NotmuchError(:attr:`STATUS`.NOT_INITIALIZED)
if dir_p is None"""
if not self._dir_p:
raise NotInitializedError()
def __init__(self, path, dir_p, parent):
"""
:param path: The absolute path of the directory object.
:param dir_p: The pointer to an internal notmuch_directory_t object.
:param parent: The object this Directory is derived from
(usually a :class:`Database`). We do not directly use
this, but store a reference to it as long as
this Directory object lives. This keeps the
parent object alive.
"""
self._path = path
self._dir_p = dir_p
self._parent = parent
def set_mtime(self, mtime):
"""Sets the mtime value of this directory in the database
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 :meth:`Database.index_file` 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 :meth:`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::
:meth:`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.
:param mtime: A (time_t) timestamp
:raises: :exc:`XapianError` a Xapian exception occurred, mtime
not stored
:raises: :exc:`ReadOnlyDatabaseError` the database was opened
in read-only mode so directory mtime cannot be modified
:raises: :exc:`NotInitializedError` the directory object has not
been initialized
"""
self._assert_dir_is_initialized()
status = Directory._set_mtime(self._dir_p, mtime)
if status != STATUS.SUCCESS:
raise NotmuchError(status)
def get_mtime(self):
"""Gets the mtime value of this directory in the database
Retrieves a previously stored mtime for this directory.
:param mtime: A (time_t) timestamp
:raises: :exc:`NotmuchError`:
:attr:`STATUS`.NOT_INITIALIZED
The directory has not been initialized
"""
self._assert_dir_is_initialized()
return Directory._get_mtime(self._dir_p)
# Make mtime attribute a property of Directory()
mtime = property(get_mtime, set_mtime, doc="""Property that allows getting
and setting of the Directory *mtime* (read-write)
See :meth:`get_mtime` and :meth:`set_mtime` for usage and
possible exceptions.""")
def get_child_files(self):
"""Gets a Filenames 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.
"""
self._assert_dir_is_initialized()
files_p = Directory._get_child_files(self._dir_p)
return Filenames(files_p, self)
def get_child_directories(self):
"""Gets a :class:`Filenames` 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.
"""
self._assert_dir_is_initialized()
files_p = Directory._get_child_directories(self._dir_p)
return Filenames(files_p, self)
@property
def path(self):
"""Returns the absolute path of this Directory (read-only)"""
return self._path
def __repr__(self):
"""Object representation"""
return "<notmuch Directory object '%s'>" % self._path
_destroy = nmlib.notmuch_directory_destroy
_destroy.argtypes = [NotmuchDirectoryP]
_destroy.restype = None
def __del__(self):
"""Close and free the Directory"""
if self._dir_p:
self._destroy(self._dir_p)

View file

@ -0,0 +1,204 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p, c_int
from .globals import (
nmlib,
Enum,
Python3StringMixIn,
)
class Status(Enum):
"""Enum with a string representation of a notmuch_status_t value."""
_status2str = nmlib.notmuch_status_to_string
_status2str.restype = c_char_p
_status2str.argtypes = [c_int]
def __init__(self, statuslist):
"""It is initialized with a list of strings that are available as
Status().string1 - Status().stringn attributes.
"""
super(Status, self).__init__(statuslist)
@classmethod
def status2str(self, status):
"""Get a (unicode) string representation of a notmuch_status_t value."""
# define strings for custom error messages
if status == STATUS.NOT_INITIALIZED:
return "Operation on uninitialized object impossible."
return unicode(Status._status2str(status))
STATUS = Status(['SUCCESS',
'OUT_OF_MEMORY',
'READ_ONLY_DATABASE',
'XAPIAN_EXCEPTION',
'FILE_ERROR',
'FILE_NOT_EMAIL',
'DUPLICATE_MESSAGE_ID',
'NULL_POINTER',
'TAG_TOO_LONG',
'UNBALANCED_FREEZE_THAW',
'UNBALANCED_ATOMIC',
'UNSUPPORTED_OPERATION',
'UPGRADE_REQUIRED',
'PATH_ERROR',
'NOT_INITIALIZED'])
"""STATUS is a class, whose attributes provide constants that serve as return
indicators for notmuch functions. Currently the following ones are defined. For
possible return values and specific meaning for each method, see the method
description.
* SUCCESS
* OUT_OF_MEMORY
* READ_ONLY_DATABASE
* XAPIAN_EXCEPTION
* FILE_ERROR
* FILE_NOT_EMAIL
* DUPLICATE_MESSAGE_ID
* NULL_POINTER
* TAG_TOO_LONG
* UNBALANCED_FREEZE_THAW
* UNBALANCED_ATOMIC
* UNSUPPORTED_OPERATION
* UPGRADE_REQUIRED
* PATH_ERROR
* NOT_INITIALIZED
Invoke the class method `notmuch.STATUS.status2str` with a status value as
argument to receive a human readable string"""
STATUS.__name__ = 'STATUS'
class NotmuchError(Exception, Python3StringMixIn):
"""Is initiated with a (notmuch.STATUS[, message=None]). It will not
return an instance of the class NotmuchError, but a derived instance
of a more specific Error Message, e.g. OutOfMemoryError. Each status
but SUCCESS has a corresponding subclassed Exception."""
@classmethod
def get_exc_subclass(cls, status):
"""Returns a fine grained Exception() type,
detailing the error status"""
subclasses = {
STATUS.OUT_OF_MEMORY: OutOfMemoryError,
STATUS.READ_ONLY_DATABASE: ReadOnlyDatabaseError,
STATUS.XAPIAN_EXCEPTION: XapianError,
STATUS.FILE_ERROR: FileError,
STATUS.FILE_NOT_EMAIL: FileNotEmailError,
STATUS.DUPLICATE_MESSAGE_ID: DuplicateMessageIdError,
STATUS.NULL_POINTER: NullPointerError,
STATUS.TAG_TOO_LONG: TagTooLongError,
STATUS.UNBALANCED_FREEZE_THAW: UnbalancedFreezeThawError,
STATUS.UNBALANCED_ATOMIC: UnbalancedAtomicError,
STATUS.UNSUPPORTED_OPERATION: UnsupportedOperationError,
STATUS.UPGRADE_REQUIRED: UpgradeRequiredError,
STATUS.PATH_ERROR: PathError,
STATUS.NOT_INITIALIZED: NotInitializedError,
}
assert 0 < status <= len(subclasses)
return subclasses[status]
def __new__(cls, *args, **kwargs):
"""Return a correct subclass of NotmuchError if needed
We return a NotmuchError instance if status is None (or 0) and a
subclass that inherits from NotmuchError depending on the
'status' parameter otherwise."""
# get 'status'. Passed in as arg or kwarg?
status = args[0] if len(args) else kwargs.get('status', None)
# no 'status' or cls is subclass already, return 'cls' instance
if not status or cls != NotmuchError:
return super(NotmuchError, cls).__new__(cls)
subclass = cls.get_exc_subclass(status) # which class to use?
return subclass.__new__(subclass, *args, **kwargs)
def __init__(self, status=None, message=None):
self.status = status
self.message = message
def __unicode__(self):
if self.message is not None:
return self.message
elif self.status is not None:
return STATUS.status2str(self.status)
else:
return 'Unknown error'
# List of Subclassed exceptions that correspond to STATUS values and are
# subclasses of NotmuchError.
class OutOfMemoryError(NotmuchError):
status = STATUS.OUT_OF_MEMORY
class ReadOnlyDatabaseError(NotmuchError):
status = STATUS.READ_ONLY_DATABASE
class XapianError(NotmuchError):
status = STATUS.XAPIAN_EXCEPTION
class FileError(NotmuchError):
status = STATUS.FILE_ERROR
class FileNotEmailError(NotmuchError):
status = STATUS.FILE_NOT_EMAIL
class DuplicateMessageIdError(NotmuchError):
status = STATUS.DUPLICATE_MESSAGE_ID
class NullPointerError(NotmuchError):
status = STATUS.NULL_POINTER
class TagTooLongError(NotmuchError):
status = STATUS.TAG_TOO_LONG
class UnbalancedFreezeThawError(NotmuchError):
status = STATUS.UNBALANCED_FREEZE_THAW
class UnbalancedAtomicError(NotmuchError):
status = STATUS.UNBALANCED_ATOMIC
class UnsupportedOperationError(NotmuchError):
status = STATUS.UNSUPPORTED_OPERATION
class UpgradeRequiredError(NotmuchError):
status = STATUS.UPGRADE_REQUIRED
class PathError(NotmuchError):
status = STATUS.PATH_ERROR
class NotInitializedError(NotmuchError):
"""Derived from NotmuchError, this occurs if the underlying data
structure (e.g. database is not initialized (yet) or an iterator has
been exhausted. You can test for NotmuchError with .status =
STATUS.NOT_INITIALIZED"""
status = STATUS.NOT_INITIALIZED

View file

@ -0,0 +1,131 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p
from .globals import (
nmlib,
NotmuchFilenamesP,
Python3StringMixIn,
)
from .errors import (
NullPointerError,
NotInitializedError,
)
class Filenames(Python3StringMixIn):
"""Represents a list of filenames as returned by notmuch
Objects of this class implement the iterator protocol.
.. note::
The underlying library only provides a one-time iterator (it
cannot reset the iterator to the start). Thus iterating over
the function will "exhaust" the list of tags, and a subsequent
iteration attempt will raise a
:exc:`NotInitializedError`. Also note, that any function that
uses iteration (nearly all) will also exhaust the tags. So
both::
for name in filenames: print name
as well as::
list_of_names = list(names)
and even a simple::
#str() iterates over all tags to construct a space separated list
print(str(filenames))
will "exhaust" the Filenames. However, you can use
:meth:`Message.get_filenames` repeatedly to get fresh
Filenames objects to perform various actions on filenames.
"""
#notmuch_filenames_get
_get = nmlib.notmuch_filenames_get
_get.argtypes = [NotmuchFilenamesP]
_get.restype = c_char_p
def __init__(self, files_p, parent):
"""
:param files_p: A pointer to an underlying *notmuch_tags_t*
structure. These are not publicly exposed, so a user
will almost never instantiate a :class:`Tags` object
herself. They are usually handed back as a result,
e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
valid, we will raise an :exc:`NullPointerError`
if it is `None`.
:type files_p: :class:`ctypes.c_void_p`
:param parent: The parent object (ie :class:`Message` these
filenames are derived from, and saves a
reference to it, so we can automatically delete the db object
once all derived objects are dead.
"""
if not files_p:
raise NullPointerError()
self._files_p = files_p
#save reference to parent object so we keep it alive
self._parent = parent
def __iter__(self):
""" Make Filenames an iterator """
return self
_valid = nmlib.notmuch_filenames_valid
_valid.argtypes = [NotmuchFilenamesP]
_valid.restype = bool
_move_to_next = nmlib.notmuch_filenames_move_to_next
_move_to_next.argtypes = [NotmuchFilenamesP]
_move_to_next.restype = None
def __next__(self):
if not self._files_p:
raise NotInitializedError()
if not self._valid(self._files_p):
self._files_p = None
raise StopIteration
file_ = Filenames._get(self._files_p)
self._move_to_next(self._files_p)
return file_.decode('utf-8', 'ignore')
next = __next__ # python2.x iterator protocol compatibility
def __unicode__(self):
"""Represent Filenames() as newline-separated list of full paths
.. note::
This method exhausts the iterator object, so you will not be able to
iterate over them again.
"""
return "\n".join(self)
_destroy = nmlib.notmuch_filenames_destroy
_destroy.argtypes = [NotmuchFilenamesP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch filenames"""
if self._files_p:
self._destroy(self._files_p)

View file

@ -0,0 +1,105 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import CDLL, Structure, POINTER
from notmuch.version import SOVERSION
#-----------------------------------------------------------------------------
#package-global instance of the notmuch library
try:
from os import uname
if uname()[0] == 'Darwin':
nmlib = CDLL("libnotmuch.{0:s}.dylib".format(SOVERSION))
else:
nmlib = CDLL("libnotmuch.so.{0:s}".format(SOVERSION))
except:
raise ImportError("Could not find shared 'notmuch' library.")
from .compat import Python3StringMixIn, encode_utf8 as _str
# We import these on behalf of other modules. Silence warning about
# these symbols not being used.
Python3StringMixIn
_str
class Enum(object):
"""Provides ENUMS as "code=Enum(['a','b','c'])" where code.a=0 etc..."""
def __init__(self, names):
for number, name in enumerate(names):
setattr(self, name, number)
class NotmuchDatabaseS(Structure):
pass
NotmuchDatabaseP = POINTER(NotmuchDatabaseS)
class NotmuchQueryS(Structure):
pass
NotmuchQueryP = POINTER(NotmuchQueryS)
class NotmuchThreadsS(Structure):
pass
NotmuchThreadsP = POINTER(NotmuchThreadsS)
class NotmuchThreadS(Structure):
pass
NotmuchThreadP = POINTER(NotmuchThreadS)
class NotmuchMessagesS(Structure):
pass
NotmuchMessagesP = POINTER(NotmuchMessagesS)
class NotmuchMessageS(Structure):
pass
NotmuchMessageP = POINTER(NotmuchMessageS)
class NotmuchMessagePropertiesS(Structure):
pass
NotmuchMessagePropertiesP = POINTER(NotmuchMessagePropertiesS)
class NotmuchTagsS(Structure):
pass
NotmuchTagsP = POINTER(NotmuchTagsS)
class NotmuchDirectoryS(Structure):
pass
NotmuchDirectoryP = POINTER(NotmuchDirectoryS)
class NotmuchFilenamesS(Structure):
pass
NotmuchFilenamesP = POINTER(NotmuchFilenamesS)
class NotmuchConfigListS(Structure):
pass
NotmuchConfigListP = POINTER(NotmuchConfigListS)
class NotmuchIndexoptsS(Structure):
pass
NotmuchIndexoptsP = POINTER(NotmuchIndexoptsS)

View file

@ -0,0 +1,721 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
Jesse Rosenthal <jrosenthal@jhu.edu>
"""
from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref
from datetime import date
from .globals import (
nmlib,
Enum,
_str,
Python3StringMixIn,
NotmuchTagsP,
NotmuchMessageP,
NotmuchMessagesP,
NotmuchMessagePropertiesP,
NotmuchFilenamesP,
)
from .errors import (
STATUS,
NotmuchError,
NullPointerError,
NotInitializedError,
)
from .tag import Tags
from .filenames import Filenames
import email
import sys
class Message(Python3StringMixIn):
r"""Represents a single Email message
Technically, this wraps the underlying *notmuch_message_t*
structure. A user will usually not create these objects themselves
but get them as search results.
As it implements :meth:`__cmp__`, it is possible to compare two
:class:`Message`\s using `if msg1 == msg2: ...`.
"""
"""notmuch_message_get_filename (notmuch_message_t *message)"""
_get_filename = nmlib.notmuch_message_get_filename
_get_filename.argtypes = [NotmuchMessageP]
_get_filename.restype = c_char_p
"""return all filenames for a message"""
_get_filenames = nmlib.notmuch_message_get_filenames
_get_filenames.argtypes = [NotmuchMessageP]
_get_filenames.restype = NotmuchFilenamesP
"""notmuch_message_get_flag"""
_get_flag = nmlib.notmuch_message_get_flag
_get_flag.argtypes = [NotmuchMessageP, c_uint]
_get_flag.restype = bool
"""notmuch_message_set_flag"""
_set_flag = nmlib.notmuch_message_set_flag
_set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
_set_flag.restype = None
"""notmuch_message_get_message_id (notmuch_message_t *message)"""
_get_message_id = nmlib.notmuch_message_get_message_id
_get_message_id.argtypes = [NotmuchMessageP]
_get_message_id.restype = c_char_p
"""notmuch_message_get_thread_id"""
_get_thread_id = nmlib.notmuch_message_get_thread_id
_get_thread_id.argtypes = [NotmuchMessageP]
_get_thread_id.restype = c_char_p
"""notmuch_message_get_replies"""
_get_replies = nmlib.notmuch_message_get_replies
_get_replies.argtypes = [NotmuchMessageP]
_get_replies.restype = NotmuchMessagesP
"""notmuch_message_get_tags (notmuch_message_t *message)"""
_get_tags = nmlib.notmuch_message_get_tags
_get_tags.argtypes = [NotmuchMessageP]
_get_tags.restype = NotmuchTagsP
_get_date = nmlib.notmuch_message_get_date
_get_date.argtypes = [NotmuchMessageP]
_get_date.restype = c_long
_get_header = nmlib.notmuch_message_get_header
_get_header.argtypes = [NotmuchMessageP, c_char_p]
_get_header.restype = c_char_p
"""notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
_tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
_tags_to_maildir_flags.argtypes = [NotmuchMessageP]
_tags_to_maildir_flags.restype = c_int
"""notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
_maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
_maildir_flags_to_tags.argtypes = [NotmuchMessageP]
_maildir_flags_to_tags.restype = c_int
"""notmuch_message_get_property"""
_get_property = nmlib.notmuch_message_get_property
_get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)]
_get_property.restype = c_int
"""notmuch_message_get_properties"""
_get_properties = nmlib.notmuch_message_get_properties
_get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int]
_get_properties.restype = NotmuchMessagePropertiesP
"""notmuch_message_properties_valid"""
_properties_valid = nmlib.notmuch_message_properties_valid
_properties_valid.argtypes = [NotmuchMessagePropertiesP]
_properties_valid.restype = bool
"""notmuch_message_properties_value"""
_properties_value = nmlib.notmuch_message_properties_value
_properties_value.argtypes = [NotmuchMessagePropertiesP]
_properties_value.restype = c_char_p
"""notmuch_message_properties_key"""
_properties_key = nmlib.notmuch_message_properties_key
_properties_key.argtypes = [NotmuchMessagePropertiesP]
_properties_key.restype = c_char_p
"""notmuch_message_properties_move_to_next"""
_properties_move_to_next = nmlib.notmuch_message_properties_move_to_next
_properties_move_to_next.argtypes = [NotmuchMessagePropertiesP]
_properties_move_to_next.restype = None
#Constants: Flags that can be set/get with set_flag
FLAG = Enum(['MATCH'])
def __init__(self, msg_p, parent=None):
"""
:param msg_p: A pointer to an internal notmuch_message_t
Structure. If it is `None`, we will raise an
:exc:`NullPointerError`.
:param parent: A 'parent' object is passed which this message is
derived from. We save a reference to it, so we can
automatically delete the parent object once all derived
objects are dead.
"""
if not msg_p:
raise NullPointerError()
self._msg = msg_p
#keep reference to parent, so we keep it alive
self._parent = parent
def get_message_id(self):
"""Returns the message ID
:returns: String with a message ID
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
def get_thread_id(self):
"""Returns the thread ID
The returned string belongs to 'message' will only be valid for as
long as the message is valid.
This function will not return `None` since Notmuch ensures that every
message belongs to a single thread.
:returns: String with a thread ID
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
def get_replies(self):
"""Gets all direct replies to this message as :class:`Messages`
iterator
.. note::
This call only makes sense if 'message' was ultimately obtained from
a :class:`Thread` object, (such as by coming directly from the
result of calling :meth:`Thread.get_toplevel_messages` or by any
number of subsequent calls to :meth:`get_replies`). If this message
was obtained through some non-thread means, (such as by a call to
:meth:`Query.search_messages`), then this function will return
an empty Messages iterator.
:returns: :class:`Messages`.
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
msgs_p = Message._get_replies(self._msg)
from .messages import Messages, EmptyMessagesResult
if not msgs_p:
return EmptyMessagesResult(self)
return Messages(msgs_p, self)
def get_date(self):
"""Returns time_t of the message date
For the original textual representation of the Date header from the
message call notmuch_message_get_header() with a header value of
"date".
:returns: A time_t timestamp.
:rtype: c_unit64
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
return Message._get_date(self._msg)
def get_header(self, header):
"""Get the value of the specified header.
The value will be read from the actual message file, not from
the notmuch database. The header name is case insensitive.
Returns an empty string ("") if the message does not contain a
header line matching 'header'.
:param header: The name of the header to be retrieved.
It is not case-sensitive.
:type header: str
:returns: The header value as string
:raises: :exc:`NotInitializedError` if the message is not
initialized
:raises: :exc:`NullPointerError` if any error occurred
"""
if not self._msg:
raise NotInitializedError()
#Returns NULL if any error occurs.
header = Message._get_header(self._msg, _str(header))
if header == None:
raise NullPointerError()
return header.decode('UTF-8', 'ignore')
def get_filename(self):
"""Returns the file path of the message file
:returns: Absolute file path & name of the message file
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
return Message._get_filename(self._msg).decode('utf-8', 'ignore')
def get_filenames(self):
"""Get all filenames for the email corresponding to 'message'
Returns a Filenames() generator with all absolute filepaths for
messages recorded to have the same Message-ID. These files must
not necessarily have identical content."""
if not self._msg:
raise NotInitializedError()
files_p = Message._get_filenames(self._msg)
return Filenames(files_p, self)
def get_flag(self, flag):
"""Checks whether a specific flag is set for this message
The method :meth:`Query.search_threads` sets
*Message.FLAG.MATCH* for those messages that match the
query. This method allows us to get the value of this flag.
:param flag: One of the :attr:`Message.FLAG` values (currently only
*Message.FLAG.MATCH*
:returns: An unsigned int (0/1), indicating whether the flag is set.
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
return Message._get_flag(self._msg, flag)
def set_flag(self, flag, value):
"""Sets/Unsets a specific flag for this message
:param flag: One of the :attr:`Message.FLAG` values (currently only
*Message.FLAG.MATCH*
:param value: A bool indicating whether to set or unset the flag.
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._msg:
raise NotInitializedError()
self._set_flag(self._msg, flag, value)
def get_tags(self):
"""Returns the message tags
:returns: A :class:`Tags` iterator.
:raises: :exc:`NotInitializedError` if the message is not
initialized
:raises: :exc:`NullPointerError` if any error occurred
"""
if not self._msg:
raise NotInitializedError()
tags_p = Message._get_tags(self._msg)
if not tags_p:
raise NullPointerError()
return Tags(tags_p, self)
_add_tag = nmlib.notmuch_message_add_tag
_add_tag.argtypes = [NotmuchMessageP, c_char_p]
_add_tag.restype = c_uint
def add_tag(self, tag, sync_maildir_flags=False):
"""Adds a tag to the given message
Adds a tag to the current message. The maximal tag length is defined in
the notmuch library and is currently 200 bytes.
:param tag: String with a 'tag' to be added.
:param sync_maildir_flags: If notmuch configuration is set to do
this, add maildir flags corresponding to notmuch tags. See
underlying method :meth:`tags_to_maildir_flags`. Use False
if you want to add/remove many tags on a message without
having to physically rename the file every time. Do note,
that this will do nothing when a message is frozen, as tag
changes will not be committed to the database yet.
:returns: STATUS.SUCCESS if the tag was successfully added.
Raises an exception otherwise.
:raises: :exc:`NullPointerError` if the `tag` argument is NULL
:raises: :exc:`TagTooLongError` if the length of `tag` exceeds
Message.NOTMUCH_TAG_MAX)
:raises: :exc:`ReadOnlyDatabaseError` if the database was opened
in read-only mode so message cannot be modified
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
status = self._add_tag(self._msg, _str(tag))
# bail out on failure
if status != STATUS.SUCCESS:
raise NotmuchError(status)
if sync_maildir_flags:
self.tags_to_maildir_flags()
return STATUS.SUCCESS
_remove_tag = nmlib.notmuch_message_remove_tag
_remove_tag.argtypes = [NotmuchMessageP, c_char_p]
_remove_tag.restype = c_uint
def remove_tag(self, tag, sync_maildir_flags=False):
"""Removes a tag from the given message
If the message has no such tag, this is a non-operation and
will report success anyway.
:param tag: String with a 'tag' to be removed.
:param sync_maildir_flags: If notmuch configuration is set to do
this, add maildir flags corresponding to notmuch tags. See
underlying method :meth:`tags_to_maildir_flags`. Use False
if you want to add/remove many tags on a message without
having to physically rename the file every time. Do note,
that this will do nothing when a message is frozen, as tag
changes will not be committed to the database yet.
:returns: STATUS.SUCCESS if the tag was successfully removed or if
the message had no such tag.
Raises an exception otherwise.
:raises: :exc:`NullPointerError` if the `tag` argument is NULL
:raises: :exc:`TagTooLongError` if the length of `tag` exceeds
Message.NOTMUCH_TAG_MAX)
:raises: :exc:`ReadOnlyDatabaseError` if the database was opened
in read-only mode so message cannot be modified
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
status = self._remove_tag(self._msg, _str(tag))
# bail out on error
if status != STATUS.SUCCESS:
raise NotmuchError(status)
if sync_maildir_flags:
self.tags_to_maildir_flags()
return STATUS.SUCCESS
_remove_all_tags = nmlib.notmuch_message_remove_all_tags
_remove_all_tags.argtypes = [NotmuchMessageP]
_remove_all_tags.restype = c_uint
def remove_all_tags(self, sync_maildir_flags=False):
"""Removes all tags from the given message.
See :meth:`freeze` for an example showing how to safely
replace tag values.
:param sync_maildir_flags: If notmuch configuration is set to do
this, add maildir flags corresponding to notmuch tags. See
:meth:`tags_to_maildir_flags`. Use False if you want to
add/remove many tags on a message without having to
physically rename the file every time. Do note, that this
will do nothing when a message is frozen, as tag changes
will not be committed to the database yet.
:returns: STATUS.SUCCESS if the tags were successfully removed.
Raises an exception otherwise.
:raises: :exc:`ReadOnlyDatabaseError` if the database was opened
in read-only mode so message cannot be modified
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
status = self._remove_all_tags(self._msg)
# bail out on error
if status != STATUS.SUCCESS:
raise NotmuchError(status)
if sync_maildir_flags:
self.tags_to_maildir_flags()
return STATUS.SUCCESS
_freeze = nmlib.notmuch_message_freeze
_freeze.argtypes = [NotmuchMessageP]
_freeze.restype = c_uint
def get_property(self, prop):
""" Retrieve the value for a single property key
:param prop: The name of the property to get.
:returns: String with the property value or None if there is no such
key. In the case of multiple values for the given key, the
first one is retrieved.
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
value = c_char_p()
status = Message._get_property(self._msg, _str(prop), byref(value))
if status != 0:
raise NotmuchError(status)
if value is None or value.value is None:
return None
return value.value.decode('utf-8')
def get_properties(self, prop="", exact=False):
""" Get the properties of the message, returning a generator of
name, value pairs.
The generator will yield once per value. There might be more than one
value on each name, so the generator might yield the same name several
times.
:param prop: The name of the property to get. Otherwise it will return
the full list of properties of the message.
:param exact: if True, require exact match with key. Otherwise
treat as prefix.
:yields: Each property values as a pair of `name, value`
:ytype: pairs of str
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
properties = Message._get_properties(self._msg, _str(prop), exact)
while Message._properties_valid(properties):
key = Message._properties_key(properties)
value = Message._properties_value(properties)
yield key.decode("utf-8"), value.decode("utf-8")
Message._properties_move_to_next(properties)
def freeze(self):
"""Freezes the current state of 'message' within the database
This means that changes to the message state, (via :meth:`add_tag`,
:meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
committed to the database until the message is :meth:`thaw` ed.
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::
msg.freeze()
msg.remove_all_tags(False)
for tag in new_tags:
msg.add_tag(tag, False)
msg.thaw()
msg.tags_to_maildir_flags()
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
:meth:`remove_all_tags` but before :meth:`add_tag`.
:returns: STATUS.SUCCESS if the message was successfully frozen.
Raises an exception otherwise.
:raises: :exc:`ReadOnlyDatabaseError` if the database was opened
in read-only mode so message cannot be modified
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
status = self._freeze(self._msg)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
_thaw = nmlib.notmuch_message_thaw
_thaw.argtypes = [NotmuchMessageP]
_thaw.restype = c_uint
def thaw(self):
"""Thaws the current 'message'
Thaw the current 'message', synchronizing any changes that may have
occurred while 'message' was frozen into the notmuch database.
See :meth:`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.
:returns: STATUS.SUCCESS if the message was successfully frozen.
Raises an exception otherwise.
:raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
to thaw an unfrozen message. That is, there have been
an unbalanced number of calls to :meth:`freeze` and
:meth:`thaw`.
:raises: :exc:`NotInitializedError` if message has not been
initialized
"""
if not self._msg:
raise NotInitializedError()
status = self._thaw(self._msg)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
def is_match(self):
"""(Not implemented)"""
return self.get_flag(Message.FLAG.MATCH)
def tags_to_maildir_flags(self):
"""Synchronize notmuch tags to file Maildir flags
'D' if the message has the "draft" tag
'F' if the message has the "flagged" tag
'P' if the message has the "passed" tag
'R' if the message has the "replied" tag
'S' if 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".
Do note that calling this method while a message is frozen might
not work yet, as the modified tags have not been committed yet
to the database.
:returns: a :class:`STATUS` value. In short, you want to see
notmuch.STATUS.SUCCESS here. See there for details."""
if not self._msg:
raise NotInitializedError()
return Message._tags_to_maildir_flags(self._msg)
def maildir_flags_to_tags(self):
"""Synchronize file Maildir flags to notmuch tags
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. 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.)
As a convenience, you can set the sync_maildir_flags parameter in
:meth:`Database.index_file` to implicitly call this.
:returns: a :class:`STATUS`. In short, you want to see
notmuch.STATUS.SUCCESS here. See there for details."""
if not self._msg:
raise NotInitializedError()
return Message._maildir_flags_to_tags(self._msg)
def __repr__(self):
"""Represent a Message() object by str()"""
return self.__str__()
def __unicode__(self):
format = "%s (%s) (%s)"
return format % (self.get_header('from'),
self.get_tags(),
date.fromtimestamp(self.get_date()),
)
def get_message_parts(self):
"""Output like notmuch show"""
fp = open(self.get_filename(), 'rb')
if sys.version_info[0] < 3:
email_msg = email.message_from_file(fp)
else:
email_msg = email.message_from_binary_file(fp)
fp.close()
out = []
for msg in email_msg.walk():
if not msg.is_multipart():
out.append(msg)
return out
def get_part(self, num):
"""Returns the nth message body part"""
parts = self.get_message_parts()
if (num <= 0 or num > len(parts)):
return ""
else:
out_part = parts[(num - 1)]
return out_part.get_payload(decode=True)
def __hash__(self):
"""Implement hash(), so we can use Message() sets"""
file = self.get_filename()
if not file:
return None
return hash(file)
def __cmp__(self, other):
"""Implement cmp(), so we can compare Message()s
2 messages are considered equal if they point to the same
Message-Id and if they point to the same file names. If 2
Messages derive from different queries where some files have
been added or removed, the same messages would not be considered
equal (as they do not point to the same set of files
any more)."""
res = cmp(self.get_message_id(), other.get_message_id())
if res:
res = cmp(list(self.get_filenames()), list(other.get_filenames()))
return res
_destroy = nmlib.notmuch_message_destroy
_destroy.argtypes = [NotmuchMessageP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch Message"""
if self._msg:
self._destroy(self._msg)

View file

@ -0,0 +1,199 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
Jesse Rosenthal <jrosenthal@jhu.edu>
"""
from .globals import (
nmlib,
NotmuchTagsP,
NotmuchMessageP,
NotmuchMessagesP,
)
from .errors import (
NullPointerError,
NotInitializedError,
)
from .tag import Tags
from .message import Message
class Messages(object):
r"""Represents a list of notmuch messages
This object provides an iterator over a list of notmuch messages
(Technically, it provides a wrapper for the underlying
*notmuch_messages_t* structure). Do note that the underlying library
only provides a one-time iterator (it cannot reset the iterator to
the start). Thus iterating over the function will "exhaust" the list
of messages, and a subsequent iteration attempt will raise a
:exc:`NotInitializedError`. If you need to
re-iterate over a list of messages you will need to retrieve a new
:class:`Messages` object or cache your :class:`Message`\s in a list
via::
msglist = list(msgs)
You can store and reuse the single :class:`Message` objects as often
as you want as long as you keep the parent :class:`Messages` object
around. (Due to hierarchical memory allocation, all derived
:class:`Message` objects will be invalid when we delete the parent
:class:`Messages` object, even if it was already exhausted.) So
this works::
db = Database()
msgs = Query(db,'').search_messages() #get a Messages() object
msglist = list(msgs)
# msgs is "exhausted" now and msgs.next() will raise an exception.
# However it will be kept alive until all retrieved Message()
# objects are also deleted. If you do e.g. an explicit del(msgs)
# here, the following lines would fail.
# You can reiterate over *msglist* however as often as you want.
# It is simply a list with :class:`Message`s.
print (msglist[0].get_filename())
print (msglist[1].get_filename())
print (msglist[0].get_message_id())
As :class:`Message` implements both __hash__() and __cmp__(), it is
possible to make sets out of :class:`Messages` and use set
arithmetic (this happens in python and will of course be *much*
slower than redoing a proper query with the appropriate filters::
s1, s2 = set(msgs1), set(msgs2)
s.union(s2)
s1 -= s2
...
Be careful when using set arithmetic between message sets derived
from different Databases (ie the same database reopened after
messages have changed). If messages have added or removed associated
files in the meantime, it is possible that the same message would be
considered as a different object (as it points to a different file).
"""
#notmuch_messages_get
_get = nmlib.notmuch_messages_get
_get.argtypes = [NotmuchMessagesP]
_get.restype = NotmuchMessageP
_collect_tags = nmlib.notmuch_messages_collect_tags
_collect_tags.argtypes = [NotmuchMessagesP]
_collect_tags.restype = NotmuchTagsP
def __init__(self, msgs_p, parent=None):
"""
:param msgs_p: A pointer to an underlying *notmuch_messages_t*
structure. These are not publicly exposed, so a user
will almost never instantiate a :class:`Messages` object
herself. They are usually handed back as a result,
e.g. in :meth:`Query.search_messages`. *msgs_p* must be
valid, we will raise an :exc:`NullPointerError` if it is
`None`.
:type msgs_p: :class:`ctypes.c_void_p`
:param parent: The parent object
(ie :class:`Query`) these tags are derived from. It saves
a reference to it, so we can automatically delete the db
object once all derived objects are dead.
:TODO: Make the iterator work more than once and cache the tags in
the Python object.(?)
"""
if not msgs_p:
raise NullPointerError()
self._msgs = msgs_p
#store parent, so we keep them alive as long as self is alive
self._parent = parent
def collect_tags(self):
"""Return the unique :class:`Tags` in the contained messages
:returns: :class:`Tags`
:exceptions: :exc:`NotInitializedError` if not init'ed
.. note::
:meth:`collect_tags` will iterate over the messages and therefore
will not allow further iterations.
"""
if not self._msgs:
raise NotInitializedError()
# collect all tags (returns NULL on error)
tags_p = Messages._collect_tags(self._msgs)
#reset _msgs as we iterated over it and can do so only once
self._msgs = None
if not tags_p:
raise NullPointerError()
return Tags(tags_p, self)
def __iter__(self):
""" Make Messages an iterator """
return self
_valid = nmlib.notmuch_messages_valid
_valid.argtypes = [NotmuchMessagesP]
_valid.restype = bool
_move_to_next = nmlib.notmuch_messages_move_to_next
_move_to_next.argtypes = [NotmuchMessagesP]
_move_to_next.restype = None
def __next__(self):
if not self._msgs:
raise NotInitializedError()
if not self._valid(self._msgs):
self._msgs = None
raise StopIteration
msg = Message(Messages._get(self._msgs), self)
self._move_to_next(self._msgs)
return msg
next = __next__ # python2.x iterator protocol compatibility
def __nonzero__(self):
'''
Implement truth value testing. If __nonzero__ is not
implemented, the python runtime would fall back to `len(..) >
0` thus exhausting the iterator.
:returns: True if the wrapped iterator has at least one more object
left.
'''
return self._msgs and self._valid(self._msgs)
_destroy = nmlib.notmuch_messages_destroy
_destroy.argtypes = [NotmuchMessagesP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch Messages"""
if self._msgs:
self._destroy(self._msgs)
class EmptyMessagesResult(Messages):
def __init__(self, parent):
self._msgs = None
self._parent = parent
def __next__(self):
raise StopIteration()
next = __next__

View file

@ -0,0 +1,237 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p, c_uint, POINTER, byref
from .globals import (
nmlib,
Enum,
_str,
NotmuchQueryP,
NotmuchThreadsP,
NotmuchDatabaseP,
NotmuchMessagesP,
)
from .errors import (
NotmuchError,
NullPointerError,
NotInitializedError,
)
from .threads import Threads
from .messages import Messages
class Query(object):
"""Represents a search query on an opened :class:`Database`.
A query selects and filters a subset of messages from the notmuch
database we derive from.
:class:`Query` provides an instance attribute :attr:`sort`, which
contains the sort order (if specified via :meth:`set_sort`) or
`None`.
Any function in this class may throw an :exc:`NotInitializedError`
in case the underlying query object was not set up correctly.
.. note:: Do remember that as soon as we tear down this object,
all underlying derived objects such as threads,
messages, tags etc will be freed by the underlying library
as well. Accessing these objects will lead to segfaults and
other unexpected behavior. See above for more details.
"""
# constants
SORT = Enum(['OLDEST_FIRST', 'NEWEST_FIRST', 'MESSAGE_ID', 'UNSORTED'])
"""Constants: Sort order in which to return results"""
def __init__(self, db, querystr):
"""
:param db: An open database which we derive the Query from.
:type db: :class:`Database`
:param querystr: The query string for the message.
:type querystr: utf-8 encoded str or unicode
"""
self._db = None
self._query = None
self.sort = None
self.create(db, querystr)
def _assert_query_is_initialized(self):
"""Raises :exc:`NotInitializedError` if self._query is `None`"""
if not self._query:
raise NotInitializedError()
"""notmuch_query_create"""
_create = nmlib.notmuch_query_create
_create.argtypes = [NotmuchDatabaseP, c_char_p]
_create.restype = NotmuchQueryP
def create(self, db, querystr):
"""Creates a new query derived from a Database
This function is utilized by __init__() and usually does not need to
be called directly.
:param db: Database to create the query from.
:type db: :class:`Database`
:param querystr: The query string
:type querystr: utf-8 encoded str or unicode
:raises:
:exc:`NullPointerError` if the query creation failed
(e.g. too little memory).
:exc:`NotInitializedError` if the underlying db was not
initialized.
"""
db._assert_db_is_initialized()
# create reference to parent db to keep it alive
self._db = db
# create query, return None if too little mem available
query_p = Query._create(db._db, _str(querystr))
if not query_p:
raise NullPointerError
self._query = query_p
_set_sort = nmlib.notmuch_query_set_sort
_set_sort.argtypes = [NotmuchQueryP, c_uint]
_set_sort.restype = None
def set_sort(self, sort):
"""Set the sort order future results will be delivered in
:param sort: Sort order (see :attr:`Query.SORT`)
"""
self._assert_query_is_initialized()
self.sort = sort
self._set_sort(self._query, sort)
_exclude_tag = nmlib.notmuch_query_add_tag_exclude
_exclude_tag.argtypes = [NotmuchQueryP, c_char_p]
_exclude_tag.restype = None
def exclude_tag(self, tagname):
"""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.
:param tagname: Name of the tag to be excluded
"""
self._assert_query_is_initialized()
self._exclude_tag(self._query, _str(tagname))
"""notmuch_query_search_threads"""
_search_threads = nmlib.notmuch_query_search_threads
_search_threads.argtypes = [NotmuchQueryP, POINTER(NotmuchThreadsP)]
_search_threads.restype = c_uint
def search_threads(self):
r"""Execute a query for threads
Execute a query for threads, returning a :class:`Threads` iterator.
The returned threads are owned by the query and as such, will only be
valid until the Query is deleted.
The method sets :attr:`Message.FLAG`\.MATCH for those messages that
match the query. The method :meth:`Message.get_flag` allows us
to get the value of this flag.
:returns: :class:`Threads`
:raises: :exc:`NullPointerError` if search_threads failed
"""
self._assert_query_is_initialized()
threads_p = NotmuchThreadsP() # == NULL
status = Query._search_threads(self._query, byref(threads_p))
if status != 0:
raise NotmuchError(status)
if not threads_p:
raise NullPointerError
return Threads(threads_p, self)
"""notmuch_query_search_messages_st"""
_search_messages = nmlib.notmuch_query_search_messages
_search_messages.argtypes = [NotmuchQueryP, POINTER(NotmuchMessagesP)]
_search_messages.restype = c_uint
def search_messages(self):
"""Filter messages according to the query and return
:class:`Messages` in the defined sort order
:returns: :class:`Messages`
:raises: :exc:`NullPointerError` if search_messages failed
"""
self._assert_query_is_initialized()
msgs_p = NotmuchMessagesP() # == NULL
status = Query._search_messages(self._query, byref(msgs_p))
if status != 0:
raise NotmuchError(status)
if not msgs_p:
raise NullPointerError
return Messages(msgs_p, self)
_count_messages = nmlib.notmuch_query_count_messages
_count_messages.argtypes = [NotmuchQueryP, POINTER(c_uint)]
_count_messages.restype = c_uint
def count_messages(self):
'''
This function performs a search and returns Xapian's best
guess as to the number of matching messages.
:returns: the estimated number of messages matching this query
:rtype: int
'''
self._assert_query_is_initialized()
count = c_uint(0)
status = Query._count_messages(self._query, byref(count))
if status != 0:
raise NotmuchError(status)
return count.value
_count_threads = nmlib.notmuch_query_count_threads
_count_threads.argtypes = [NotmuchQueryP, POINTER(c_uint)]
_count_threads.restype = c_uint
def count_threads(self):
'''
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
meth:`Query.count_messages`.
:returns: the number of threads returned by this query
:rtype: int
'''
self._assert_query_is_initialized()
count = c_uint(0)
status = Query._count_threads(self._query, byref(count))
if status != 0:
raise NotmuchError(status)
return count.value
_destroy = nmlib.notmuch_query_destroy
_destroy.argtypes = [NotmuchQueryP]
_destroy.restype = None
def __del__(self):
"""Close and free the Query"""
if self._query:
self._destroy(self._query)

View file

@ -0,0 +1,141 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p
from .globals import (
nmlib,
Python3StringMixIn,
NotmuchTagsP,
)
from .errors import (
NullPointerError,
NotInitializedError,
)
class Tags(Python3StringMixIn):
"""Represents a list of notmuch tags
This object provides an iterator over a list of notmuch tags (which
are unicode instances).
Do note that the underlying library only provides a one-time
iterator (it cannot reset the iterator to the start). Thus iterating
over the function will "exhaust" the list of tags, and a subsequent
iteration attempt will raise a :exc:`NotInitializedError`.
Also note, that any function that uses iteration (nearly all) will
also exhaust the tags. So both::
for tag in tags: print tag
as well as::
number_of_tags = len(tags)
and even a simple::
#str() iterates over all tags to construct a space separated list
print(str(tags))
will "exhaust" the Tags. If you need to re-iterate over a list of
tags you will need to retrieve a new :class:`Tags` object.
"""
#notmuch_tags_get
_get = nmlib.notmuch_tags_get
_get.argtypes = [NotmuchTagsP]
_get.restype = c_char_p
def __init__(self, tags_p, parent=None):
"""
:param tags_p: A pointer to an underlying *notmuch_tags_t*
structure. These are not publicly exposed, so a user
will almost never instantiate a :class:`Tags` object
herself. They are usually handed back as a result,
e.g. in :meth:`Database.get_all_tags`. *tags_p* must be
valid, we will raise an :exc:`NullPointerError` if it is
`None`.
:type tags_p: :class:`ctypes.c_void_p`
:param parent: The parent object (ie :class:`Database` or
:class:`Message` these tags are derived from, and saves a
reference to it, so we can automatically delete the db object
once all derived objects are dead.
:TODO: Make the iterator optionally work more than once by
cache the tags in the Python object(?)
"""
if not tags_p:
raise NullPointerError()
self._tags = tags_p
#save reference to parent object so we keep it alive
self._parent = parent
def __iter__(self):
""" Make Tags an iterator """
return self
_valid = nmlib.notmuch_tags_valid
_valid.argtypes = [NotmuchTagsP]
_valid.restype = bool
_move_to_next = nmlib.notmuch_tags_move_to_next
_move_to_next.argtypes = [NotmuchTagsP]
_move_to_next.restype = None
def __next__(self):
if not self._tags:
raise NotInitializedError()
if not self._valid(self._tags):
self._tags = None
raise StopIteration
tag = Tags._get(self._tags).decode('UTF-8')
self._move_to_next(self._tags)
return tag
next = __next__ # python2.x iterator protocol compatibility
def __nonzero__(self):
'''
Implement truth value testing. If __nonzero__ is not
implemented, the python runtime would fall back to `len(..) >
0` thus exhausting the iterator.
:returns: True if the wrapped iterator has at least one more object
left.
'''
return self._tags and self._valid(self._tags)
def __unicode__(self):
"""string representation of :class:`Tags`: a space separated list of tags
.. note::
As this iterates over the tags, we will not be able to iterate over
them again (as in retrieve them)! If the tags have been exhausted
already, this will raise a :exc:`NotInitializedError`on subsequent
attempts.
"""
return " ".join(self)
_destroy = nmlib.notmuch_tags_destroy
_destroy.argtypes = [NotmuchTagsP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch tags"""
if self._tags:
self._destroy(self._tags)

View file

@ -0,0 +1,281 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from ctypes import c_char_p, c_long, c_int
from .globals import (
nmlib,
NotmuchThreadP,
NotmuchMessagesP,
NotmuchTagsP,
)
from .errors import (
NullPointerError,
NotInitializedError,
)
from .messages import Messages
from .tag import Tags
from datetime import date
class Thread(object):
"""Represents a single message thread."""
"""notmuch_thread_get_thread_id"""
_get_thread_id = nmlib.notmuch_thread_get_thread_id
_get_thread_id.argtypes = [NotmuchThreadP]
_get_thread_id.restype = c_char_p
"""notmuch_thread_get_authors"""
_get_authors = nmlib.notmuch_thread_get_authors
_get_authors.argtypes = [NotmuchThreadP]
_get_authors.restype = c_char_p
"""notmuch_thread_get_subject"""
_get_subject = nmlib.notmuch_thread_get_subject
_get_subject.argtypes = [NotmuchThreadP]
_get_subject.restype = c_char_p
"""notmuch_thread_get_toplevel_messages"""
_get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
_get_toplevel_messages.argtypes = [NotmuchThreadP]
_get_toplevel_messages.restype = NotmuchMessagesP
_get_newest_date = nmlib.notmuch_thread_get_newest_date
_get_newest_date.argtypes = [NotmuchThreadP]
_get_newest_date.restype = c_long
_get_oldest_date = nmlib.notmuch_thread_get_oldest_date
_get_oldest_date.argtypes = [NotmuchThreadP]
_get_oldest_date.restype = c_long
"""notmuch_thread_get_tags"""
_get_tags = nmlib.notmuch_thread_get_tags
_get_tags.argtypes = [NotmuchThreadP]
_get_tags.restype = NotmuchTagsP
def __init__(self, thread_p, parent=None):
"""
:param thread_p: A pointer to an internal notmuch_thread_t
Structure. These are not publicly exposed, so a user
will almost never instantiate a :class:`Thread` object
herself. They are usually handed back as a result,
e.g. when iterating through :class:`Threads`. *thread_p*
must be valid, we will raise an :exc:`NullPointerError`
if it is `None`.
:param parent: A 'parent' object is passed which this message is
derived from. We save a reference to it, so we can
automatically delete the parent object once all derived
objects are dead.
"""
if not thread_p:
raise NullPointerError()
self._thread = thread_p
#keep reference to parent, so we keep it alive
self._parent = parent
def get_thread_id(self):
"""Get the thread ID of 'thread'
The returned string belongs to 'thread' and will only be valid
for as long as the thread is valid.
:returns: String with a message ID
:raises: :exc:`NotInitializedError` if the thread
is not initialized.
"""
if not self._thread:
raise NotInitializedError()
return Thread._get_thread_id(self._thread).decode('utf-8', 'ignore')
_get_total_messages = nmlib.notmuch_thread_get_total_messages
_get_total_messages.argtypes = [NotmuchThreadP]
_get_total_messages.restype = c_int
def get_total_messages(self):
"""Get the total number of messages in 'thread'
:returns: The number of all messages in the database
belonging to this thread. Contrast with
:meth:`get_matched_messages`.
:raises: :exc:`NotInitializedError` if the thread
is not initialized.
"""
if not self._thread:
raise NotInitializedError()
return self._get_total_messages(self._thread)
def get_toplevel_messages(self):
"""Returns a :class:`Messages` iterator for the top-level messages in
'thread'
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.
:returns: :class:`Messages`
:raises: :exc:`NotInitializedError` if query is not initialized
:raises: :exc:`NullPointerError` if search_messages failed
"""
if not self._thread:
raise NotInitializedError()
msgs_p = Thread._get_toplevel_messages(self._thread)
if not msgs_p:
raise NullPointerError()
return Messages(msgs_p, self)
"""notmuch_thread_get_messages"""
_get_messages = nmlib.notmuch_thread_get_messages
_get_messages.argtypes = [NotmuchThreadP]
_get_messages.restype = NotmuchMessagesP
def get_messages(self):
"""Returns a :class:`Messages` iterator for all messages in 'thread'
:returns: :class:`Messages`
:raises: :exc:`NotInitializedError` if query is not initialized
:raises: :exc:`NullPointerError` if get_messages failed
"""
if not self._thread:
raise NotInitializedError()
msgs_p = Thread._get_messages(self._thread)
if not msgs_p:
raise NullPointerError()
return Messages(msgs_p, self)
_get_matched_messages = nmlib.notmuch_thread_get_matched_messages
_get_matched_messages.argtypes = [NotmuchThreadP]
_get_matched_messages.restype = c_int
def get_matched_messages(self):
"""Returns the number of messages in 'thread' that matched the query
:returns: The number of all messages belonging to this thread that
matched the :class:`Query`from which this thread was created.
Contrast with :meth:`get_total_messages`.
:raises: :exc:`NotInitializedError` if the thread
is not initialized.
"""
if not self._thread:
raise NotInitializedError()
return self._get_matched_messages(self._thread)
def get_authors(self):
"""Returns the authors of 'thread'
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 returned string belongs to 'thread' and will only be valid for
as long as this Thread() is not deleted.
"""
if not self._thread:
raise NotInitializedError()
authors = Thread._get_authors(self._thread)
if not authors:
return None
return authors.decode('UTF-8', 'ignore')
def get_subject(self):
"""Returns the Subject of 'thread'
The returned string belongs to 'thread' and will only be valid for
as long as this Thread() is not deleted.
"""
if not self._thread:
raise NotInitializedError()
subject = Thread._get_subject(self._thread)
if not subject:
return None
return subject.decode('UTF-8', 'ignore')
def get_newest_date(self):
"""Returns time_t of the newest message date
:returns: A time_t timestamp.
:rtype: c_unit64
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._thread:
raise NotInitializedError()
return Thread._get_newest_date(self._thread)
def get_oldest_date(self):
"""Returns time_t of the oldest message date
:returns: A time_t timestamp.
:rtype: c_unit64
:raises: :exc:`NotInitializedError` if the message
is not initialized.
"""
if not self._thread:
raise NotInitializedError()
return Thread._get_oldest_date(self._thread)
def get_tags(self):
""" Returns the message tags
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 :class:`Tags` object is owned by the thread and as such, will only
be valid for as long as this :class:`Thread` is valid (e.g. until the
query from which it derived is explicitly deleted).
:returns: A :class:`Tags` iterator.
:raises: :exc:`NotInitializedError` if query is not initialized
:raises: :exc:`NullPointerError` if search_messages failed
"""
if not self._thread:
raise NotInitializedError()
tags_p = Thread._get_tags(self._thread)
if not tags_p:
raise NullPointerError()
return Tags(tags_p, self)
def __unicode__(self):
frm = "thread:%s %12s [%d/%d] %s; %s (%s)"
return frm % (self.get_thread_id(),
date.fromtimestamp(self.get_newest_date()),
self.get_matched_messages(),
self.get_total_messages(),
self.get_authors(),
self.get_subject(),
self.get_tags(),
)
_destroy = nmlib.notmuch_thread_destroy
_destroy.argtypes = [NotmuchThreadP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch Thread"""
if self._thread:
self._destroy(self._thread)

View file

@ -0,0 +1,152 @@
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
from .globals import (
nmlib,
Python3StringMixIn,
NotmuchThreadP,
NotmuchThreadsP,
)
from .errors import (
NullPointerError,
NotInitializedError,
)
from .thread import Thread
class Threads(Python3StringMixIn):
"""Represents a list of notmuch threads
This object provides an iterator over a list of notmuch threads
(Technically, it provides a wrapper for the underlying
*notmuch_threads_t* structure). Do note that the underlying
library only provides a one-time iterator (it cannot reset the
iterator to the start). Thus iterating over the function will
"exhaust" the list of threads, and a subsequent iteration attempt
will raise a :exc:`NotInitializedError`. Also
note, that any function that uses iteration will also
exhaust the messages. So both::
for thread in threads: print thread
as well as::
list_of_threads = list(threads)
will "exhaust" the threads. If you need to re-iterate over a list of
messages you will need to retrieve a new :class:`Threads` object.
Things are not as bad as it seems though, you can store and reuse
the single Thread objects as often as you want as long as you
keep the parent Threads object around. (Recall that due to
hierarchical memory allocation, all derived Threads objects will
be invalid when we delete the parent Threads() object, even if it
was already "exhausted".) So this works::
db = Database()
threads = Query(db,'').search_threads() #get a Threads() object
threadlist = []
for thread in threads:
threadlist.append(thread)
# threads is "exhausted" now.
# However it will be kept around until all retrieved Thread() objects are
# also deleted. If you did e.g. an explicit del(threads) here, the
# following lines would fail.
# You can reiterate over *threadlist* however as often as you want.
# It is simply a list with Thread objects.
print (threadlist[0].get_thread_id())
print (threadlist[1].get_thread_id())
print (threadlist[0].get_total_messages())
"""
#notmuch_threads_get
_get = nmlib.notmuch_threads_get
_get.argtypes = [NotmuchThreadsP]
_get.restype = NotmuchThreadP
def __init__(self, threads_p, parent=None):
"""
:param threads_p: A pointer to an underlying *notmuch_threads_t*
structure. These are not publicly exposed, so a user
will almost never instantiate a :class:`Threads` object
herself. They are usually handed back as a result,
e.g. in :meth:`Query.search_threads`. *threads_p* must be
valid, we will raise an :exc:`NullPointerError` if it is
`None`.
:type threads_p: :class:`ctypes.c_void_p`
:param parent: The parent object
(ie :class:`Query`) these tags are derived from. It saves
a reference to it, so we can automatically delete the db
object once all derived objects are dead.
:TODO: Make the iterator work more than once and cache the tags in
the Python object.(?)
"""
if not threads_p:
raise NullPointerError()
self._threads = threads_p
#store parent, so we keep them alive as long as self is alive
self._parent = parent
def __iter__(self):
""" Make Threads an iterator """
return self
_valid = nmlib.notmuch_threads_valid
_valid.argtypes = [NotmuchThreadsP]
_valid.restype = bool
_move_to_next = nmlib.notmuch_threads_move_to_next
_move_to_next.argtypes = [NotmuchThreadsP]
_move_to_next.restype = None
def __next__(self):
if not self._threads:
raise NotInitializedError()
if not self._valid(self._threads):
self._threads = None
raise StopIteration
thread = Thread(Threads._get(self._threads), self)
self._move_to_next(self._threads)
return thread
next = __next__ # python2.x iterator protocol compatibility
def __nonzero__(self):
'''
Implement truth value testing. If __nonzero__ is not
implemented, the python runtime would fall back to `len(..) >
0` thus exhausting the iterator.
:returns: True if the wrapped iterator has at least one more object
left.
'''
return self._threads and self._valid(self._threads)
_destroy = nmlib.notmuch_threads_destroy
_destroy.argtypes = [NotmuchThreadsP]
_destroy.restype = None
def __del__(self):
"""Close and free the notmuch Threads"""
if self._threads:
self._destroy(self._threads)

View file

@ -0,0 +1,3 @@
# this file should be kept in sync with ../../../version
__VERSION__ = '0.38.2'
SOVERSION = '5'

70
bindings/python/setup.py Normal file
View file

@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
This file is part of notmuch.
Notmuch 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.
Notmuch 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 notmuch. If not, see <https://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
"""
import os
from distutils.core import setup
# get the notmuch version number without importing the notmuch module
version_file = os.path.join(os.path.dirname(__file__),
'notmuch', 'version.py')
exec(compile(open(version_file).read(), version_file, 'exec'))
assert '__VERSION__' in globals(), \
'Failed to read the notmuch binding version number'
setup(name='notmuch',
version=__VERSION__,
description='Python binding of the notmuch mail search and indexing library.',
author='Sebastian Spaeth',
author_email='Sebastian@SSpaeth.de',
url='https://notmuchmail.org/',
download_url='https://notmuchmail.org/releases/notmuch-%s.tar.gz' % __VERSION__,
packages=['notmuch'],
keywords=['library', 'email'],
long_description='''Overview
========
The notmuch module provides an interface to the `notmuch
<https://notmuchmail.org>`_ functionality, directly interfacing with a
shared notmuch library. Notmuch provides a maildatabase that allows
for extremely quick searching and filtering of your email according to
various criteria.
The documentation for the latest notmuch release can be `viewed
online <https://notmuch.readthedocs.io/>`_.
Requirements
------------
You need to have notmuch installed (or rather libnotmuch.so.1). Also,
notmuch makes use of the ctypes library, and has only been tested with
python >= 2.5. It will not work on earlier python versions.
''',
classifiers=['Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 3',
'Topic :: Communications :: Email',
'Topic :: Software Development :: Libraries'
],
platforms='',
license='https://www.gnu.org/licenses/gpl-3.0.txt',
)

7
bindings/ruby/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# .gitignore for bindings/ruby
# Generated files
/Makefile
/mkmf.log
/notmuch.so
*.o

7
bindings/ruby/README Normal file
View file

@ -0,0 +1,7 @@
To build the the notmuch ruby extension, run the following commands
from the *top level* notmuch source directory:
% ./configure
% make ruby-bindings
The generic documentation about building notmuch also applies.

493
bindings/ruby/database.c Normal file
View file

@ -0,0 +1,493 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
VALUE
notmuch_rb_database_alloc (VALUE klass)
{
return Data_Wrap_Notmuch_Object (klass, &notmuch_rb_database_type, NULL);
}
/*
* call-seq: DB.destroy => nil
*
* Destroys the database, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_database_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_database_type);
return Qnil;
}
/*
* call-seq: Notmuch::Database.new(path [, {:create => false, :mode => Notmuch::MODE_READ_ONLY}]) => DB
*
* Create or open a notmuch database using the given path.
*
* If :create is +true+, create the database instead of opening.
*
* The argument :mode specifies the open mode of the database.
*/
VALUE
notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE self)
{
const char *path;
int create, mode;
VALUE pathv, hashv;
VALUE modev;
notmuch_database_t *database;
notmuch_status_t ret;
path = NULL;
create = 0;
mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
/* Check arguments */
rb_scan_args (argc, argv, "02", &pathv, &hashv);
if (!NIL_P (pathv)) {
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
}
if (!NIL_P (hashv)) {
VALUE rmode, rcreate;
VALUE kwargs[2];
static ID keyword_ids[2];
if (!keyword_ids[0]) {
keyword_ids[0] = rb_intern_const ("mode");
keyword_ids[1] = rb_intern_const ("create");
}
rb_get_kwargs (hashv, keyword_ids, 0, 2, kwargs);
rmode = kwargs[0];
rcreate = kwargs[1];
if (rmode != Qundef) {
if (!FIXNUM_P (rmode))
rb_raise (rb_eTypeError, ":mode isn't a Fixnum");
else {
mode = FIX2INT (rmode);
switch (mode) {
case NOTMUCH_DATABASE_MODE_READ_ONLY:
case NOTMUCH_DATABASE_MODE_READ_WRITE:
break;
default:
rb_raise ( rb_eTypeError, "Invalid mode");
}
}
}
if (rcreate != Qundef)
create = RTEST (rcreate);
}
rb_check_typeddata (self, &notmuch_rb_database_type);
if (create)
ret = notmuch_database_create (path, &database);
else
ret = notmuch_database_open_with_config (path, mode, NULL, NULL, &database, NULL);
notmuch_rb_status_raise (ret);
DATA_PTR (self) = notmuch_rb_object_create (database, "notmuch_rb_database");
return self;
}
/*
* call-seq: Notmuch::Database.open(path [, ahash]) {|db| ...}
*
* Identical to new, except that when it is called with a block, it yields with
* the new instance and closes it, and returns the result which is returned from
* the block.
*/
VALUE
notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass)
{
VALUE obj;
obj = rb_class_new_instance (argc, argv, klass);
if (!rb_block_given_p ())
return obj;
return rb_ensure (rb_yield, obj, notmuch_rb_database_close, obj);
}
/*
* call-seq: DB.close => nil
*
* Close the notmuch database.
*/
VALUE
notmuch_rb_database_close (VALUE self)
{
notmuch_database_t *db;
notmuch_status_t ret;
Data_Get_Notmuch_Database (self, db);
ret = notmuch_database_close (db);
notmuch_rb_status_raise (ret);
return Qnil;
}
/*
* call-seq: DB.path => String
*
* Return the path of the database
*/
VALUE
notmuch_rb_database_path (VALUE self)
{
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
return rb_str_new2 (notmuch_database_get_path (db));
}
/*
* call-seq: DB.version => Fixnum
*
* Return the version of the database
*/
VALUE
notmuch_rb_database_version (VALUE self)
{
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
return INT2FIX (notmuch_database_get_version (db));
}
/*
* call-seq: DB.needs_upgrade? => true or false
*
* Return the +true+ if the database needs upgrading, +false+ otherwise
*/
VALUE
notmuch_rb_database_needs_upgrade (VALUE self)
{
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
return notmuch_database_needs_upgrade (db) ? Qtrue : Qfalse;
}
static void
notmuch_rb_upgrade_notify (void *closure, double progress)
{
VALUE *block = (VALUE *) closure;
rb_funcall (*block, ID_call, 1, rb_float_new (progress));
}
/*
* call-seq: DB.upgrade! [{|progress| block }] => nil
*
* Upgrade the database.
*
* If a block is given the block is called with a progress indicator as a
* floating point value in the range of [0.0..1.0].
*/
VALUE
notmuch_rb_database_upgrade (VALUE self)
{
notmuch_status_t ret;
void (*pnotify) (void *closure, double progress);
notmuch_database_t *db;
VALUE block;
Data_Get_Notmuch_Database (self, db);
if (rb_block_given_p ()) {
pnotify = notmuch_rb_upgrade_notify;
block = rb_block_proc ();
}
else
pnotify = NULL;
ret = notmuch_database_upgrade (db, pnotify, pnotify ? &block : NULL);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: DB.begin_atomic => nil
*
* Begin an atomic database operation.
*/
VALUE
notmuch_rb_database_begin_atomic (VALUE self)
{
notmuch_status_t ret;
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
ret = notmuch_database_begin_atomic (db);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: DB.end_atomic => nil
*
* Indicate the end of an atomic database operation.
*/
VALUE
notmuch_rb_database_end_atomic (VALUE self)
{
notmuch_status_t ret;
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
ret = notmuch_database_end_atomic (db);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: DB.get_directory(path) => DIR
*
* Retrieve a directory object from the database for 'path'
*/
VALUE
notmuch_rb_database_get_directory (VALUE self, VALUE pathv)
{
const char *path;
notmuch_status_t ret;
notmuch_directory_t *dir;
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
ret = notmuch_database_get_directory (db, path, &dir);
notmuch_rb_status_raise (ret);
if (dir)
return Data_Wrap_Notmuch_Object (notmuch_rb_cDirectory, &notmuch_rb_directory_type, dir);
return Qnil;
}
/*
* call-seq: DB.add_message(path) => MESSAGE, isdup
*
* Add a message to the database and return it.
*
* +isdup+ is a boolean that specifies whether the added message was a
* duplicate.
*/
VALUE
notmuch_rb_database_add_message (VALUE self, VALUE pathv)
{
const char *path;
notmuch_status_t ret;
notmuch_message_t *message;
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
ret = notmuch_database_index_file (db, path, NULL, &message);
notmuch_rb_status_raise (ret);
return rb_assoc_new (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message),
(ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse);
}
/*
* call-seq: DB.remove_message (path) => isdup
*
* Remove a message from the database.
*
* +isdup+ is a boolean that specifies whether the removed message was a
* duplicate.
*/
VALUE
notmuch_rb_database_remove_message (VALUE self, VALUE pathv)
{
const char *path;
notmuch_status_t ret;
notmuch_database_t *db;
Data_Get_Notmuch_Database (self, db);
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
ret = notmuch_database_remove_message (db, path);
notmuch_rb_status_raise (ret);
return (ret == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) ? Qtrue : Qfalse;
}
/*
* call-seq: DB.find_message(id) => MESSAGE or nil
*
* Find a message by message id.
*/
VALUE
notmuch_rb_database_find_message (VALUE self, VALUE idv)
{
const char *id;
notmuch_status_t ret;
notmuch_database_t *db;
notmuch_message_t *message;
Data_Get_Notmuch_Database (self, db);
SafeStringValue (idv);
id = RSTRING_PTR (idv);
ret = notmuch_database_find_message (db, id, &message);
notmuch_rb_status_raise (ret);
if (message)
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
return Qnil;
}
/*
* call-seq: DB.find_message_by_filename(path) => MESSAGE or nil
*
* Find a message by filename.
*/
VALUE
notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv)
{
const char *path;
notmuch_status_t ret;
notmuch_database_t *db;
notmuch_message_t *message;
Data_Get_Notmuch_Database (self, db);
SafeStringValue (pathv);
path = RSTRING_PTR (pathv);
ret = notmuch_database_find_message_by_filename (db, path, &message);
notmuch_rb_status_raise (ret);
if (message)
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message);
return Qnil;
}
/*
* call-seq: DB.get_all_tags() => TAGS
*
* Returns a list of all tags found in the database.
*/
VALUE
notmuch_rb_database_get_all_tags (VALUE self)
{
notmuch_database_t *db;
notmuch_tags_t *tags;
Data_Get_Notmuch_Database (self, db);
tags = notmuch_database_get_all_tags (db);
if (!tags) {
const char *msg = notmuch_database_status_string (db);
if (!msg)
msg = "Unknown notmuch error";
rb_raise (notmuch_rb_eBaseError, "%s", msg);
}
return notmuch_rb_tags_get (tags);
}
/*
* call-seq:
* DB.query(query) => QUERY
* DB.query(query, sort:, excluded_tags:, omit_excluded:) => QUERY
*
* Retrieve a query object for the query string 'query'. When using keyword
* arguments they are passwed to the query object.
*/
VALUE
notmuch_rb_database_query_create (int argc, VALUE *argv, VALUE self)
{
VALUE qstrv;
VALUE opts;
const char *qstr;
notmuch_query_t *query;
notmuch_database_t *db;
rb_scan_args (argc, argv, "1:", &qstrv, &opts);
Data_Get_Notmuch_Database (self, db);
SafeStringValue (qstrv);
qstr = RSTRING_PTR (qstrv);
query = notmuch_query_create (db, qstr);
if (!query)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
if (!NIL_P (opts)) {
VALUE sort, exclude_tags, omit_excluded;
VALUE kwargs[3];
static ID keyword_ids[3];
if (!keyword_ids[0]) {
keyword_ids[0] = rb_intern_const ("sort");
keyword_ids[1] = rb_intern_const ("exclude_tags");
keyword_ids[2] = rb_intern_const ("omit_excluded");
}
rb_get_kwargs (opts, keyword_ids, 0, 3, kwargs);
sort = kwargs[0];
exclude_tags = kwargs[1];
omit_excluded = kwargs[2];
if (sort != Qundef)
notmuch_query_set_sort (query, FIX2UINT (sort));
if (exclude_tags != Qundef) {
for (int i = 0; i < RARRAY_LEN (exclude_tags); i++) {
VALUE e = RARRAY_AREF (exclude_tags, i);
notmuch_query_add_tag_exclude (query, RSTRING_PTR (e));
}
}
if (omit_excluded != Qundef) {
notmuch_exclude_t omit;
omit = FIXNUM_P (omit_excluded) ? FIX2UINT (omit_excluded) : RTEST(omit_excluded);
notmuch_query_set_omit_excluded (query, omit);
}
}
return Data_Wrap_Notmuch_Object (notmuch_rb_cQuery, &notmuch_rb_query_type, query);
}

369
bindings/ruby/defs.h Normal file
View file

@ -0,0 +1,369 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011, 2012 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#ifndef DEFS_H
#define DEFS_H
#include <notmuch.h>
#include <ruby.h>
#include <talloc.h>
extern VALUE notmuch_rb_cDatabase;
extern VALUE notmuch_rb_cDirectory;
extern VALUE notmuch_rb_cFileNames;
extern VALUE notmuch_rb_cQuery;
extern VALUE notmuch_rb_cThreads;
extern VALUE notmuch_rb_cThread;
extern VALUE notmuch_rb_cMessages;
extern VALUE notmuch_rb_cMessage;
extern VALUE notmuch_rb_eBaseError;
extern VALUE notmuch_rb_eDatabaseError;
extern VALUE notmuch_rb_eMemoryError;
extern VALUE notmuch_rb_eReadOnlyError;
extern VALUE notmuch_rb_eXapianError;
extern VALUE notmuch_rb_eFileError;
extern VALUE notmuch_rb_eFileNotEmailError;
extern VALUE notmuch_rb_eNullPointerError;
extern VALUE notmuch_rb_eTagTooLongError;
extern VALUE notmuch_rb_eUnbalancedFreezeThawError;
extern VALUE notmuch_rb_eUnbalancedAtomicError;
extern ID ID_call;
/* RSTRING_PTR() is new in ruby-1.9 */
#if !defined(RSTRING_PTR)
# define RSTRING_PTR(v) (RSTRING((v))->ptr)
#endif /* !defined (RSTRING_PTR) */
extern const rb_data_type_t notmuch_rb_object_type;
extern const rb_data_type_t notmuch_rb_database_type;
extern const rb_data_type_t notmuch_rb_directory_type;
extern const rb_data_type_t notmuch_rb_query_type;
extern const rb_data_type_t notmuch_rb_threads_type;
extern const rb_data_type_t notmuch_rb_thread_type;
extern const rb_data_type_t notmuch_rb_messages_type;
extern const rb_data_type_t notmuch_rb_message_type;
extern const rb_data_type_t notmuch_rb_tags_type;
#define Data_Get_Notmuch_Rb_Object(obj, type, ptr) \
do { \
(ptr) = rb_check_typeddata ((obj), (type)); \
if (RB_UNLIKELY (!(ptr))) { \
VALUE cname = rb_class_name (CLASS_OF ((obj))); \
rb_raise (rb_eRuntimeError, "%"PRIsVALUE" object destroyed", cname); \
} \
} while (0)
#define Data_Get_Notmuch_Object(obj, type, ptr) \
do { \
notmuch_rb_object_t *rb_wrapper; \
Data_Get_Notmuch_Rb_Object ((obj), (type), rb_wrapper); \
(ptr) = rb_wrapper->nm_object; \
} while (0)
#define Data_Wrap_Notmuch_Object(klass, type, ptr) \
TypedData_Wrap_Struct ((klass), (type), notmuch_rb_object_create ((ptr), "notmuch_rb_object: " __location__))
#define Data_Get_Notmuch_Database(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_database_type, (ptr))
#define Data_Get_Notmuch_Directory(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_directory_type, (ptr))
#define Data_Get_Notmuch_Query(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_query_type, (ptr))
#define Data_Get_Notmuch_Threads(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_threads_type, (ptr))
#define Data_Get_Notmuch_Messages(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_messages_type, (ptr))
#define Data_Get_Notmuch_Thread(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_thread_type, (ptr))
#define Data_Get_Notmuch_Message(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_message_type, (ptr))
#define Data_Get_Notmuch_Tags(obj, ptr) \
Data_Get_Notmuch_Object ((obj), &notmuch_rb_tags_type, (ptr))
typedef struct {
void *nm_object;
} notmuch_rb_object_t;
static inline void *
notmuch_rb_object_create (void *nm_object, const char *name)
{
notmuch_rb_object_t *rb_wrapper = talloc_named_const (NULL, sizeof (*rb_wrapper), name);
if (RB_UNLIKELY (!rb_wrapper))
return NULL;
rb_wrapper->nm_object = nm_object;
talloc_steal (rb_wrapper, nm_object);
return rb_wrapper;
}
static inline void
notmuch_rb_object_free (void *rb_wrapper)
{
talloc_free (rb_wrapper);
}
static inline void
notmuch_rb_object_destroy (VALUE rb_object, const rb_data_type_t *type)
{
notmuch_rb_object_t *rb_wrapper;
Data_Get_Notmuch_Rb_Object (rb_object, type, rb_wrapper);
/* Call the corresponding notmuch_*_destroy function */
((void (*)(void *)) type->data) (rb_wrapper->nm_object);
notmuch_rb_object_free (rb_wrapper);
DATA_PTR (rb_object) = NULL;
}
/* status.c */
void
notmuch_rb_status_raise (notmuch_status_t status);
/* database.c */
VALUE
notmuch_rb_database_alloc (VALUE klass);
VALUE
notmuch_rb_database_destroy (VALUE self);
VALUE
notmuch_rb_database_initialize (int argc, VALUE *argv, VALUE klass);
VALUE
notmuch_rb_database_open (int argc, VALUE *argv, VALUE klass);
VALUE
notmuch_rb_database_close (VALUE self);
VALUE
notmuch_rb_database_path (VALUE self);
VALUE
notmuch_rb_database_version (VALUE self);
VALUE
notmuch_rb_database_needs_upgrade (VALUE self);
VALUE
notmuch_rb_database_upgrade (VALUE self);
VALUE
notmuch_rb_database_begin_atomic (VALUE self);
VALUE
notmuch_rb_database_end_atomic (VALUE self);
VALUE
notmuch_rb_database_get_directory (VALUE self, VALUE pathv);
VALUE
notmuch_rb_database_add_message (VALUE self, VALUE pathv);
VALUE
notmuch_rb_database_remove_message (VALUE self, VALUE pathv);
VALUE
notmuch_rb_database_find_message (VALUE self, VALUE idv);
VALUE
notmuch_rb_database_find_message_by_filename (VALUE self, VALUE pathv);
VALUE
notmuch_rb_database_get_all_tags (VALUE self);
VALUE
notmuch_rb_database_query_create (int argc, VALUE *argv, VALUE self);
/* directory.c */
VALUE
notmuch_rb_directory_destroy (VALUE self);
VALUE
notmuch_rb_directory_get_mtime (VALUE self);
VALUE
notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev);
VALUE
notmuch_rb_directory_get_child_files (VALUE self);
VALUE
notmuch_rb_directory_get_child_directories (VALUE self);
/* filenames.c */
VALUE
notmuch_rb_filenames_get (notmuch_filenames_t *fnames);
/* query.c */
VALUE
notmuch_rb_query_destroy (VALUE self);
VALUE
notmuch_rb_query_get_sort (VALUE self);
VALUE
notmuch_rb_query_set_sort (VALUE self, VALUE sortv);
VALUE
notmuch_rb_query_get_string (VALUE self);
VALUE
notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv);
VALUE
notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv);
VALUE
notmuch_rb_query_search_threads (VALUE self);
VALUE
notmuch_rb_query_search_messages (VALUE self);
VALUE
notmuch_rb_query_count_messages (VALUE self);
VALUE
notmuch_rb_query_count_threads (VALUE self);
/* threads.c */
VALUE
notmuch_rb_threads_destroy (VALUE self);
VALUE
notmuch_rb_threads_each (VALUE self);
/* messages.c */
VALUE
notmuch_rb_messages_destroy (VALUE self);
VALUE
notmuch_rb_messages_each (VALUE self);
VALUE
notmuch_rb_messages_collect_tags (VALUE self);
/* thread.c */
VALUE
notmuch_rb_thread_destroy (VALUE self);
VALUE
notmuch_rb_thread_get_thread_id (VALUE self);
VALUE
notmuch_rb_thread_get_total_messages (VALUE self);
VALUE
notmuch_rb_thread_get_toplevel_messages (VALUE self);
VALUE
notmuch_rb_thread_get_messages (VALUE self);
VALUE
notmuch_rb_thread_get_matched_messages (VALUE self);
VALUE
notmuch_rb_thread_get_authors (VALUE self);
VALUE
notmuch_rb_thread_get_subject (VALUE self);
VALUE
notmuch_rb_thread_get_oldest_date (VALUE self);
VALUE
notmuch_rb_thread_get_newest_date (VALUE self);
VALUE
notmuch_rb_thread_get_tags (VALUE self);
/* message.c */
VALUE
notmuch_rb_message_destroy (VALUE self);
VALUE
notmuch_rb_message_get_message_id (VALUE self);
VALUE
notmuch_rb_message_get_thread_id (VALUE self);
VALUE
notmuch_rb_message_get_replies (VALUE self);
VALUE
notmuch_rb_message_get_filename (VALUE self);
VALUE
notmuch_rb_message_get_filenames (VALUE self);
VALUE
notmuch_rb_message_get_flag (VALUE self, VALUE flagv);
VALUE
notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev);
VALUE
notmuch_rb_message_get_date (VALUE self);
VALUE
notmuch_rb_message_get_header (VALUE self, VALUE headerv);
VALUE
notmuch_rb_message_get_tags (VALUE self);
VALUE
notmuch_rb_message_add_tag (VALUE self, VALUE tagv);
VALUE
notmuch_rb_message_remove_tag (VALUE self, VALUE tagv);
VALUE
notmuch_rb_message_remove_all_tags (VALUE self);
VALUE
notmuch_rb_message_maildir_flags_to_tags (VALUE self);
VALUE
notmuch_rb_message_tags_to_maildir_flags (VALUE self);
VALUE
notmuch_rb_message_freeze (VALUE self);
VALUE
notmuch_rb_message_thaw (VALUE self);
/* tags.c */
VALUE
notmuch_rb_tags_get (notmuch_tags_t *tags);
/* init.c */
void
Init_notmuch (void);
#endif

110
bindings/ruby/directory.c Normal file
View file

@ -0,0 +1,110 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: DIR.destroy! => nil
*
* Destroys the directory, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_directory_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_directory_type);
return Qnil;
}
/*
* call-seq: DIR.mtime => fixnum
*
* Returns the mtime of the directory or +0+ if no mtime has been previously
* stored.
*/
VALUE
notmuch_rb_directory_get_mtime (VALUE self)
{
notmuch_directory_t *dir;
Data_Get_Notmuch_Directory (self, dir);
return UINT2NUM (notmuch_directory_get_mtime (dir));
}
/*
* call-seq: DIR.mtime=(fixnum) => nil
*
* Store an mtime within the database for the directory object.
*/
VALUE
notmuch_rb_directory_set_mtime (VALUE self, VALUE mtimev)
{
notmuch_status_t ret;
notmuch_directory_t *dir;
Data_Get_Notmuch_Directory (self, dir);
if (!FIXNUM_P (mtimev))
rb_raise (rb_eTypeError, "First argument not a fixnum");
ret = notmuch_directory_set_mtime (dir, FIX2UINT (mtimev));
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: DIR.child_files => FILENAMES
*
* Return a Notmuch::FileNames object, which is an +Enumerable+ listing all the
* filenames of messages in the database within the given directory.
*/
VALUE
notmuch_rb_directory_get_child_files (VALUE self)
{
notmuch_directory_t *dir;
notmuch_filenames_t *fnames;
Data_Get_Notmuch_Directory (self, dir);
fnames = notmuch_directory_get_child_files (dir);
return notmuch_rb_filenames_get (fnames);
}
/*
* call-seq: DIR.child_directories => FILENAMES
*
* Return a Notmuch::FileNames object, which is an +Enumerable+ listing all the
* directories in the database within the given directory.
*/
VALUE
notmuch_rb_directory_get_child_directories (VALUE self)
{
notmuch_directory_t *dir;
notmuch_filenames_t *fnames;
Data_Get_Notmuch_Directory (self, dir);
fnames = notmuch_directory_get_child_directories (dir);
return notmuch_rb_filenames_get (fnames);
}

26
bindings/ruby/extconf.rb Normal file
View file

@ -0,0 +1,26 @@
#!/usr/bin/env ruby
# coding: utf-8
# Copyright 2010, 2011, 2012 Ali Polatel <alip@exherbo.org>
# Distributed under the terms of the GNU General Public License v3
require 'mkmf'
dir = File.join(ENV['NOTMUCH_SRCDIR'], 'lib')
# includes
$INCFLAGS = "-I#{dir} #{$INCFLAGS}"
if ENV['EXTRA_LDFLAGS']
$LDFLAGS += " " + ENV['EXTRA_LDFLAGS']
end
if not ENV['LIBNOTMUCH']
exit 1
end
$LOCAL_LIBS += ENV['LIBNOTMUCH']
$LIBS += " -ltalloc"
# Create Makefile
dir_config('notmuch')
create_makefile('notmuch')

11
bindings/ruby/filenames.c Normal file
View file

@ -0,0 +1,11 @@
#include "defs.h"
VALUE
notmuch_rb_filenames_get (notmuch_filenames_t *fnames)
{
VALUE rb_array = rb_ary_new ();
for (; notmuch_filenames_valid (fnames); notmuch_filenames_move_to_next (fnames))
rb_ary_push (rb_array, rb_str_new2 (notmuch_filenames_get (fnames)));
return rb_array;
}

377
bindings/ruby/init.c Normal file
View file

@ -0,0 +1,377 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011, 2012 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
VALUE notmuch_rb_cDatabase;
VALUE notmuch_rb_cDirectory;
VALUE notmuch_rb_cQuery;
VALUE notmuch_rb_cThreads;
VALUE notmuch_rb_cThread;
VALUE notmuch_rb_cMessages;
VALUE notmuch_rb_cMessage;
VALUE notmuch_rb_eBaseError;
VALUE notmuch_rb_eDatabaseError;
VALUE notmuch_rb_eMemoryError;
VALUE notmuch_rb_eReadOnlyError;
VALUE notmuch_rb_eXapianError;
VALUE notmuch_rb_eFileError;
VALUE notmuch_rb_eFileNotEmailError;
VALUE notmuch_rb_eNullPointerError;
VALUE notmuch_rb_eTagTooLongError;
VALUE notmuch_rb_eUnbalancedFreezeThawError;
VALUE notmuch_rb_eUnbalancedAtomicError;
ID ID_call;
const rb_data_type_t notmuch_rb_object_type = {
.wrap_struct_name = "notmuch_object",
.function = {
.dfree = notmuch_rb_object_free,
},
};
#define define_type(id) \
const rb_data_type_t notmuch_rb_ ## id ## _type = { \
.wrap_struct_name = "notmuch_" #id, \
.parent = &notmuch_rb_object_type, \
.data = &notmuch_ ## id ## _destroy, \
.function = { \
.dfree = notmuch_rb_object_free, \
}, \
}
define_type (database);
define_type (directory);
define_type (query);
define_type (threads);
define_type (thread);
define_type (messages);
define_type (message);
/*
* Document-module: Notmuch
*
* == Summary
*
* Ruby extension to the <tt>notmuch</tt> mail library.
*
* == Classes
*
* Following are the classes that are most likely to be of interest to
* the user:
*
* - Notmuch::Database
* - Notmuch::Query
* - Notmuch::Threads
* - Notmuch::Messages
* - Notmuch::Thread
* - Notmuch::Message
*/
void
Init_notmuch (void)
{
VALUE mod;
ID_call = rb_intern ("call");
mod = rb_define_module ("Notmuch");
/*
* Document-const: Notmuch::MODE_READ_ONLY
*
* Open the database in read only mode
*/
rb_define_const (mod, "MODE_READ_ONLY", INT2FIX (NOTMUCH_DATABASE_MODE_READ_ONLY));
/*
* Document-const: Notmuch::MODE_READ_WRITE
*
* Open the database in read write mode
*/
rb_define_const (mod, "MODE_READ_WRITE", INT2FIX (NOTMUCH_DATABASE_MODE_READ_WRITE));
/*
* Document-const: Notmuch::SORT_OLDEST_FIRST
*
* Sort query results by oldest first
*/
rb_define_const (mod, "SORT_OLDEST_FIRST", INT2FIX (NOTMUCH_SORT_OLDEST_FIRST));
/*
* Document-const: Notmuch::SORT_NEWEST_FIRST
*
* Sort query results by newest first
*/
rb_define_const (mod, "SORT_NEWEST_FIRST", INT2FIX (NOTMUCH_SORT_NEWEST_FIRST));
/*
* Document-const: Notmuch::SORT_MESSAGE_ID
*
* Sort query results by message id
*/
rb_define_const (mod, "SORT_MESSAGE_ID", INT2FIX (NOTMUCH_SORT_MESSAGE_ID));
/*
* Document-const: Notmuch::SORT_UNSORTED
*
* Do not sort query results
*/
rb_define_const (mod, "SORT_UNSORTED", INT2FIX (NOTMUCH_SORT_UNSORTED));
/*
* Document-const: Notmuch::MESSAGE_FLAG_MATCH
*
* Message flag "match"
*/
rb_define_const (mod, "MESSAGE_FLAG_MATCH", INT2FIX (NOTMUCH_MESSAGE_FLAG_MATCH));
/*
* Document-const: Notmuch::MESSAGE_FLAG_EXCLUDED
*
* Message flag "excluded"
*/
rb_define_const (mod, "MESSAGE_FLAG_EXCLUDED", INT2FIX (NOTMUCH_MESSAGE_FLAG_EXCLUDED));
/*
* Document-const: Notmuch::TAG_MAX
*
* Maximum allowed length of a tag
*/
rb_define_const (mod, "TAG_MAX", INT2FIX (NOTMUCH_TAG_MAX));
/*
* Document-const: Notmuch::EXCLUDE_FLAG
*
* Only flag excluded results
*/
rb_define_const (mod, "EXCLUDE_FLAG", INT2FIX (NOTMUCH_EXCLUDE_FLAG));
/*
* Document-const: Notmuch::EXCLUDE_TRUE
*
* Exclude messages from the results
*/
rb_define_const (mod, "EXCLUDE_TRUE", INT2FIX (NOTMUCH_EXCLUDE_TRUE));
/*
* Document-const: Notmuch::EXCLUDE_FALSE
*
* Don't exclude anything
*/
rb_define_const (mod, "EXCLUDE_FALSE", INT2FIX (NOTMUCH_EXCLUDE_FALSE));
/*
* Document-const: Notmuch::EXCLUDE_ALL
*
* Exclude all results
*/
rb_define_const (mod, "EXCLUDE_ALL", INT2FIX (NOTMUCH_EXCLUDE_ALL));
/*
* Document-class: Notmuch::BaseError
*
* Base class for all notmuch exceptions
*/
notmuch_rb_eBaseError = rb_define_class_under (mod, "BaseError", rb_eStandardError);
/*
* Document-class: Notmuch::DatabaseError
*
* Raised when the database can't be created or opened.
*/
notmuch_rb_eDatabaseError = rb_define_class_under (mod, "DatabaseError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::MemoryError
*
* Raised when notmuch is out of memory
*/
notmuch_rb_eMemoryError = rb_define_class_under (mod, "MemoryError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::ReadOnlyError
*
* Raised when an attempt was made to write to a database opened in read-only
* mode.
*/
notmuch_rb_eReadOnlyError = rb_define_class_under (mod, "ReadOnlyError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::XapianError
*
* Raised when a Xapian exception occurs
*/
notmuch_rb_eXapianError = rb_define_class_under (mod, "XapianError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::FileError
*
* Raised when an error occurs trying to read or write to a file.
*/
notmuch_rb_eFileError = rb_define_class_under (mod, "FileError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::FileNotEmailError
*
* Raised when a file is presented that doesn't appear to be an email message.
*/
notmuch_rb_eFileNotEmailError = rb_define_class_under (mod, "FileNotEmailError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::NullPointerError
*
* Raised when the user erroneously passes a +NULL+ pointer to a notmuch
* function.
*/
notmuch_rb_eNullPointerError = rb_define_class_under (mod, "NullPointerError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::TagTooLongError
*
* Raised when a tag value is too long (exceeds Notmuch::TAG_MAX)
*/
notmuch_rb_eTagTooLongError = rb_define_class_under (mod, "TagTooLongError", notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::UnbalancedFreezeThawError
*
* Raised when the notmuch_message_thaw function has been called more times
* than notmuch_message_freeze.
*/
notmuch_rb_eUnbalancedFreezeThawError = rb_define_class_under (mod, "UnbalancedFreezeThawError",
notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::UnbalancedAtomicError
*
* Raised when notmuch_database_end_atomic has been called more times than
* notmuch_database_begin_atomic
*/
notmuch_rb_eUnbalancedAtomicError = rb_define_class_under (mod, "UnbalancedAtomicError",
notmuch_rb_eBaseError);
/*
* Document-class: Notmuch::Database
*
* Notmuch database interaction
*/
notmuch_rb_cDatabase = rb_define_class_under (mod, "Database", rb_cObject);
rb_define_alloc_func (notmuch_rb_cDatabase, notmuch_rb_database_alloc);
rb_define_singleton_method (notmuch_rb_cDatabase, "open", notmuch_rb_database_open, -1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "initialize", notmuch_rb_database_initialize, -1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "destroy!", notmuch_rb_database_destroy, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "close", notmuch_rb_database_close, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "path", notmuch_rb_database_path, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "version", notmuch_rb_database_version, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "needs_upgrade?", notmuch_rb_database_needs_upgrade, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "upgrade!", notmuch_rb_database_upgrade, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "begin_atomic", notmuch_rb_database_begin_atomic, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "end_atomic", notmuch_rb_database_end_atomic, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "get_directory", notmuch_rb_database_get_directory, 1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "add_message", notmuch_rb_database_add_message, 1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "remove_message", notmuch_rb_database_remove_message, 1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "find_message",
notmuch_rb_database_find_message, 1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "find_message_by_filename",
notmuch_rb_database_find_message_by_filename, 1); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "all_tags", notmuch_rb_database_get_all_tags, 0); /* in database.c */
rb_define_method (notmuch_rb_cDatabase, "query", notmuch_rb_database_query_create, -1); /* in database.c */
/*
* Document-class: Notmuch::Directory
*
* Notmuch directory
*/
notmuch_rb_cDirectory = rb_define_class_under (mod, "Directory", rb_cObject);
rb_undef_method (notmuch_rb_cDirectory, "initialize");
rb_define_method (notmuch_rb_cDirectory, "destroy!", notmuch_rb_directory_destroy, 0); /* in directory.c */
rb_define_method (notmuch_rb_cDirectory, "mtime", notmuch_rb_directory_get_mtime, 0); /* in directory.c */
rb_define_method (notmuch_rb_cDirectory, "mtime=", notmuch_rb_directory_set_mtime, 1); /* in directory.c */
rb_define_method (notmuch_rb_cDirectory, "child_files", notmuch_rb_directory_get_child_files, 0); /* in directory.c */
rb_define_method (notmuch_rb_cDirectory, "child_directories", notmuch_rb_directory_get_child_directories, 0); /* in directory.c */
/*
* Document-class: Notmuch::Query
*
* Notmuch query
*/
notmuch_rb_cQuery = rb_define_class_under (mod, "Query", rb_cObject);
rb_undef_method (notmuch_rb_cQuery, "initialize");
rb_define_method (notmuch_rb_cQuery, "destroy!", notmuch_rb_query_destroy, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "sort", notmuch_rb_query_get_sort, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "sort=", notmuch_rb_query_set_sort, 1); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "to_s", notmuch_rb_query_get_string, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "add_tag_exclude", notmuch_rb_query_add_tag_exclude, 1); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "omit_excluded=", notmuch_rb_query_set_omit_excluded, 1); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "search_threads", notmuch_rb_query_search_threads, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "search_messages", notmuch_rb_query_search_messages, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "count_messages", notmuch_rb_query_count_messages, 0); /* in query.c */
rb_define_method (notmuch_rb_cQuery, "count_threads", notmuch_rb_query_count_threads, 0); /* in query.c */
/*
* Document-class: Notmuch::Threads
*
* Notmuch threads
*/
notmuch_rb_cThreads = rb_define_class_under (mod, "Threads", rb_cObject);
rb_undef_method (notmuch_rb_cThreads, "initialize");
rb_define_method (notmuch_rb_cThreads, "destroy!", notmuch_rb_threads_destroy, 0); /* in threads.c */
rb_define_method (notmuch_rb_cThreads, "each", notmuch_rb_threads_each, 0); /* in threads.c */
rb_include_module (notmuch_rb_cThreads, rb_mEnumerable);
/*
* Document-class: Notmuch::Messages
*
* Notmuch messages
*/
notmuch_rb_cMessages = rb_define_class_under (mod, "Messages", rb_cObject);
rb_undef_method (notmuch_rb_cMessages, "initialize");
rb_define_method (notmuch_rb_cMessages, "destroy!", notmuch_rb_messages_destroy, 0); /* in messages.c */
rb_define_method (notmuch_rb_cMessages, "each", notmuch_rb_messages_each, 0); /* in messages.c */
rb_define_method (notmuch_rb_cMessages, "tags", notmuch_rb_messages_collect_tags, 0); /* in messages.c */
rb_include_module (notmuch_rb_cMessages, rb_mEnumerable);
/*
* Document-class: Notmuch::Thread
*
* Notmuch thread
*/
notmuch_rb_cThread = rb_define_class_under (mod, "Thread", rb_cObject);
rb_undef_method (notmuch_rb_cThread, "initialize");
rb_define_method (notmuch_rb_cThread, "destroy!", notmuch_rb_thread_destroy, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "thread_id", notmuch_rb_thread_get_thread_id, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "total_messages", notmuch_rb_thread_get_total_messages, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "toplevel_messages", notmuch_rb_thread_get_toplevel_messages, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "messages", notmuch_rb_thread_get_messages, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "matched_messages", notmuch_rb_thread_get_matched_messages, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "authors", notmuch_rb_thread_get_authors, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "subject", notmuch_rb_thread_get_subject, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "oldest_date", notmuch_rb_thread_get_oldest_date, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "newest_date", notmuch_rb_thread_get_newest_date, 0); /* in thread.c */
rb_define_method (notmuch_rb_cThread, "tags", notmuch_rb_thread_get_tags, 0); /* in thread.c */
/*
* Document-class: Notmuch::Message
*
* Notmuch message
*/
notmuch_rb_cMessage = rb_define_class_under (mod, "Message", rb_cObject);
rb_undef_method (notmuch_rb_cMessage, "initialize");
rb_define_method (notmuch_rb_cMessage, "destroy!", notmuch_rb_message_destroy, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "message_id", notmuch_rb_message_get_message_id, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "thread_id", notmuch_rb_message_get_thread_id, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "replies", notmuch_rb_message_get_replies, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "filename", notmuch_rb_message_get_filename, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "filenames", notmuch_rb_message_get_filenames, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "get_flag", notmuch_rb_message_get_flag, 1); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "set_flag", notmuch_rb_message_set_flag, 2); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "date", notmuch_rb_message_get_date, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "header", notmuch_rb_message_get_header, 1); /* in message.c */
rb_define_alias (notmuch_rb_cMessage, "[]", "header");
rb_define_method (notmuch_rb_cMessage, "tags", notmuch_rb_message_get_tags, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "add_tag", notmuch_rb_message_add_tag, 1); /* in message.c */
rb_define_alias (notmuch_rb_cMessage, "<<", "add_tag");
rb_define_method (notmuch_rb_cMessage, "remove_tag", notmuch_rb_message_remove_tag, 1); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "remove_all_tags", notmuch_rb_message_remove_all_tags, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "maildir_flags_to_tags", notmuch_rb_message_maildir_flags_to_tags, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "tags_to_maildir_flags", notmuch_rb_message_tags_to_maildir_flags, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "freeze", notmuch_rb_message_freeze, 0); /* in message.c */
rb_define_method (notmuch_rb_cMessage, "thaw", notmuch_rb_message_thaw, 0); /* in message.c */
}

366
bindings/ruby/message.c Normal file
View file

@ -0,0 +1,366 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: MESSAGE.destroy! => nil
*
* Destroys the message, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_message_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_message_type);
return Qnil;
}
/*
* call-seq: MESSAGE.message_id => String
*
* Get the message ID of 'message'.
*/
VALUE
notmuch_rb_message_get_message_id (VALUE self)
{
const char *msgid;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
msgid = notmuch_message_get_message_id (message);
return rb_str_new2 (msgid);
}
/*
* call-seq: MESSAGE.thread_id => String
*
* Get the thread ID of 'message'.
*/
VALUE
notmuch_rb_message_get_thread_id (VALUE self)
{
const char *tid;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
tid = notmuch_message_get_thread_id (message);
return rb_str_new2 (tid);
}
/*
* call-seq: MESSAGE.replies => MESSAGES
*
* Get a Notmuch::Messages enumerable for all of the replies to 'message'.
*/
VALUE
notmuch_rb_message_get_replies (VALUE self)
{
notmuch_messages_t *messages;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
messages = notmuch_message_get_replies (message);
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
}
/*
* call-seq: MESSAGE.filename => String
*
* Get a filename for the email corresponding to 'message'
*/
VALUE
notmuch_rb_message_get_filename (VALUE self)
{
const char *fname;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
fname = notmuch_message_get_filename (message);
return rb_str_new2 (fname);
}
/*
* call-seq: MESSAGE.filenames => FILENAMES
*
* Get all filenames for the email corresponding to MESSAGE.
*/
VALUE
notmuch_rb_message_get_filenames (VALUE self)
{
notmuch_filenames_t *fnames;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
fnames = notmuch_message_get_filenames (message);
return notmuch_rb_filenames_get (fnames);
}
/*
* call-seq: MESSAGE.get_flag (flag) => true or false
*
* Get a value of a flag for the email corresponding to 'message'
*/
VALUE
notmuch_rb_message_get_flag (VALUE self, VALUE flagv)
{
notmuch_message_t *message;
notmuch_bool_t is_set;
notmuch_status_t status;
Data_Get_Notmuch_Message (self, message);
if (!FIXNUM_P (flagv))
rb_raise (rb_eTypeError, "Flag not a Fixnum");
status = notmuch_message_get_flag_st (message, FIX2INT (flagv), &is_set);
notmuch_rb_status_raise (status);
return is_set ? Qtrue : Qfalse;
}
/*
* call-seq: MESSAGE.set_flag (flag, value) => nil
*
* Set a value of a flag for the email corresponding to 'message'
*/
VALUE
notmuch_rb_message_set_flag (VALUE self, VALUE flagv, VALUE valuev)
{
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
if (!FIXNUM_P (flagv))
rb_raise (rb_eTypeError, "Flag not a Fixnum");
notmuch_message_set_flag (message, FIX2INT (flagv), RTEST (valuev));
return Qnil;
}
/*
* call-seq: MESSAGE.date => Fixnum
*
* Get the date of 'message'
*/
VALUE
notmuch_rb_message_get_date (VALUE self)
{
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
return UINT2NUM (notmuch_message_get_date (message));
}
/*
* call-seq: MESSAGE.header (name) => String
*
* Get the value of the specified header from 'message'
*/
VALUE
notmuch_rb_message_get_header (VALUE self, VALUE headerv)
{
const char *header, *value;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
SafeStringValue (headerv);
header = RSTRING_PTR (headerv);
value = notmuch_message_get_header (message, header);
if (!value)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return rb_str_new2 (value);
}
/*
* call-seq: MESSAGE.tags => TAGS
*
* Get a Notmuch::Tags enumerable for all of the tags of 'message'.
*/
VALUE
notmuch_rb_message_get_tags (VALUE self)
{
notmuch_message_t *message;
notmuch_tags_t *tags;
Data_Get_Notmuch_Message (self, message);
tags = notmuch_message_get_tags (message);
if (!tags)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return notmuch_rb_tags_get (tags);
}
/*
* call-seq: MESSAGE.add_tag (tag) => true
*
* Add a tag to the 'message'
*/
VALUE
notmuch_rb_message_add_tag (VALUE self, VALUE tagv)
{
const char *tag;
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
SafeStringValue (tagv);
tag = RSTRING_PTR (tagv);
ret = notmuch_message_add_tag (message, tag);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.remove_tag (tag) => true
*
* Remove a tag from the 'message'
*/
VALUE
notmuch_rb_message_remove_tag (VALUE self, VALUE tagv)
{
const char *tag;
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
SafeStringValue (tagv);
tag = RSTRING_PTR (tagv);
ret = notmuch_message_remove_tag (message, tag);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.remove_all_tags => true
*
* Remove all tags of the 'message'
*/
VALUE
notmuch_rb_message_remove_all_tags (VALUE self)
{
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
ret = notmuch_message_remove_all_tags (message);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.maildir_flags_to_tags => true
*
* Add/remove tags according to maildir flags in the message filename (s)
*/
VALUE
notmuch_rb_message_maildir_flags_to_tags (VALUE self)
{
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
ret = notmuch_message_maildir_flags_to_tags (message);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.tags_to_maildir_flags => true
*
* Rename message filename (s) to encode tags as maildir flags
*/
VALUE
notmuch_rb_message_tags_to_maildir_flags (VALUE self)
{
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
ret = notmuch_message_tags_to_maildir_flags (message);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.freeze => true
*
* Freeze the 'message'
*/
VALUE
notmuch_rb_message_freeze (VALUE self)
{
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
ret = notmuch_message_freeze (message);
notmuch_rb_status_raise (ret);
return Qtrue;
}
/*
* call-seq: MESSAGE.thaw => true
*
* Thaw a 'message'
*/
VALUE
notmuch_rb_message_thaw (VALUE self)
{
notmuch_status_t ret;
notmuch_message_t *message;
Data_Get_Notmuch_Message (self, message);
ret = notmuch_message_thaw (message);
notmuch_rb_status_raise (ret);
return Qtrue;
}

75
bindings/ruby/messages.c Normal file
View file

@ -0,0 +1,75 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: MESSAGES.destroy! => nil
*
* Destroys the messages, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_messages_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_messages_type);
return Qnil;
}
/* call-seq: MESSAGES.each {|item| block } => MESSAGES
*
* Calls +block+ once for each message in +self+, passing that element as a
* parameter.
*/
VALUE
notmuch_rb_messages_each (VALUE self)
{
notmuch_message_t *message;
notmuch_messages_t *messages;
Data_Get_Notmuch_Messages (self, messages);
for (; notmuch_messages_valid (messages); notmuch_messages_move_to_next (messages)) {
message = notmuch_messages_get (messages);
rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cMessage, &notmuch_rb_message_type, message));
}
return self;
}
/*
* call-seq: MESSAGES.tags => TAGS
*
* Collect tags from the messages
*/
VALUE
notmuch_rb_messages_collect_tags (VALUE self)
{
notmuch_tags_t *tags;
notmuch_messages_t *messages;
Data_Get_Notmuch_Messages (self, messages);
tags = notmuch_messages_collect_tags (messages);
if (!tags)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return notmuch_rb_tags_get (tags);
}

206
bindings/ruby/query.c Normal file
View file

@ -0,0 +1,206 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011, 2012 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: QUERY.destroy! => nil
*
* Destroys the query, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_query_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_query_type);
return Qnil;
}
/*
* call-seq: QUERY.sort => fixnum
*
* Get sort type of the +QUERY+
*/
VALUE
notmuch_rb_query_get_sort (VALUE self)
{
notmuch_query_t *query;
Data_Get_Notmuch_Query (self, query);
return INT2FIX (notmuch_query_get_sort (query));
}
/*
* call-seq: QUERY.sort=(fixnum) => nil
*
* Set sort type of the +QUERY+
*/
VALUE
notmuch_rb_query_set_sort (VALUE self, VALUE sortv)
{
notmuch_query_t *query;
Data_Get_Notmuch_Query (self, query);
if (!FIXNUM_P (sortv))
rb_raise (rb_eTypeError, "Not a Fixnum");
notmuch_query_set_sort (query, FIX2UINT (sortv));
return Qnil;
}
/*
* call-seq: QUERY.to_s => string
*
* Get query string of the +QUERY+
*/
VALUE
notmuch_rb_query_get_string (VALUE self)
{
notmuch_query_t *query;
Data_Get_Notmuch_Query (self, query);
return rb_str_new2 (notmuch_query_get_query_string (query));
}
/*
* call-seq: QUERY.add_tag_exclude(tag) => nil
*
* Add a tag that will be excluded from the query results by default.
*/
VALUE
notmuch_rb_query_add_tag_exclude (VALUE self, VALUE tagv)
{
notmuch_query_t *query;
const char *tag;
Data_Get_Notmuch_Query (self, query);
tag = RSTRING_PTR(tagv);
notmuch_query_add_tag_exclude(query, tag);
return Qnil;
}
/*
* call-seq: QUERY.omit_excluded=(fixnum) => nil
*
* Specify whether to omit excluded results or simply flag them.
* By default, this is set to +Notmuch::EXCLUDE_TRUE+.
*/
VALUE
notmuch_rb_query_set_omit_excluded (VALUE self, VALUE omitv)
{
notmuch_query_t *query;
notmuch_exclude_t value;
Data_Get_Notmuch_Query (self, query);
value = FIXNUM_P (omitv) ? FIX2UINT (omitv) : RTEST(omitv);
notmuch_query_set_omit_excluded (query, value);
return Qnil;
}
/*
* call-seq: QUERY.search_threads => THREADS
*
* Search for threads
*/
VALUE
notmuch_rb_query_search_threads (VALUE self)
{
notmuch_query_t *query;
notmuch_threads_t *threads;
notmuch_status_t status;
Data_Get_Notmuch_Query (self, query);
status = notmuch_query_search_threads (query, &threads);
if (status)
notmuch_rb_status_raise (status);
return Data_Wrap_Notmuch_Object (notmuch_rb_cThreads, &notmuch_rb_threads_type, threads);
}
/*
* call-seq: QUERY.search_messages => MESSAGES
*
* Search for messages
*/
VALUE
notmuch_rb_query_search_messages (VALUE self)
{
notmuch_query_t *query;
notmuch_messages_t *messages;
notmuch_status_t status;
Data_Get_Notmuch_Query (self, query);
status = notmuch_query_search_messages (query, &messages);
if (status)
notmuch_rb_status_raise (status);
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
}
/*
* call-seq: QUERY.count_messages => Fixnum
*
* Return an estimate of the number of messages matching a search
*/
VALUE
notmuch_rb_query_count_messages (VALUE self)
{
notmuch_query_t *query;
notmuch_status_t status;
unsigned int count;
Data_Get_Notmuch_Query (self, query);
status = notmuch_query_count_messages (query, &count);
if (status)
notmuch_rb_status_raise (status);
return UINT2NUM(count);
}
/*
* call-seq: QUERY.count_threads => Fixnum
*
* Return an estimate of the number of threads matching a search
*/
VALUE
notmuch_rb_query_count_threads (VALUE self)
{
notmuch_query_t *query;
notmuch_status_t status;
unsigned int count;
Data_Get_Notmuch_Query (self, query);
status = notmuch_query_count_threads (query, &count);
if (status)
notmuch_rb_status_raise (status);
return UINT2NUM(count);
}

17
bindings/ruby/rdoc.sh Executable file
View file

@ -0,0 +1,17 @@
#!/bin/sh
if test -z "$RDOC"; then
RDOC=rdoc
if which rdoc19 >/dev/null 2>&1; then
RDOC=rdoc19
fi
fi
set -e
set -x
$RDOC --main 'Notmuch' --title 'Notmuch Ruby API' --op ruby *.c
if test "$1" = "--upload"; then
rsync -avze ssh --delete --partial --progress ruby bach.exherbo.org:public_html/notmuch/
fi

51
bindings/ruby/status.c Normal file
View file

@ -0,0 +1,51 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
void
notmuch_rb_status_raise (notmuch_status_t status)
{
switch (status) {
case NOTMUCH_STATUS_SUCCESS:
case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
break;
case NOTMUCH_STATUS_OUT_OF_MEMORY:
rb_raise (notmuch_rb_eMemoryError, "out of memory");
case NOTMUCH_STATUS_READ_ONLY_DATABASE:
rb_raise (notmuch_rb_eReadOnlyError, "read-only database");
case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
rb_raise (notmuch_rb_eXapianError, "xapian exception");
case NOTMUCH_STATUS_FILE_ERROR:
rb_raise (notmuch_rb_eFileError, "failed to read/write file");
case NOTMUCH_STATUS_FILE_NOT_EMAIL:
rb_raise (notmuch_rb_eFileNotEmailError, "file not email");
case NOTMUCH_STATUS_NULL_POINTER:
rb_raise (notmuch_rb_eNullPointerError, "null pointer");
case NOTMUCH_STATUS_TAG_TOO_LONG:
rb_raise (notmuch_rb_eTagTooLongError, "tag too long");
case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
rb_raise (notmuch_rb_eUnbalancedFreezeThawError, "unbalanced freeze/thaw");
case NOTMUCH_STATUS_UNBALANCED_ATOMIC:
rb_raise (notmuch_rb_eUnbalancedAtomicError, "unbalanced atomic");
default:
rb_raise (notmuch_rb_eBaseError, "unknown notmuch error");
}
}

13
bindings/ruby/tags.c Normal file
View file

@ -0,0 +1,13 @@
#include "defs.h"
VALUE
notmuch_rb_tags_get (notmuch_tags_t *tags)
{
VALUE rb_array = rb_ary_new ();
for (; notmuch_tags_valid (tags); notmuch_tags_move_to_next (tags)) {
const char *tag = notmuch_tags_get (tags);
rb_ary_push (rb_array, rb_str_new2 (tag));
}
return rb_array;
}

208
bindings/ruby/thread.c Normal file
View file

@ -0,0 +1,208 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: THREAD.destroy! => nil
*
* Destroys the thread, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_thread_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_thread_type);
return Qnil;
}
/*
* call-seq: THREAD.thread_id => String
*
* Returns the thread id
*/
VALUE
notmuch_rb_thread_get_thread_id (VALUE self)
{
const char *tid;
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
tid = notmuch_thread_get_thread_id (thread);
return rb_str_new2 (tid);
}
/*
* call-seq: THREAD.total_messages => fixnum
*
* Returns the number of total messages
*/
VALUE
notmuch_rb_thread_get_total_messages (VALUE self)
{
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
return INT2FIX (notmuch_thread_get_total_messages (thread));
}
/*
* call-seq: THREAD.toplevel_messages => MESSAGES
*
* Get a Notmuch::Messages iterator for the top level messages in thread.
*/
VALUE
notmuch_rb_thread_get_toplevel_messages (VALUE self)
{
notmuch_messages_t *messages;
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
messages = notmuch_thread_get_toplevel_messages (thread);
if (!messages)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
}
/*
* call-seq: THREAD.messages => MESSAGES
*
* Get a Notmuch::Messages iterator for the all messages in thread.
*/
VALUE
notmuch_rb_thread_get_messages (VALUE self)
{
notmuch_messages_t *messages;
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
messages = notmuch_thread_get_messages (thread);
if (!messages)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return Data_Wrap_Notmuch_Object (notmuch_rb_cMessages, &notmuch_rb_messages_type, messages);
}
/*
* call-seq: THREAD.matched_messages => fixnum
*
* Get the number of messages in thread that matched the search
*/
VALUE
notmuch_rb_thread_get_matched_messages (VALUE self)
{
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
return INT2FIX (notmuch_thread_get_matched_messages (thread));
}
/*
* call-seq: THREAD.authors => String
*
* Get a comma-separated list of the names of the authors.
*/
VALUE
notmuch_rb_thread_get_authors (VALUE self)
{
const char *authors;
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
authors = notmuch_thread_get_authors (thread);
return rb_str_new2 (authors);
}
/*
* call-seq: THREAD.subject => String
*
* Returns the subject of the thread
*/
VALUE
notmuch_rb_thread_get_subject (VALUE self)
{
const char *subject;
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
subject = notmuch_thread_get_subject (thread);
return rb_str_new2 (subject);
}
/*
* call-seq: THREAD.oldest_date => Fixnum
*
* Get the date of the oldest message in thread.
*/
VALUE
notmuch_rb_thread_get_oldest_date (VALUE self)
{
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
return UINT2NUM (notmuch_thread_get_oldest_date (thread));
}
/*
* call-seq: THREAD.newest_date => fixnum
*
* Get the date of the newest message in thread.
*/
VALUE
notmuch_rb_thread_get_newest_date (VALUE self)
{
notmuch_thread_t *thread;
Data_Get_Notmuch_Thread (self, thread);
return UINT2NUM (notmuch_thread_get_newest_date (thread));
}
/*
* call-seq: THREAD.tags => TAGS
*
* Get a Notmuch::Tags iterator for the tags of the thread
*/
VALUE
notmuch_rb_thread_get_tags (VALUE self)
{
notmuch_thread_t *thread;
notmuch_tags_t *tags;
Data_Get_Notmuch_Thread (self, thread);
tags = notmuch_thread_get_tags (thread);
if (!tags)
rb_raise (notmuch_rb_eMemoryError, "Out of memory");
return notmuch_rb_tags_get (tags);
}

55
bindings/ruby/threads.c Normal file
View file

@ -0,0 +1,55 @@
/* The Ruby interface to the notmuch mail library
*
* Copyright © 2010, 2011 Ali Polatel
*
* 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/ .
*
* Author: Ali Polatel <alip@exherbo.org>
*/
#include "defs.h"
/*
* call-seq: THREADS.destroy! => nil
*
* Destroys the threads, freeing all resources allocated for it.
*/
VALUE
notmuch_rb_threads_destroy (VALUE self)
{
notmuch_rb_object_destroy (self, &notmuch_rb_threads_type);
return Qnil;
}
/* call-seq: THREADS.each {|item| block } => THREADS
*
* Calls +block+ once for each thread in +self+, passing that element as a
* parameter.
*/
VALUE
notmuch_rb_threads_each (VALUE self)
{
notmuch_thread_t *thread;
notmuch_threads_t *threads;
Data_Get_Notmuch_Threads (self, threads);
for (; notmuch_threads_valid (threads); notmuch_threads_move_to_next (threads)) {
thread = notmuch_threads_get (threads);
rb_yield (Data_Wrap_Notmuch_Object (notmuch_rb_cThread, &notmuch_rb_thread_type, thread));
}
return self;
}

320
command-line-arguments.c Normal file
View file

@ -0,0 +1,320 @@
#include <assert.h>
#include <string.h>
#include <stdio.h>
#include "error_util.h"
#include "command-line-arguments.h"
typedef enum {
OPT_FAILED, /* false */
OPT_OK, /* good */
OPT_GIVEBACK, /* pop one of the arguments you thought you were getting off the stack */
} opt_handled;
/*
* Search the array of keywords for a given argument, assigning the
* output variable to the corresponding value. Return false if nothing
* matches.
*/
static opt_handled
_process_keyword_arg (const notmuch_opt_desc_t *arg_desc, char next,
const char *arg_str, bool negate)
{
const notmuch_keyword_t *keywords;
if (next == '\0') {
/* No keyword given */
arg_str = "";
}
for (keywords = arg_desc->keywords; keywords->name; keywords++) {
if (strcmp (arg_str, keywords->name) != 0)
continue;
if (arg_desc->opt_flags && negate)
*arg_desc->opt_flags &= ~keywords->value;
else if (arg_desc->opt_flags)
*arg_desc->opt_flags |= keywords->value;
else
*arg_desc->opt_keyword = keywords->value;
return OPT_OK;
}
if (arg_desc->opt_keyword && arg_desc->keyword_no_arg_value && next != ':' && next != '=') {
for (keywords = arg_desc->keywords; keywords->name; keywords++) {
if (strcmp (arg_desc->keyword_no_arg_value, keywords->name) != 0)
continue;
*arg_desc->opt_keyword = keywords->value;
fprintf (stderr,
"Warning: No known keyword option given for \"%s\", choosing value \"%s\"."
" Please specify the argument explicitly!\n", arg_desc->name,
arg_desc->keyword_no_arg_value);
return OPT_GIVEBACK;
}
fprintf (stderr,
"No matching keyword for option \"%s\" and default value \"%s\" is invalid.\n",
arg_str,
arg_desc->name);
return OPT_FAILED;
}
if (next != '\0')
fprintf (stderr, "Unknown keyword argument \"%s\" for option \"%s\".\n", arg_str,
arg_desc->name);
else
fprintf (stderr, "Option \"%s\" needs a keyword argument.\n", arg_desc->name);
return OPT_FAILED;
}
static opt_handled
_process_boolean_arg (const notmuch_opt_desc_t *arg_desc, char next,
const char *arg_str, bool negate)
{
bool value;
if (next == '\0' || strcmp (arg_str, "true") == 0) {
value = true;
} else if (strcmp (arg_str, "false") == 0) {
value = false;
} else {
fprintf (stderr, "Unknown argument \"%s\" for (boolean) option \"%s\".\n", arg_str,
arg_desc->name);
return OPT_FAILED;
}
*arg_desc->opt_bool = negate ? (! value) : value;
return OPT_OK;
}
static opt_handled
_process_int_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
{
char *endptr;
if (next == '\0' || arg_str[0] == '\0') {
fprintf (stderr, "Option \"%s\" needs an integer argument.\n", arg_desc->name);
return OPT_FAILED;
}
*arg_desc->opt_int = strtol (arg_str, &endptr, 10);
if (*endptr == '\0')
return OPT_OK;
fprintf (stderr, "Unable to parse argument \"%s\" for option \"%s\" as an integer.\n",
arg_str, arg_desc->name);
return OPT_FAILED;
}
static opt_handled
_process_string_arg (const notmuch_opt_desc_t *arg_desc, char next, const char *arg_str)
{
if (next == '\0') {
fprintf (stderr, "Option \"%s\" needs a string argument.\n", arg_desc->name);
return OPT_FAILED;
}
if (arg_str[0] == '\0' && ! arg_desc->allow_empty) {
fprintf (stderr, "String argument for option \"%s\" must be non-empty.\n", arg_desc->name);
return OPT_FAILED;
}
*arg_desc->opt_string = arg_str;
return OPT_OK;
}
/* Return number of non-NULL opt_* fields in opt_desc. */
static int
_opt_set_count (const notmuch_opt_desc_t *opt_desc)
{
return
(bool) opt_desc->opt_inherit +
(bool) opt_desc->opt_bool +
(bool) opt_desc->opt_int +
(bool) opt_desc->opt_keyword +
(bool) opt_desc->opt_flags +
(bool) opt_desc->opt_string +
(bool) opt_desc->opt_position;
}
/* Return true if opt_desc is valid. */
static bool
_opt_valid (const notmuch_opt_desc_t *opt_desc)
{
int n = _opt_set_count (opt_desc);
if (n > 1)
INTERNAL_ERROR ("more than one non-NULL opt_* field for argument \"%s\"",
opt_desc->name);
return n > 0;
}
/*
* Search for the {pos_arg_index}th position argument, return false if
* that does not exist.
*/
bool
parse_position_arg (const char *arg_str, int pos_arg_index,
const notmuch_opt_desc_t *arg_desc)
{
int pos_arg_counter = 0;
while (_opt_valid (arg_desc)) {
if (arg_desc->opt_position) {
if (pos_arg_counter == pos_arg_index) {
*arg_desc->opt_position = arg_str;
if (arg_desc->present)
*arg_desc->present = true;
return true;
}
pos_arg_counter++;
}
arg_desc++;
}
return false;
}
#define NEGATIVE_PREFIX "no-"
/*
* Search for a non-positional (i.e. starting with --) argument matching arg,
* parse a possible value, and assign to *output_var
*/
int
parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index)
{
assert (argv);
const char *_arg = argv[opt_index];
assert (_arg);
assert (options);
const char *arg = _arg + 2; /* _arg starts with -- */
const char *negative_arg = NULL;
/* See if this is a --no-argument */
if (strlen (arg) > strlen (NEGATIVE_PREFIX) &&
strncmp (arg, NEGATIVE_PREFIX, strlen (NEGATIVE_PREFIX)) == 0) {
negative_arg = arg + strlen (NEGATIVE_PREFIX);
}
const notmuch_opt_desc_t *try;
const char *next_arg = NULL;
if (opt_index < argc - 1 && strncmp (argv[opt_index + 1], "--", 2) != 0)
next_arg = argv[opt_index + 1];
for (try = options; _opt_valid (try); try++) {
if (try->opt_inherit) {
int new_index = parse_option (argc, argv, try->opt_inherit, opt_index);
if (new_index >= 0)
return new_index;
}
if (! try->name)
continue;
char next;
const char *value;
bool negate = false;
if (strncmp (arg, try->name, strlen (try->name)) == 0) {
next = arg[strlen (try->name)];
value = arg + strlen (try->name) + 1;
} else if (negative_arg && (try->opt_bool || try->opt_flags) &&
strncmp (negative_arg, try->name, strlen (try->name)) == 0) {
next = negative_arg[strlen (try->name)];
value = negative_arg + strlen (try->name) + 1;
/* The argument part of --no-argument matches, negate the result. */
negate = true;
} else {
continue;
}
/*
* If we have not reached the end of the argument (i.e. the
* next character is not a space or delimiter) then the
* argument could still match a longer option name later in
* the option table.
*/
if (next != '=' && next != ':' && next != '\0')
continue;
bool lookahead = (next == '\0' && next_arg != NULL && ! try->opt_bool);
if (lookahead) {
next = ' ';
value = next_arg;
opt_index++;
}
opt_handled opt_status = OPT_FAILED;
if (try->opt_keyword || try->opt_flags)
opt_status = _process_keyword_arg (try, next, value, negate);
else if (try->opt_bool)
opt_status = _process_boolean_arg (try, next, value, negate);
else if (try->opt_int)
opt_status = _process_int_arg (try, next, value);
else if (try->opt_string)
opt_status = _process_string_arg (try, next, value);
else
INTERNAL_ERROR ("unknown or unhandled option \"%s\"", try->name);
if (opt_status == OPT_FAILED)
return -1;
if (lookahead && opt_status == OPT_GIVEBACK)
opt_index--;
if (try->present)
*try->present = true;
return opt_index + 1;
}
return -1;
}
/* See command-line-arguments.h for description */
int
parse_arguments (int argc, char **argv,
const notmuch_opt_desc_t *options, int opt_index)
{
int pos_arg_index = 0;
bool more_args = true;
while (more_args && opt_index < argc) {
if (strncmp (argv[opt_index], "--", 2) != 0) {
more_args = parse_position_arg (argv[opt_index], pos_arg_index, options);
if (more_args) {
pos_arg_index++;
opt_index++;
}
} else {
int prev_opt_index = opt_index;
if (strlen (argv[opt_index]) == 2)
return opt_index + 1;
opt_index = parse_option (argc, argv, options, opt_index);
if (opt_index < 0) {
fprintf (stderr, "Unrecognized option: %s\n", argv[prev_opt_index]);
more_args = false;
}
}
}
return opt_index;
}

82
command-line-arguments.h Normal file
View file

@ -0,0 +1,82 @@
#ifndef NOTMUCH_OPTS_H
#define NOTMUCH_OPTS_H
#include <stdbool.h>
#include "notmuch.h"
/*
* Describe one of the possibilities for a keyword option
* 'value' will be copied to the output variable
*/
typedef struct notmuch_keyword {
const char *name;
int value;
} notmuch_keyword_t;
/* Describe one option. */
typedef struct notmuch_opt_desc {
/* One and only one of opt_* must be set. */
const struct notmuch_opt_desc *opt_inherit;
bool *opt_bool;
int *opt_int;
int *opt_keyword;
int *opt_flags;
const char **opt_string;
const char **opt_position;
/* for opt_keyword only: if no matching arguments were found, and
* keyword_no_arg_value is set, then use keyword_no_arg_value instead. */
const char *keyword_no_arg_value;
/* Must be set except for opt_inherit and opt_position. */
const char *name;
/* Optional, if non-NULL, set to true if the option is present. */
bool *present;
/* Optional, allow empty strings for opt_string. */
bool allow_empty;
/* Must be set for opt_keyword and opt_flags. */
const struct notmuch_keyword *keywords;
} notmuch_opt_desc_t;
/*
* This is the main entry point for command line argument parsing.
*
* Parse command line arguments according to structure options,
* starting at position opt_index.
*
* All output of parsed values is via pointers in options.
*
* Parsing stops at -- (consumed) or at the (k+1)st argument
* not starting with -- (a "positional argument") if options contains
* k positional argument descriptors.
*
* Returns the index of first non-parsed argument, or -1 in case of error.
*
*/
int
parse_arguments (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
/*
* If the argument parsing loop provided by parse_arguments is not
* flexible enough, then the user might be interested in the following
* routines, but note that the API to parse_option might have to
* change. See command-line-arguments.c for descriptions of these
* functions.
*/
int
parse_option (int argc, char **argv, const notmuch_opt_desc_t *options, int opt_index);
bool
parse_position_arg (const char *arg,
int position_arg_index,
const notmuch_opt_desc_t *options);
#endif

1
compat/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/zlib.pc

5
compat/Makefile Normal file
View file

@ -0,0 +1,5 @@
all:
$(MAKE) -C .. all
.DEFAULT:
$(MAKE) -C .. $@

24
compat/Makefile.local Normal file
View file

@ -0,0 +1,24 @@
# -*- makefile-gmake -*-
dir := compat
extra_cflags += -I$(srcdir)/$(dir)
notmuch_compat_srcs :=
ifneq ($(HAVE_GETLINE),1)
notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c
endif
ifneq ($(HAVE_STRCASESTR),1)
notmuch_compat_srcs += $(dir)/strcasestr.c
endif
ifneq ($(HAVE_STRSEP),1)
notmuch_compat_srcs += $(dir)/strsep.c
endif
ifneq ($(HAVE_TIMEGM),1)
notmuch_compat_srcs += $(dir)/timegm.c
endif
SRCS := $(SRCS) $(notmuch_compat_srcs)

21
compat/README Normal file
View file

@ -0,0 +1,21 @@
notmuch/compat
This directory consists of three things:
1. Small programs used by the notmuch configure script to test for the
availability of certain system features, (library functions, etc.).
For example: have_getline.c
2. Compatibility implementations of those system features for systems
that don't provide their own versions.
For example: getline.c
The compilation of these files is made conditional on the output of
the test programs from [1].
3. Macro definitions abstracting compiler differences (e.g. function
attributes).
For example: function-attributes.h

12
compat/check_asctime.c Normal file
View file

@ -0,0 +1,12 @@
#include <time.h>
#include <stdio.h>
int
main ()
{
struct tm tm;
(void) asctime_r (&tm, NULL);
return (0);
}

12
compat/check_getpwuid.c Normal file
View file

@ -0,0 +1,12 @@
#include <stdio.h>
#include <pwd.h>
int
main ()
{
struct passwd passwd, *ignored;
(void) getpwuid_r (0, &passwd, NULL, 0, &ignored);
return (0);
}

77
compat/compat.h Normal file
View file

@ -0,0 +1,77 @@
/* notmuch - Not much of an email library, (just index and search)
*
* Copyright © 2009 Carl Worth
*
* 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/ .
*
* Author: Carl Worth <cworth@cworth.org>
*/
/* This header file defines functions that will only be conditionally
* compiled for compatibility on systems that don't provide their own
* implementations of the functions.
*/
#ifndef NOTMUCH_COMPAT_H
#define NOTMUCH_COMPAT_H
#ifdef __cplusplus
extern "C" {
#endif
#if ! STD_GETPWUID
#define _POSIX_PTHREAD_SEMANTICS 1
#endif
#if ! STD_ASCTIME
#define _POSIX_PTHREAD_SEMANTICS 1
#endif
#if ! HAVE_GETLINE
#include <stdio.h>
#include <unistd.h>
ssize_t
getline (char **lineptr, size_t *n, FILE *stream);
ssize_t
getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
#endif /* !HAVE_GETLINE */
#if ! HAVE_STRCASESTR
char *strcasestr (const char *haystack, const char *needle);
#endif /* !HAVE_STRCASESTR */
#if ! HAVE_STRSEP
char *strsep (char **stringp, const char *delim);
#endif /* !HAVE_STRSEP */
#if ! HAVE_TIMEGM
#include <time.h>
time_t timegm (struct tm *tm);
#endif /* !HAVE_TIMEGM */
/* Silence gcc warnings about unused results. These warnings exist
* for a reason; any use of this needs to be justified. */
#ifdef __GNUC__
#define IGNORE_RESULT(x) ({ __typeof__(x) __z = (x); (void) (__z = __z); })
#else /* !__GNUC__ */
#define IGNORE_RESULT(x) x
#endif /* __GNUC__ */
#ifdef __cplusplus
}
#endif
#endif /* NOTMUCH_COMPAT_H */

View file

@ -0,0 +1,47 @@
/* function-attributes.h - Provides compiler abstractions for
* function attributes
*
* Copyright (c) 2012 Justus Winter <4winter@informatik.uni-hamburg.de>
*
* 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/ .
*/
#ifndef FUNCTION_ATTRIBUTES_H
#define FUNCTION_ATTRIBUTES_H
/* clang provides this macro to test for support for function
* attributes. If it isn't defined, this provides a compatibility
* macro for other compilers.
*/
#ifndef __has_attribute
#define __has_attribute(x) 0
#endif
/* Provide a NORETURN_ATTRIBUTE macro similar to PRINTF_ATTRIBUTE from
* talloc.
*
* This attribute is understood by gcc since version 2.5. clang
* provides support for testing for function attributes.
*/
#ifndef NORETURN_ATTRIBUTE
#if (__GNUC__ >= 3 || \
(__GNUC__ == 2 && __GNUC_MINOR__ >= 5) || \
__has_attribute (noreturn))
#define NORETURN_ATTRIBUTE __attribute__ ((noreturn))
#else
#define NORETURN_ATTRIBUTE
#endif
#endif
#endif

19
compat/gen_zlib_pc.c Normal file
View file

@ -0,0 +1,19 @@
#include <stdio.h>
#include <zlib.h>
static const char *template =
"prefix=/usr\n"
"exec_prefix=${prefix}\n"
"libdir=${exec_prefix}/lib\n"
"\n"
"Name: zlib\n"
"Description: zlib compression library\n"
"Version: %s\n"
"Libs: -lz\n";
int
main (void)
{
printf (template, ZLIB_VERSION);
return 0;
}

125
compat/getdelim.c Normal file
View file

@ -0,0 +1,125 @@
/* getdelim.c --- Implementation of replacement getdelim function.
* Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
* 2008, 2009 Free Software Foundation, Inc.
*
* 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, 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA. */
/* Ported from glibc by Simon Josefsson. */
#include "compat.h"
#include <stdio.h>
#include <limits.h>
#include <stdint.h>
#include <stdlib.h>
#include <errno.h>
#ifndef SSIZE_MAX
# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
#endif
#if USE_UNLOCKED_IO
# include "unlocked-io.h"
# define getc_maybe_unlocked(fp) getc (fp)
#elif ! HAVE_FLOCKFILE || ! HAVE_FUNLOCKFILE || ! HAVE_DECL_GETC_UNLOCKED
# undef flockfile
# undef funlockfile
# define flockfile(x) ((void) 0)
# define funlockfile(x) ((void) 0)
# define getc_maybe_unlocked(fp) getc (fp)
#else
# define getc_maybe_unlocked(fp) getc_unlocked (fp)
#endif
/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and
* NUL-terminate it). *LINEPTR is a pointer returned from malloc (or
* NULL), pointing to *N characters of space. It is realloc'ed as
* necessary. Returns the number of characters read (not including
* the null terminator), or -1 on error or EOF. */
ssize_t
getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
{
ssize_t result = -1;
size_t cur_len = 0;
if (lineptr == NULL || n == NULL || fp == NULL) {
errno = EINVAL;
return -1;
}
flockfile (fp);
if (*lineptr == NULL || *n == 0) {
char *new_lineptr;
*n = 120;
new_lineptr = (char *) realloc (*lineptr, *n);
if (new_lineptr == NULL) {
result = -1;
goto unlock_return;
}
*lineptr = new_lineptr;
}
for (;;) {
int i;
i = getc_maybe_unlocked (fp);
if (i == EOF) {
result = -1;
break;
}
/* Make enough space for len+1 (for final NUL) bytes. */
if (cur_len + 1 >= *n) {
size_t needed_max =
SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX;
size_t needed = 2 * *n + 1; /* Be generous. */
char *new_lineptr;
if (needed_max < needed)
needed = needed_max;
if (cur_len + 1 >= needed) {
result = -1;
errno = EOVERFLOW;
goto unlock_return;
}
new_lineptr = (char *) realloc (*lineptr, needed);
if (new_lineptr == NULL) {
result = -1;
goto unlock_return;
}
*lineptr = new_lineptr;
*n = needed;
}
(*lineptr)[cur_len] = i;
cur_len++;
if (i == delimiter)
break;
}
(*lineptr)[cur_len] = '\0';
result = cur_len ? (ssize_t) cur_len : result;
unlock_return:
funlockfile (fp); /* doesn't set errno */
return result;
}

29
compat/getline.c Normal file
View file

@ -0,0 +1,29 @@
/* getline.c --- Implementation of replacement getline function.
* Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
*
* 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, 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA. */
/* Written by Simon Josefsson. */
#include "compat.h"
#include <stdio.h>
ssize_t
getline (char **lineptr, size_t *n, FILE *stream)
{
return getdelim (lineptr, n, '\n', stream);
}

View file

@ -0,0 +1,11 @@
#define _GNU_SOURCE
#include <stdlib.h>
int
main ()
{
char *found;
char *string;
found = canonicalize_file_name (string);
}

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