Merge Sebastian Spaeth's python bindings into bindings/python

Sebastian offered to maintain these bindings within the notmuch
repository and offered them in the following repository:

	git://github.com/spaetz/python-notmuch.git

These are the bindings formerly known as "cnotmuch" and now known
simply as "notmuch" from within python.

The bindings are not yet integrated into the build system and
packaging of the primary ntomuch repository.
This commit is contained in:
Carl Worth 2010-04-21 17:29:48 -07:00
commit 8cbb5114a2
24 changed files with 4975 additions and 0 deletions

8
.hgignore Normal file
View file

@ -0,0 +1,8 @@
.*\.pyc$
~$
^MANIFEST
^docs/html
^docs/build
^test/test
^dist
^build

5
.hgtags Normal file
View file

@ -0,0 +1,5 @@
27f38f75d27d1dd5abbb1d5d9d5929372f84b2b2 v0.1
4fdbf2935cdf58865745d8bbc5e9b6be008ecb1c v0.1.1
36e2dd4e739a9da87837773467777d6d67e319f5 v0.2.0
8f496a1f9b3431609ba0e4bc5524d6179c6d7155 v0.2.1
0122a27667adbed8e246729fcdd82b4e4e9ae420 v0.2.2

View file

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

87
bindings/python/README Normal file
View file

@ -0,0 +1,87 @@
notmuch -- The python interface to notmuch.so
==============================================
This module makes the functionality of the notmuch library
(`http://notmuchmail.org`_) available to python. Successful import of
this modul 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:
http://packages.python.org/cnotmuch/
The current source code is being hosted at
http://bitbucket.org/spaetz/cnotmuch which also provides an issue
tracker, and release downloads. This package is tracked by the python
package index repository at `http://pypi.python.org/pypi/cnotmuch`_ and can thus be installed on a user's computer easily via "sudo easy_install cnotmuch" (you will still need to install the notmuch shared library separately as it is not included in this package).
The original source has been provided by (c)Sebastian Spaeth, 2010.
All code is available under the GNU GPLv3+ (see docs/COPYING) unless specified otherwise.
INSTALLATION & DEINSTALL
------------------------
cnotmuch is available on pypi.python.org. This means you can do
"easy_install cnotmuch" on your linux box and it will get installed
into:
/usr/local/lib/python2.x/dist-packages/
For uninstalling, you'll need to remove the "cnotmuch-0.1-py2.x.egg"
directory and delete one entry in the "easy-install.pth" file in that
directory.
It needs to have a libnotmuch.so or libnotmuch.so.1 available in some
library folder or will raise an exception when loading.
"OSError: libnotmuch.so.1: cannot open shared object file: No such file or directory"
Usage
-----
For more examples of how to use the cnotmuch interface, have a look at the
notmuch "binary" and the generated documentation.
Example session:
>>>import notmuch
>>>db = notmuch.Database("/home/spaetz/mail")
db.get_path()
'/home/spaetz/mail'
>>>tags = db.get_all_tags()
>>>for tag in tags:
>>> print tag
inbox
...
maildir::draft
#---------------------------------------------
q = notmuch.Query(db,'from:Sebastian')
count = len(q.search_messages())
1300
#---------------------------------------------
>>>db = notmuch.Database("/home/spaetz/mailHAHA")
NotmuchError: Could not open the specified database
#---------------------------------------------
>>>tags = notmuch.Database("/home/spaetz/mail").get_all_tags()
>>>del(tags)
Building for a Debian package
------------------------------
dpkg-buildpackage -i"\.hg|\/build"
Changelog
---------
0.1 First public release
0.1.1 Fixed Database.create_query()
0.2.0 Implemented Thread() and Threads() methods
0.2.1 Implemented the remaining API methods, notably Directory() and Filenames()
0.2.2 Bug fixes
0.3.0 Incorporated in the notmuchmail.org git repository

View file

@ -0,0 +1,5 @@
cnotmuch (0.2.1-1) karmic; urgency=low
* Initial release
-- Sebastian Spaeth <Sebastian@SSpaeth.de> Tue, 30 Mar 2010 11:31:39 +0200

View file

@ -0,0 +1 @@
7

View file

@ -0,0 +1,18 @@
Source: cnotmuch
Section: python
Priority: extra
Maintainer: Sebastian Spaeth <Sebastian@SSpaeth.de>
Build-Depends: debhelper (>= 7.3.0), python-support (>= 0.5.3)
Build-Depends-Indep: python (>= 2.5), python-support
Standards-Version: 3.8.1
Homepage: http://pypi.python.org/pypi/cnotmuch
Package: cnotmuch
Architecture: all
XB-Python-Version: ${python:Versions}
Depends: ${misc:Depends},${python:Depends}, notmuch (> 0.0+201001211401)
Description: Interface to the notmuch mail search and index library
The cnotmuch module provides an interface to the notmuch 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.

View file

@ -0,0 +1,42 @@
This package was debianized by:
Sebastian Spaeth <Sebastian@SSpaeth.de> on Tue, 30 Mar 2010 10:02:22 +0200
It was downloaded from:
http://pypi.python.org/packages/source/c/cnotmuch/cnotmuch-0.2.1.tar.gz
Upstream Author(s):
Sebastian Spaeth <Sebastian@SSpaeth.de>
Copyright:
Copyright (C) 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
License:
This package 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 2 of the License, or
(at your option) any later version.
This package 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 package; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
On Debian systems, the complete text of the GNU General
Public License version 2 can be found in `/usr/share/common-licenses/GPL-2'.
The Debian packaging is:
Copyright (C) 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
and is licensed under the GPL version 3,
see `/usr/share/common-licenses/GPL-3'.

View file

@ -0,0 +1 @@
README

4
bindings/python/debian/rules Executable file
View file

@ -0,0 +1,4 @@
#!/usr/bin/make -f
%:
dh $@

View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://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 <http://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
<http://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
<http://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,200 @@
# -*- 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
# 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('../..'))
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'cnotmuch'
copyright = u'2010, ' + __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 = ['../html']
# 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 = {'http://docs.python.org/': None}

View file

@ -0,0 +1,276 @@
.. notmuch documentation master file, created by
sphinx-quickstart on Tue Feb 2 10:00:47 2010.
.. currentmodule:: notmuch
Welcome to :mod:`notmuch`'s documentation
===========================================
The :mod:`notmuch` module provides an interface to the `notmuch <http://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).
This page contains the main API overview of notmuch |release|.
Notmuch can be imported as::
import notmuch
or::
from notmuch import Query,Database
More information on specific topics can be found on the following pages:
.. toctree::
:maxdepth: 1
notmuch
:mod:`notmuch` -- The Notmuch interface
=================================================
.. automodule:: notmuch
:todo: Document nmlib,STATUS
:class:`notmuch.Database` -- The underlying notmuch database
---------------------------------------------------------------------
.. autoclass:: notmuch.Database([path=None[, create=False[, mode=MODE.READ_ONLY]]])
.. automethod:: create
.. automethod:: open(path, status=MODE.READ_ONLY)
.. automethod:: get_path
.. automethod:: get_version
.. automethod:: needs_upgrade
.. automethod:: upgrade
.. automethod:: get_directory
.. automethod:: add_message
.. automethod:: remove_message
.. automethod:: find_message
.. automethod:: get_all_tags
.. automethod:: create_query
.. note:: :meth:`create_query` was broken in release
0.1 and is fixed since 0.1.1.
.. 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
.. autoattribute:: db_p
:class:`notmuch.Query` -- A search query
-------------------------------------------------
.. autoclass:: notmuch.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
.. automethod:: set_sort
.. attribute:: sort
Instance attribute :attr:`sort` contains the sort order (see
:attr:`Query.SORT`) if explicitely specified via
:meth:`set_sort`. By default it is set to `None`.
.. automethod:: search_threads
.. automethod:: search_messages
.. automethod:: count_messages
:class:`Messages` -- A bunch of messages
----------------------------------------
.. autoclass:: Messages
.. automethod:: collect_tags
.. automethod:: __len__
:class:`Message` -- A single message
----------------------------------------
.. autoclass:: Message
.. automethod:: get_message_id
.. automethod:: get_thread_id
.. automethod:: get_replies
.. automethod:: get_filename
.. 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:: remove_tag
.. automethod:: add_tag
.. automethod:: remove_all_tags
.. automethod:: freeze
.. automethod:: thaw
.. automethod:: format_as_text
.. automethod:: __str__
:class:`Tags` -- Notmuch tags
-----------------------------
.. autoclass:: Tags
:members:
.. automethod:: __len__
.. automethod:: __str__
:class:`notmuch.Threads` -- Threads iterator
-----------------------------------------------------
.. autoclass:: notmuch.Threads
.. automethod:: __len__
.. automethod:: __str__
:class:`Thread` -- A single thread
------------------------------------
.. 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__
:class:`Filenames` -- An iterator over filenames
------------------------------------------------
.. autoclass:: notmuch.database.Filenames
.. automethod:: notmuch.database.Filenames.__len__
:class:`notmuch.database.Directoy` -- A directory entry in the database
------------------------------------------------------------------------
.. autoclass:: notmuch.database.Directory
.. automethod:: notmuch.database.Directory.get_child_files
.. automethod:: notmuch.database.Directory.get_child_directories
.. automethod:: notmuch.database.Directory.get_mtime
.. automethod:: notmuch.database.Directory.set_mtime
.. autoattribute:: notmuch.database.Directory.mtime
.. autoattribute:: notmuch.database.Directory.path
:exc:`NotmuchError` -- A Notmuch execution error
------------------------------------------------
.. autoexception:: NotmuchError
:members:
This execption inherits directly from :exc:`Exception` and is raised on errors during the notmuch execution.
:class:`STATUS` -- Notmuch operation return status
--------------------------------------------------
.. data:: STATUS
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
* NOT_INITIALIZED
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

View file

@ -0,0 +1,68 @@
The notmuch 'binary'
====================
The cnotmuch module provides *notmuch*, a python reimplementation of the standard notmuch binary for two purposes: first, to allow running the standard notmuch testsuite over the cnotmuch bindings (for correctness and performance testing) and second, to give some examples as to how to use cnotmuch. 'Notmuch' provides a command line interface to your mail database.
A standard install via `easy_install cnotmuch` will not install the notmuch binary, however it is available in the `cnotmuch source code repository <http://bitbucket.org/spaetz/cnotmuch/src/>`_.
It is invoked with the following pattern: `notmuch <command> [args...]`.
Where <command> and [args...] are as follows:
**setup** Interactively setup notmuch for first use.
This has not yet been implemented, and will probably not be
implemented unless someone puts in the effort.
**new** [--verbose]
Find and import new messages to the notmuch database.
This has not been implemented yet. We cheat by calling
the regular "notmuch" binary (which must be in your path
somewhere).
**search** [options...] <search-terms> [...] Search for messages matching the given search terms.
This has been implemented but for the `--format` and
`--sort` options.
**show** <search-terms> [...]
Show all messages matching the search terms.
This has been partially implemented, we show a stub for each
found message, but do not output the full message body yet.
**count** <search-terms> [...]
Count messages matching the search terms.
This has been fully implemented.
**reply** [options...] <search-terms> [...]
Construct a reply template for a set of messages.
This has not been implemented yet.
**tag** +<tag>|-<tag> [...] [--] <search-terms> [...]
Add/remove tags for all messages matching the search terms.
This has been fully implemented.
**dump** [<filename>]
Create a plain-text dump of the tags for each message.
This has been fully implemented.
**restore** <filename>
Restore the tags from the given dump file (see 'dump').
This has been fully implemented.
**search-tags** [<search-terms> [...] ]
List all tags found in the database or matching messages.
This has been fully implemented.
**help** [<command>]
This message, or more detailed help for the named command.
The 'help' page has been implemented, help for single
commands are missing though. Patches are welcome.

647
bindings/python/notmuch.py Executable file
View file

@ -0,0 +1,647 @@
#!/usr/bin/env python
"""This is a notmuch implementation in python.
It's goal is to allow running the test suite on the cnotmuch python bindings.
This "binary" honors the NOTMUCH_CONFIG environmen variable for reading a user's
notmuch configuration (e.g. the database path).
(c) 2010 by Sebastian Spaeth <Sebastian@SSpaeth.de>
Jesse Rosenthal <jrosenthal@jhu.edu>
This code is licensed under the GNU GPL v3+.
"""
import sys
import os
import re
import stat
import email
from notmuch import Database, Query, NotmuchError, STATUS
from ConfigParser import SafeConfigParser
from cStringIO import StringIO
PREFIX = re.compile('(\w+):(.*$)')
HELPTEXT = """The notmuch mail system.
Usage: notmuch <command> [args...]
Where <command> and [args...] are as follows:
setup Interactively setup notmuch for first use.
new [--verbose]
Find and import new messages to the notmuch database.
search [options...] <search-terms> [...]
Search for messages matching the given search terms.
show <search-terms> [...]
Show all messages matching the search terms.
count <search-terms> [...]
Count messages matching the search terms.
reply [options...] <search-terms> [...]
Construct a reply template for a set of messages.
tag +<tag>|-<tag> [...] [--] <search-terms> [...]
Add/remove tags for all messages matching the search terms.
dump [<filename>]
Create a plain-text dump of the tags for each message.
restore <filename>
Restore the tags from the given dump file (see 'dump').
search-tags [<search-terms> [...] ]
List all tags found in the database or matching messages.
help [<command>]
This message, or more detailed help for the named command.
Use "notmuch help <command>" for more details on each command.
And "notmuch help search-terms" for the common search-terms syntax.
"""
USAGE = """Notmuch is configured and appears to have a database. Excellent!
At this point you can start exploring the functionality of notmuch by
using commands such as:
notmuch search tag:inbox
notmuch search to:"%(fullname)s"
notmuch search from:"%(mailaddress)s"
notmuch search subject:"my favorite things"
See "notmuch help search" for more details.
You can also use "notmuch show" with any of the thread IDs resulting
from a search. Finally, you may want to explore using a more sophisticated
interface to notmuch such as the emacs interface implemented in notmuch.el
or any other interface described at http://notmuchmail.org
And don't forget to run "notmuch new" whenever new mail arrives.
Have fun, and may your inbox never have much mail.
"""
#-------------------------------------------------------------------------
def quote_query_line(argv):
# mangle arguments wrapping terms with spaces in quotes
for (num, item) in enumerate(argv):
if item.find(' ') >= 0:
# if we use prefix:termWithSpaces, put quotes around term
match = PREFIX.match(item)
if match:
argv[num] = '%s:"%s"' %(match.group(1), match.group(2))
else:
argv[num] = '"%s"' % item
return ' '.join(argv)
#-------------------------------------------------------------------------
class Notmuch(object):
def __init__(self, configpath="~/.notmuch-config)"):
self._config = None
self._configpath = os.getenv('NOTMUCH_CONFIG',
os.path.expanduser(configpath))
def cmd_usage(self):
"""Print the usage text and exits"""
data={}
names = self.get_user_email_addresses()
data['fullname'] = names[0] if names[0] else 'My Name'
data['mailaddress'] = names[1] if names[1] else 'My@email.address'
print USAGE % data
def cmd_new(self):
"""Run 'notmuch new'"""
#get the database directory
db = Database(mode=Database.MODE.READ_WRITE)
path = db.get_path()
print self._add_new_files_recursively(path, db)
def cmd_help(self, subcmd=None):
"""Print help text for 'notmuch help'"""
if len(subcmd) > 1:
print "Help for specific commands not implemented"
return
print HELPTEXT
def _get_user_notmuch_config(self):
"""Returns the ConfigParser of the user's notmuch-config"""
# return the cached config parser if we read it already
if self._config:
return self._config
config = SafeConfigParser()
config.read(self._configpath)
self._config = config
return config
def _add_new_files_recursively(self, path, db):
""":returns: (added, moved, removed)"""
print "Enter add new files with path %s" % path
try:
#get the Directory() object for this path
db_dir = db.get_directory(path)
added = moved = removed = 0
except NotmuchError:
# Occurs if we have wrong absolute paths in the db, for example
return (0,0,0)
# for folder in subdirs:
# TODO, retrieve dir mtime here and store it later
# as long as Filenames() does not allow multiple iteration, we need to
# use this kludgy way to get a sorted list of filenames
# db_files is a list of subdirectories and filenames in this folder
db_files = set()
db_folders = set()
for subdir in db_dir.get_child_directories():
db_folders.add(subdir)
# file is a keyword (remove this ;))
for mail in db_dir.get_child_files():
db_files.add(mail)
fs_files = set(os.listdir(db_dir.path))
# list of files (and folders) on the fs, but not the db
for fs_file in ((fs_files - db_files) - db_folders):
absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
statinfo = os.stat(absfile)
if stat.S_ISDIR(statinfo.st_mode):
# This is a directory
if fs_file in ['.notmuch','tmp','.']:
continue
print "%s %s" % (fs_file, db_folders)
print "Directory not in db yet. Descending into %s" % absfile
new = self._add_new_files_recursively(absfile, db)
added += new[0]
moved += new[1]
removed += new[2]
elif stat.S_ISLNK(statinfo.st_mode):
print ("%s is a symbolic link (%d). FIXME!!!" %
(absfile, statinfo.st_mode))
exit(1)
else:
# This is a regular file, not in the db yet. Add it.
print "This file needs to be added %s" % (absfile)
(msg, status) = db.add_message(absfile)
# We increases 'added', even on dupe messages. If it is a moved
# message, we will deduct it later and increase 'moved' instead
added += 1
if status == STATUS.DUPLICATE_MESSAGE_ID:
print "Added msg was in the db"
else:
print "New message."
# Finally a list of files (not folders) in the database,
# but not the filesystem
for db_file in (db_files - fs_files):
absfile = os.path.normpath(os.path.join(db_dir.path, db_file))
# remove a mail message from the db
print ("%s is not on the fs anymore. Delete" % absfile)
status = db.remove_message(absfile)
if status == STATUS.SUCCESS:
# we just deleted the last reference, so this was a remove
removed += 1
sys.stderr.write("SUCCESS %d %s %s.\n" %
(status, STATUS.status2str(status), absfile))
elif status == STATUS.DUPLICATE_MESSAGE_ID:
# The filename exists already somewhere else, so this is a move
moved += 1
added -= 1
sys.stderr.write("DUPE %d %s %s.\n" %
(status, STATUS.status2str(status), absfile))
else:
# This should not occur
sys.stderr.write("This should not occur %d %s %s.\n" %
(status, STATUS.status2str(status), absfile))
# list of folders in the filesystem. Just descend into dirs
for fs_file in fs_files:
absfile = os.path.normpath(os.path.join(db_dir.path, fs_file))
if os.path.isdir(absfile):
# This is a directory. Remove it from the db_folder list.
# All remaining db_folders at the end will be not present
# on the file system.
db_folders.remove(fs_file)
if fs_file in ['.notmuch','tmp','.']:
continue
new = self._add_new_files_recursively(absfile, db)
added += new[0]
moved += new[0]
removed += new[0]
# we are not interested in anything but directories here
#TODO: All remaining elements of db_folders are not in the filesystem
#delete those
return added, moved, removed
#Read the mtime of a directory from the filesystem
#
#* Call :meth:`Database.add_message` for all mail files in
# the directory
#* Call notmuch_directory_set_mtime with the mtime read from the
# filesystem. Then, when wanting to check for updates to the
# directory in the future, the client can call :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.
def get_user_email_addresses(self):
""" Reads a user's notmuch config and returns his email addresses as
list (name, primary_address, other_address1,...)"""
#read the config file
config = self._get_user_notmuch_config()
conf = {'name': '', 'primary_email': ''}
for entry in conf:
if config.has_option('user', entry):
conf[entry] = config.get('user', entry)
if config.has_option('user','other_email'):
other = config.get('user','other_email')
other = [mail.strip() for mail in other.split(';') if mail]
else:
other = []
# for being compatible. It would be nicer to return a dict.
return conf.keys() + other
def quote_msg_body(self, oldbody ,date, from_address):
"""Transform a mail body into a quoted text,
starting with On foo, bar wrote:
:param body: a str with a mail body
:returns: The new payload of the email.message()
"""
# we get handed a string, wrap it in a file-like object
oldbody = StringIO(oldbody)
newbody = StringIO()
newbody.write("On %s, %s wrote:\n" % (date, from_address))
for line in oldbody:
newbody.write("> " + line)
return newbody.getvalue()
def format_reply(self, msgs):
"""Gets handed Messages() and displays the reply to them
This is pretty ugly and hacky. It tries to mimic the "real"
notmuch output as much as it can to pass the test suite. It
could deserve a healthy bit of love. It is also buggy because
it returns after the first message it has handled."""
for msg in msgs:
f = open(msg.get_filename(), "r")
reply = email.message_from_file(f)
# handle the easy non-multipart case:
if not reply.is_multipart():
reply.set_payload(self.quote_msg_body(reply.get_payload(),
reply['date'], reply['from']))
else:
# handle the tricky multipart case
deleted = ""
"""A string describing which nontext attachements
that have been deleted"""
delpayloads = []
"""A list of payload indices to be deleted"""
payloads = reply.get_payload()
for (num, part) in enumerate(payloads):
mime_main = part.get_content_maintype()
if mime_main not in ['multipart', 'message', 'text']:
deleted += "Non-text part: %s\n" % (part.get_content_type())
payloads[num].set_payload("Non-text part: %s" %
(part.get_content_type()))
payloads[num].set_type('text/plain')
delpayloads.append(num)
elif mime_main == 'text':
payloads[num].set_payload(self.quote_msg_body(
payloads[num].get_payload(),
reply['date'], reply['from']))
else:
# TODO handle deeply nested multipart messages
sys.stderr.write ("FIXME: Ignoring multipart part. Handle me\n")
# Delete those payloads that we don't need anymore
for item in reversed(sorted(delpayloads)):
del payloads[item]
# Back to single- and multipart handling
my_addresses = self.get_user_email_addresses()
used_address = None
# filter our email addresses from all to: cc: and bcc: fields
# if we find one of "my" addresses being used,
# it is stored in used_address
for header in ['To', 'CC', 'Bcc']:
if not header in reply:
#only handle fields that exist
continue
addresses = email.utils.getaddresses(reply.get_all(header, []))
purged_addr = []
for (name, mail) in addresses:
if mail in my_addresses[1:]:
used_address = email.utils.formataddr(
(my_addresses[0], mail))
else:
purged_addr.append(email.utils.formataddr((name, mail)))
if purged_addr:
reply.replace_header(header, ", ".join(purged_addr))
else:
# we deleted all addresses, delete the header
del reply[header]
# Use our primary email address to the From
# (save original from line, we still need it)
new_to = reply['From']
if used_address:
reply['From'] = used_address
else:
email.utils.formataddr((my_addresses[0], my_addresses[1]))
reply['Subject'] = 'Re: ' + reply['Subject']
# Calculate our new To: field
# add all remaining original 'To' addresses
if 'To' in reply:
new_to += ", " + reply['To']
reply.add_header('To', new_to)
# Add our primary email address to the BCC
new_bcc = my_addresses[1]
if 'Bcc' in reply:
new_bcc += ', ' + reply['Bcc']
reply['Bcc'] = new_bcc
# Set replies 'In-Reply-To' header to original's Message-ID
if 'Message-ID' in reply:
reply['In-Reply-To'] = reply['Message-ID']
#Add original's Message-ID to replies 'References' header.
if 'References' in reply:
reply['References'] = ' '.join([reply['References'], reply['Message-ID']])
else:
reply['References'] = reply['Message-ID']
# Delete the original Message-ID.
del(reply['Message-ID'])
# filter all existing headers but a few and delete them from 'reply'
delheaders = filter(lambda x: x not in ['From', 'To', 'Subject', 'CC',
'Bcc', 'In-Reply-To',
'References', 'Content-Type'],
reply.keys())
map(reply.__delitem__, delheaders)
# TODO: OUCH, we return after the first msg we have handled rather than
# handle all of them
# return resulting message without Unixfrom
return reply.as_string(False)
def main():
# Handle command line options
#------------------------------------
# No option given, print USAGE and exit
if len(sys.argv) == 1:
Notmuch().cmd_usage()
#------------------------------------
elif sys.argv[1] == 'setup':
"""Interactively setup notmuch for first use."""
exit("Not implemented.")
#-------------------------------------
elif sys.argv[1] == 'new':
"""Check for new and removed messages."""
Notmuch().cmd_new()
#-------------------------------------
elif sys.argv[1] == 'help':
"""Print the help text"""
Notmuch().cmd_help(sys.argv[1:])
#-------------------------------------
elif sys.argv[1] == 'part':
part()
#-------------------------------------
elif sys.argv[1] == 'search':
search()
#-------------------------------------
elif sys.argv[1] == 'show':
show()
#-------------------------------------
elif sys.argv[1] == 'reply':
db = Database()
if len(sys.argv) == 2:
# no search term. abort
exit("Error: notmuch reply requires at least one search term.")
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[2:])
msgs = Query(db, querystr).search_messages()
print Notmuch().format_reply(msgs)
#-------------------------------------
elif sys.argv[1] == 'count':
if len(sys.argv) == 2:
# no further search term, count all
querystr = ''
else:
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[2:])
print Database().create_query(querystr).count_messages()
#-------------------------------------
elif sys.argv[1] == 'tag':
# build lists of tags to be added and removed
add = []
remove = []
while not sys.argv[2] == '--' and \
(sys.argv[2].startswith('+') or sys.argv[2].startswith('-')):
if sys.argv[2].startswith('+'):
# append to add list without initial +
add.append(sys.argv.pop(2)[1:])
else:
# append to remove list without initial -
remove.append(sys.argv.pop(2)[1:])
# skip eventual '--'
if sys.argv[2] == '--': sys.argv.pop(2)
# the rest is search terms
querystr = quote_query_line(sys.argv[2:])
db = Database(mode=Database.MODE.READ_WRITE)
msgs = Query(db, querystr).search_messages()
for msg in msgs:
# actually add and remove all tags
map(msg.add_tag, add)
map(msg.remove_tag, remove)
#-------------------------------------
elif sys.argv[1] == 'search-tags':
if len(sys.argv) == 2:
# no further search term
print "\n".join(Database().get_all_tags())
else:
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[2:])
db = Database()
msgs = Query(db, querystr).search_messages()
print "\n".join([t for t in msgs.collect_tags()])
#-------------------------------------
elif sys.argv[1] == 'dump':
# TODO: implement "dump <filename>"
if len(sys.argv) == 2:
f = sys.stdout
else:
f = open(sys.argv[2], "w")
db = Database()
query = Query(db, '')
query.set_sort(Query.SORT.MESSAGE_ID)
msgs = query.search_messages()
for msg in msgs:
f.write("%s (%s)\n" % (msg.get_message_id(), msg.get_tags()))
#-------------------------------------
elif sys.argv[1] == 'restore':
if len(sys.argv) == 2:
print("No filename given. Reading dump from stdin.")
f = sys.stdin
else:
f = open(sys.argv[2], "r")
# split the msg id and the tags
MSGID_TAGS = re.compile("(\S+)\s\((.*)\)$")
db = Database(mode=Database.MODE.READ_WRITE)
#read each line of the dump file
for line in f:
msgs = MSGID_TAGS.match(line)
if not msgs:
sys.stderr.write("Warning: Ignoring invalid input line: %s" %
line)
continue
# split line in components and fetch message
msg_id = msgs.group(1)
new_tags = set(msgs.group(2).split())
msg = db.find_message(msg_id)
if msg == None:
sys.stderr.write(
"Warning: Cannot apply tags to missing message: %s\n" % msg_id)
continue
# do nothing if the old set of tags is the same as the new one
old_tags = set(msg.get_tags())
if old_tags == new_tags: continue
# set the new tags
msg.freeze()
# only remove tags if the new ones are not a superset anyway
if not (new_tags > old_tags): msg.remove_all_tags()
for tag in new_tags: msg.add_tag(tag)
msg.thaw()
#-------------------------------------
else:
# unknown command
exit("Error: Unknown command '%s' (see \"notmuch help\")" % sys.argv[1])
def part():
db = Database()
query_string = ''
part_num = 0
first_search_term = 0
for (num, arg) in enumerate(sys.argv[1:]):
if arg.startswith('--part='):
part_num_str = arg.split("=")[1]
try:
part_num = int(part_num_str)
except ValueError:
# just emulating behavior
exit(1)
elif not arg.startswith('--'):
# save the position of the first sys.argv
# that is a search term
first_search_term = num + 1
if first_search_term:
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[first_search_term:])
qry = Query(db,querystr)
msgs = [msg for msg in qry.search_messages()]
if not msgs:
sys.exit(1)
elif len(msgs) > 1:
raise Exception("search term did not match precisely one message")
else:
msg = msgs[0]
print msg.get_part(part_num)
def search():
db = Database()
query_string = ''
sort_order = "newest-first"
first_search_term = 0
for (num, arg) in enumerate(sys.argv[1:]):
if arg.startswith('--sort='):
sort_order=arg.split("=")[1]
if not sort_order in ("oldest-first", "newest-first"):
raise Exception("unknown sort order")
elif not arg.startswith('--'):
# save the position of the first sys.argv that is a search term
first_search_term = num + 1
if first_search_term:
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[first_search_term:])
qry = Query(db, querystr)
if sort_order == "oldest-first":
qry.set_sort(Query.SORT.OLDEST_FIRST)
else:
qry.set_sort(Query.SORT.NEWEST_FIRST)
threads = qry.search_threads()
for thread in threads:
print thread
def show():
entire_thread = False
db = Database()
out_format = "text"
querystr = ''
first_search_term = None
# ugly homegrown option parsing
# TODO: use OptionParser
for (num, arg) in enumerate(sys.argv[1:]):
if arg == '--entire-thread':
entire_thread = True
elif arg.startswith("--format="):
out_format = arg.split("=")[1]
if out_format == 'json':
# for compatibility use --entire-thread for json
entire_thread = True
if not out_format in ("json", "text"):
raise Exception("unknown format")
elif not arg.startswith('--'):
# save the position of the first sys.argv that is a search term
first_search_term = num + 1
if first_search_term:
# mangle arguments wrapping terms with spaces in quotes
querystr = quote_query_line(sys.argv[first_search_term:])
threads = Query(db, querystr).search_threads()
first_toplevel = True
if out_format == "json":
sys.stdout.write("[")
for thread in threads:
msgs = thread.get_toplevel_messages()
if not first_toplevel:
if out_format == "json":
sys.stdout.write(", ")
first_toplevel = False
msgs.print_messages(out_format, 0, entire_thread)
if out_format == "json":
sys.stdout.write("]")
sys.stdout.write("\n")
if __name__ == '__main__':
main()

View file

@ -0,0 +1,61 @@
"""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 a :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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
"""
from database import Database, Query
from message import Messages, Message
from thread import Threads, Thread
from tag import Tags
from notmuch.globals import nmlib, STATUS, NotmuchError
__LICENSE__="GPL v3+"
__VERSION__='0.2.2'
__AUTHOR__ ='Sebastian Spaeth <Sebastian@SSpaeth.de>'

View file

@ -0,0 +1,841 @@
"""
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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
"""
import os
from ctypes import c_int, c_char_p, c_void_p, c_uint, c_long, byref
from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
from notmuch.thread import Threads
from notmuch.message import Messages, Message
from notmuch.tag import Tags
class Database(object):
"""Represents a notmuch database (wraps notmuch_database_t)
.. note:: Do remember that as soon as we tear down this object,
all underlying derived objects such as queries, 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.
"""
_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"""
"""notmuch_database_get_directory"""
_get_directory = nmlib.notmuch_database_get_directory
_get_directory.restype = c_void_p
"""notmuch_database_get_path"""
_get_path = nmlib.notmuch_database_get_path
_get_path.restype = c_char_p
"""notmuch_database_get_version"""
_get_version = nmlib.notmuch_database_get_version
_get_version.restype = c_uint
"""notmuch_database_open"""
_open = nmlib.notmuch_database_open
_open.restype = c_void_p
"""notmuch_database_upgrade"""
_upgrade = nmlib.notmuch_database_upgrade
_upgrade.argtypes = [c_void_p, c_void_p, c_void_p]
""" notmuch_database_find_message"""
_find_message = nmlib.notmuch_database_find_message
_find_message.restype = c_void_p
"""notmuch_database_get_all_tags"""
_get_all_tags = nmlib.notmuch_database_get_all_tags
_get_all_tags.restype = c_void_p
"""notmuch_database_create"""
_create = nmlib.notmuch_database_create
_create.restype = c_void_p
def __init__(self, path=None, create=False, mode= 0):
"""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`
:returns: Nothing
:exception: :exc:`NotmuchError` in case of failure.
"""
self._db = None
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)
def _verify_initialized_db(self):
"""Raises a NotmuchError in case self._db is still None"""
if self._db is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
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
:returns: Nothing
:exception: :exc:`NotmuchError` in case of any failure
(after printing an error message on stderr).
"""
if self._db is not None:
raise NotmuchError(
message="Cannot create db, this Database() already has an open one.")
res = Database._create(path, Database.MODE.READ_WRITE)
if res is None:
raise NotmuchError(
message="Could not create the specified database")
self._db = res
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`
:returns: Nothing
:exception: Raises :exc:`NotmuchError` in case
of any failure (after printing an error message on stderr).
"""
res = Database._open(path, mode)
if res is None:
raise NotmuchError(
message="Could not open the specified database")
self._db = res
def get_path(self):
"""Returns the file path of an open database
Wraps *notmuch_database_get_path*."""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
return Database._get_path(self._db)
def get_version(self):
"""Returns the database format version
:returns: The database version as positive integer
:exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
the database was not intitialized.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
return Database._get_version (self._db)
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:`add_message`,
:meth:`Message.add_tag`, :meth:`Directory.set_mtime`,
etc.) will work unless :meth:`upgrade` is called successfully first.
:returns: `True` or `False`
:exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
the database was not intitialized.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
return notmuch_database_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...
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
status = Database._upgrade (self._db, None, None)
#TODO: catch exceptions, document return values and etc
return status
def get_directory(self, path):
"""Returns a :class:`Directory` of path,
(creating it if it does not exist(?))
.. warning:: This call needs a writeable database in
Database.MODE.READ_WRITE mode. The underlying library will exit the
program if this method is used on a read-only database!
:param path: A str 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.
:exception: :exc:`NotmuchError`
STATUS.NOT_INITIALIZED
If the database was not intitialized.
STATUS.FILE_ERROR
If path is not relative database or absolute with initial
components same as database.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
# sanity checking if path is valid, and make path absolute
if 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 NotmuchError(STATUS.FILE_ERROR,
message="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 = Database._get_directory(self._db, path);
# return the Directory, init it with the absolute path
return Directory(abs_dirpath, dir_p, self)
def add_message(self, filename):
"""Adds a new message to the database
`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.
: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 STATUS values:
STATUS.SUCCESS
Message successfully added to database.
STATUS.DUPLICATE_MESSAGE_ID
Message has the same message ID as another message already
in the database. The new filename was successfully added
to the message in the database.
:rtype: 2-tuple(:class:`Message`, STATUS)
:exception: Raises a :exc:`NotmuchError` with the following meaning.
If such an exception occurs, nothing was added to the database.
STATUS.FILE_ERROR
An error occurred trying to open the file, (such as
permission denied, or file not found, etc.).
STATUS.FILE_NOT_EMAIL
The contents of filename don't look like an email message.
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so no message can
be added.
STATUS.NOT_INITIALIZED
The database has not been initialized.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
msg_p = c_void_p()
status = nmlib.notmuch_database_add_message(self._db,
filename,
byref(msg_p))
if not status in [STATUS.SUCCESS,STATUS.DUPLICATE_MESSAGE_ID]:
raise NotmuchError(status)
#construct Message() and return
msg = Message(msg_p, self)
return (msg, status)
def remove_message(self, filename):
"""Removes a message 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 STATUS value with the following meaning:
STATUS.SUCCESS
The last filename was removed and the message was removed
from the database.
STATUS.DUPLICATE_MESSAGE_ID
This filename was removed but the message persists in the
database with at least one other filename.
:exception: Raises a :exc:`NotmuchError` with the following meaning.
If such an exception occurs, nothing was removed from the database.
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so no message can be
removed.
STATUS.NOT_INITIALIZED
The database has not been initialized.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
return nmlib.notmuch_database_remove_message(self._db,
filename)
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: string
:returns: :class:`Message` or `None` if no message is found or if an
out-of-memory situation occurs.
:exception: :exc:`NotmuchError` with STATUS.NOT_INITIALIZED if
the database was not intitialized.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
msg_p = Database._find_message(self._db, msgid)
if msg_p is None:
return None
return Message(msg_p, self)
def get_all_tags(self):
"""Returns :class:`Tags` with a list of all tags found in the database
:returns: :class:`Tags`
:execption: :exc:`NotmuchError` with STATUS.NULL_POINTER on error
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
tags_p = Database._get_all_tags (self._db)
if tags_p == None:
raise NotmuchError(STATUS.NULL_POINTER)
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.
"""
# Raise a NotmuchError if not initialized
self._verify_initialized_db()
return Query(self, querystring)
def __repr__(self):
return "'Notmuch DB " + self.get_path() + "'"
def __del__(self):
"""Close and free the notmuch database if needed"""
if self._db is not None:
nmlib.notmuch_database_close(self._db)
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"""
from ConfigParser import SafeConfigParser
config = SafeConfigParser()
conf_f = os.getenv('NOTMUCH_CONFIG',
os.path.expanduser('~/.notmuch-config'))
config.read(conf_f)
if not config.has_option('database','path'):
raise NotmuchError(message=
"No DB path specified and no user default found")
return config.get('database','path')
@property
def db_p(self):
"""Property returning a pointer to `notmuch_database_t` or `None`
This should normally not be needed by a user (and is not yet
guaranteed to remain stable in future versions).
"""
return self._db
#------------------------------------------------------------------------------
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.
Query() provides an instance attribute :attr:`sort`, which
contains the sort order (if specified via :meth:`set_sort`) or
`None`.
Technically, it wraps the underlying *notmuch_query_t* struct.
.. 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'])
"""Constants: Sort order in which to return results"""
"""notmuch_query_create"""
_create = nmlib.notmuch_query_create
_create.restype = c_void_p
"""notmuch_query_search_threads"""
_search_threads = nmlib.notmuch_query_search_threads
_search_threads.restype = c_void_p
"""notmuch_query_search_messages"""
_search_messages = nmlib.notmuch_query_search_messages
_search_messages.restype = c_void_p
"""notmuch_query_count_messages"""
_count_messages = nmlib.notmuch_query_count_messages
_count_messages.restype = c_uint
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: str
"""
self._db = None
self._query = None
self.sort = None
self.create(db, querystr)
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: str
:returns: Nothing
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if db is not inited
* STATUS.NULL_POINTER if the query creation failed
(too little memory)
"""
if db.db_p is None:
raise NotmuchError(STATUS.NOT_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_p, querystr)
if query_p is None:
NotmuchError(STATUS.NULL_POINTER)
self._query = query_p
def set_sort(self, sort):
"""Set the sort order future results will be delivered in
Wraps the underlying *notmuch_query_set_sort* function.
:param sort: Sort order (see :attr:`Query.SORT`)
:returns: Nothing
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if query has not
been initialized.
"""
if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
self.sort = sort
nmlib.notmuch_query_set_sort(self._query, sort)
def search_threads(self):
"""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.
Technically, it wraps the underlying
*notmuch_query_search_threads* function.
:returns: :class:`Threads`
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if query is not inited
* STATUS.NULL_POINTER if search_threads failed
"""
if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
threads_p = Query._search_threads(self._query)
if threads_p is None:
NotmuchError(STATUS.NULL_POINTER)
return Threads(threads_p,self)
def search_messages(self):
"""Filter messages according to the query and return
:class:`Messages` in the defined sort order
Technically, it wraps the underlying
*notmuch_query_search_messages* function.
:returns: :class:`Messages`
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if query is not inited
* STATUS.NULL_POINTER if search_messages failed
"""
if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
msgs_p = Query._search_messages(self._query)
if msgs_p is None:
NotmuchError(STATUS.NULL_POINTER)
return Messages(msgs_p,self)
def count_messages(self):
"""Estimate the number of messages matching the query
This function performs a search and returns Xapian's best
guess as to the number of matching messages. It is much faster
than performing :meth:`search_messages` and counting the
result with `len()` (although it always returned the same
result in my tests). Technically, it wraps the underlying
*notmuch_query_count_messages* function.
:returns: :class:`Messages`
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if query is not inited
"""
if self._query is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Query._count_messages(self._query)
def __del__(self):
"""Close and free the Query"""
if self._query is not None:
nmlib.notmuch_query_destroy (self._query)
#------------------------------------------------------------------------------
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.restype = c_long
"""notmuch_directory_set_mtime"""
_set_mtime = nmlib.notmuch_directory_set_mtime
_set_mtime.argtypes = [c_char_p, c_long]
"""notmuch_directory_get_child_files"""
_get_child_files = nmlib.notmuch_directory_get_child_files
_get_child_files.restype = c_void_p
"""notmuch_directory_get_child_directories"""
_get_child_directories = nmlib.notmuch_directory_get_child_directories
_get_child_directories.restype = c_void_p
def _verify_dir_initialized(self):
"""Raises a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None"""
if self._dir_p is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
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.add_message` for all mail files in
the directory
* Call notmuch_directory_set_mtime with the mtime read from the
filesystem. Then, when wanting to check for updates to the
directory in the future, the client can call :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
:returns: Nothing on success, raising an exception on failure.
:exception: :exc:`NotmuchError`:
STATUS.XAPIAN_EXCEPTION
A Xapian exception occurred, mtime not stored.
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so directory
mtime cannot be modified.
STATUS.NOT_INITIALIZED
The directory has not been initialized
"""
#Raise a NotmuchError(STATUS.NOT_INITIALIZED) if the dir_p is None
self._verify_dir_initialized()
#TODO: make sure, we convert the mtime parameter to a 'c_long'
status = Directory._set_mtime(self._dir_p, mtime)
#return on success
if status == STATUS.SUCCESS:
return
#fail with Exception otherwise
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
:returns: Nothing on success, raising an exception on failure.
:exception: :exc:`NotmuchError`:
STATUS.NOT_INITIALIZED
The directory has not been initialized
"""
#Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self.dir_p is None
self._verify_dir_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.
"""
#Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
self._verify_dir_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.
"""
#Raise a NotmuchError(STATUS.NOT_INITIALIZED) if self._dir_p is None
self._verify_dir_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
def __del__(self):
"""Close and free the Directory"""
if self._dir_p is not None:
nmlib.notmuch_directory_destroy(self._dir_p)
#------------------------------------------------------------------------------
class Filenames(object):
"""An iterator over File- or Directory names that are stored in the database
"""
#notmuch_filenames_get
_get = nmlib.notmuch_filenames_get
_get.restype = c_char_p
def __init__(self, files_p, parent):
"""
:param files_p: The pointer to an internal notmuch_filenames_t object.
:param parent: The object this Directory is derived from
(usually a Directory()). 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._files_p = files_p
self._parent = parent
def __iter__(self):
""" Make Filenames an iterator """
return self
def next(self):
if self._files_p is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_filenames_valid(self._files_p):
self._files_p = None
raise StopIteration
file = Filenames._get (self._files_p)
nmlib.notmuch_filenames_move_to_next(self._files_p)
return file
def __len__(self):
"""len(:class:`Filenames`) returns the number of contained files
.. note:: As this iterates over the files, we will not be able to
iterate over them again! So this will fail::
#THIS FAILS
files = Database().get_directory('').get_child_files()
if len(files) > 0: #this 'exhausts' msgs
# next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
for file in files: print file
"""
if self._files_p is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
while nmlib.notmuch_filenames_valid(self._files_p):
nmlib.notmuch_filenames_move_to_next(self._files_p)
i += 1
self._files_p = None
return i
def __del__(self):
"""Close and free Filenames"""
if self._files_p is not None:
nmlib.notmuch_filenames_destroy(self._files_p)

View file

@ -0,0 +1,80 @@
"""
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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
"""
from ctypes import CDLL, c_char_p, c_int
from ctypes.util import find_library
#-----------------------------------------------------------------------------
#package-global instance of the notmuch library
try:
nmlib = CDLL("libnotmuch.so.1")
except:
raise ImportError("Could not find shared 'notmuch' library.")
#-----------------------------------------------------------------------------
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 Status(Enum):
"""Enum with a string representation of a notmuch_status_t value."""
__name__="foo"
_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 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 str(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',
'NOT_INITIALIZED'])
class NotmuchError(Exception):
def __init__(self, status=None, message=None):
"""Is initiated with a (notmuch.STATUS[,message=None])"""
super(NotmuchError, self).__init__(message, status)
def __str__(self):
if self.args[0] is not None: return self.args[0]
else: return STATUS.status2str(self.args[1])

View file

@ -0,0 +1,777 @@
"""
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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
Jesse Rosenthal <jrosenthal@jhu.edu>
"""
from ctypes import c_char_p, c_void_p, c_long, c_uint
from datetime import date
from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
from notmuch.tag import Tags
import sys
import email
import types
try:
import simplejson as json
except ImportError:
import json
#------------------------------------------------------------------------------
class Messages(object):
"""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:`NotmuchError` STATUS.NOT_INITIALIZED. Also
note, that any function that uses iteration will also
exhaust the messages. So both::
for msg in msgs: print msg
as well as::
number_of_msgs = len(msgs)
will "exhaust" the Messages. If you need to re-iterate over a list of
messages you will need to retrieve a new :class:`Messages` object.
Things are not as bad as it seems though, you can store and reuse
the single Message objects as often as you want as long as you
keep the parent Messages object around. (Recall that due to
hierarchical memory allocation, all derived Message objects will
be invalid when we delete the parent Messages() object, even if it
was already "exhausted".) So this works::
db = Database()
msgs = Query(db,'').search_messages() #get a Messages() object
msglist = []
for m in msgs:
msglist.append(m)
# msgs is "exhausted" now and even len(msgs) will raise an exception.
# However it will be kept around until all retrieved Message() objects are
# also deleted. If you did 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 Message objects.
print (msglist[0].get_filename())
print (msglist[1].get_filename())
print (msglist[0].get_message_id())
"""
#notmuch_tags_get
_get = nmlib.notmuch_messages_get
_get.restype = c_void_p
_collect_tags = nmlib.notmuch_messages_collect_tags
_collect_tags.restype = c_void_p
def __init__(self, msgs_p, parent=None):
"""
:param msgs_p: A pointer to an underlying *notmuch_messages_t*
structure. These are not publically 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:`NotmuchError`
(STATUS.NULL_POINTER) 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 msgs_p is None:
NotmuchError(STATUS.NULL_POINTER)
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:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
.. note:: :meth:`collect_tags` will iterate over the messages and
therefore will not allow further iterations.
"""
if self._msgs is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
# 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 tags_p == None:
raise NotmuchError(STATUS.NULL_POINTER)
return Tags(tags_p, self)
def __iter__(self):
""" Make Messages an iterator """
return self
def next(self):
if self._msgs is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_messages_valid(self._msgs):
self._msgs = None
raise StopIteration
msg = Message(Messages._get (self._msgs), self)
nmlib.notmuch_messages_move_to_next(self._msgs)
return msg
def __len__(self):
"""len(:class:`Messages`) returns the number of contained messages
.. note:: As this iterates over the messages, we will not be able to
iterate over them again! So this will fail::
#THIS FAILS
msgs = Database().create_query('').search_message()
if len(msgs) > 0: #this 'exhausts' msgs
# next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
for msg in msgs: print msg
Most of the time, using the
:meth:`Query.count_messages` is therefore more
appropriate (and much faster). While not guaranteeing
that it will return the exact same number than len(),
in my tests it effectively always did so.
"""
if self._msgs is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
while nmlib.notmuch_messages_valid(self._msgs):
nmlib.notmuch_messages_move_to_next(self._msgs)
i += 1
self._msgs = None
return i
def __del__(self):
"""Close and free the notmuch Messages"""
if self._msgs is not None:
nmlib.notmuch_messages_destroy (self._msgs)
def print_messages(self, format, indent=0, entire_thread=False):
"""Outputs messages as needed for 'notmuch show' to sys.stdout
:param format: A string of either 'text' or 'json'.
:param indent: A number indicating the reply depth of these messages.
:param entire_thread: A bool, indicating whether we want to output
whole threads or only the matching messages.
"""
if format.lower() == "text":
set_start = ""
set_end = ""
set_sep = ""
elif format.lower() == "json":
set_start = "["
set_end = "]"
set_sep = ", "
else:
raise Exception
first_set = True
sys.stdout.write(set_start)
# iterate through all toplevel messages in this thread
for msg in self:
# if not msg:
# break
if not first_set:
sys.stdout.write(set_sep)
first_set = False
sys.stdout.write(set_start)
match = msg.is_match()
next_indent = indent
if (match or entire_thread):
if format.lower() == "text":
sys.stdout.write(msg.format_message_as_text(indent))
elif format.lower() == "json":
sys.stdout.write(msg.format_message_as_json(indent))
else:
raise NotmuchError
next_indent = indent + 1
# get replies and print them also out (if there are any)
replies = msg.get_replies()
if not replies is None:
sys.stdout.write(set_sep)
replies.print_messages(format, next_indent, entire_thread)
sys.stdout.write(set_end)
sys.stdout.write(set_end)
#------------------------------------------------------------------------------
class Message(object):
"""Represents a single Email message
Technically, this wraps the underlying *notmuch_message_t* structure.
"""
"""notmuch_message_get_filename (notmuch_message_t *message)"""
_get_filename = nmlib.notmuch_message_get_filename
_get_filename.restype = c_char_p
"""notmuch_message_get_flag"""
_get_flag = nmlib.notmuch_message_get_flag
_get_flag.restype = c_uint
"""notmuch_message_get_message_id (notmuch_message_t *message)"""
_get_message_id = nmlib.notmuch_message_get_message_id
_get_message_id.restype = c_char_p
"""notmuch_message_get_thread_id"""
_get_thread_id = nmlib.notmuch_message_get_thread_id
_get_thread_id.restype = c_char_p
"""notmuch_message_get_replies"""
_get_replies = nmlib.notmuch_message_get_replies
_get_replies.restype = c_void_p
"""notmuch_message_get_tags (notmuch_message_t *message)"""
_get_tags = nmlib.notmuch_message_get_tags
_get_tags.restype = c_void_p
_get_date = nmlib.notmuch_message_get_date
_get_date.restype = c_long
_get_header = nmlib.notmuch_message_get_header
_get_header.restype = c_char_p
#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:`NotmuchError`
STATUS.NULL_POINTER.
: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 msg_p is None:
NotmuchError(STATUS.NULL_POINTER)
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
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Message._get_message_id(self._msg)
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
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Message._get_thread_id (self._msg);
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 `None`.
:returns: :class:`Messages` or `None` if there are no replies to
this message.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
msgs_p = Message._get_replies(self._msg);
if msgs_p is None:
return None
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
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Message._get_date(self._msg)
def get_header(self, header):
"""Returns a message header
This returns any message header that is stored in the notmuch database.
This is only a selected subset of headers, which is currently:
TODO: add stored headers
:param header: The name of the header to be retrieved.
It is not case-sensitive (TODO: confirm).
:type header: str
:returns: The header value as string
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if the message
is not initialized.
* STATUS.NULL_POINTER, if no header was found
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
#Returns NULL if any error occurs.
header = Message._get_header (self._msg, header)
if header == None:
raise NotmuchError(STATUS.NULL_POINTER)
return header
def get_filename(self):
"""Returns the file path of the message file
:returns: Absolute file path & name of the message file
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Message._get_filename(self._msg)
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.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
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.
:returns: Nothing
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
nmlib.notmuch_message_set_flag(self._msg, flag, value)
def get_tags(self):
"""Returns the message tags
:returns: A :class:`Tags` iterator.
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if the message
is not initialized.
* STATUS.NULL_POINTER, on error
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
tags_p = Message._get_tags(self._msg)
if tags_p == None:
raise NotmuchError(STATUS.NULL_POINTER)
return Tags(tags_p, self)
def add_tag(self, tag):
"""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.
:returns: STATUS.SUCCESS if the tag was successfully added.
Raises an exception otherwise.
:exception: :exc:`NotmuchError`. They have the following meaning:
STATUS.NULL_POINTER
The 'tag' argument is NULL
STATUS.TAG_TOO_LONG
The length of 'tag' is too long
(exceeds Message.NOTMUCH_TAG_MAX)
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot be
modified.
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
status = nmlib.notmuch_message_add_tag (self._msg, tag)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
def remove_tag(self, tag):
"""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.
:returns: STATUS.SUCCESS if the tag was successfully removed or if
the message had no such tag.
Raises an exception otherwise.
:exception: :exc:`NotmuchError`. They have the following meaning:
STATUS.NULL_POINTER
The 'tag' argument is NULL
STATUS.TAG_TOO_LONG
The length of 'tag' is too long
(exceeds NOTMUCH_TAG_MAX)
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot
be modified.
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
status = nmlib.notmuch_message_remove_tag(self._msg, tag)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
def remove_all_tags(self):
"""Removes all tags from the given message.
See :meth:`freeze` for an example showing how to safely
replace tag values.
:returns: STATUS.SUCCESS if the tags were successfully removed.
Raises an exception otherwise.
:exception: :exc:`NotmuchError`. They have the following meaning:
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot
be modified.
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
status = nmlib.notmuch_message_remove_all_tags(self._msg)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
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()
for tag in new_tags:
msg.add_tag(tag)
msg.thaw()
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.
:exception: :exc:`NotmuchError`. They have the following meaning:
STATUS.READ_ONLY_DATABASE
Database was opened in read-only mode so message cannot
be modified.
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
status = nmlib.notmuch_message_freeze(self._msg)
if STATUS.SUCCESS == status:
# return on success
return status
raise NotmuchError(status)
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.
:exception: :exc:`NotmuchError`. They have the following meaning:
STATUS.UNBALANCED_FREEZE_THAW
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`.
STATUS.NOT_INITIALIZED
The message has not been initialized.
"""
if self._msg is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
status = nmlib.notmuch_message_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 __str__(self):
"""A message() is represented by a 1-line summary"""
msg = {}
msg['from'] = self.get_header('from')
msg['tags'] = str(self.get_tags())
msg['date'] = date.fromtimestamp(self.get_date())
replies = self.get_replies()
msg['replies'] = len(replies) if replies is not None else -1
return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
def get_message_parts(self):
"""Output like notmuch show"""
fp = open(self.get_filename())
email_msg = email.message_from_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 format_message_internal(self):
"""Create an internal representation of the message parts,
which can easily be output to json, text, or another output
format. The argument match tells whether this matched a
query."""
output = {}
output["id"] = self.get_message_id()
output["match"] = self.is_match()
output["filename"] = self.get_filename()
output["tags"] = list(self.get_tags())
headers = {}
for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
headers[h] = self.get_header(h)
output["headers"] = headers
body = []
parts = self.get_message_parts()
for i in xrange(len(parts)):
msg = parts[i]
part_dict = {}
part_dict["id"] = i + 1
# We'll be using this is a lot, so let's just get it once.
cont_type = msg.get_content_type()
part_dict["content-type"] = cont_type
# NOTE:
# Now we emulate the current behaviour, where it ignores
# the html if there's a text representation.
#
# This is being worked on, but it will be easier to fix
# here in the future than to end up with another
# incompatible solution.
disposition = msg["Content-Disposition"]
if disposition and disposition.lower().startswith("attachment"):
part_dict["filename"] = msg.get_filename()
else:
if cont_type.lower() == "text/plain":
part_dict["content"] = msg.get_payload()
elif (cont_type.lower() == "text/html" and
i == 0):
part_dict["content"] = msg.get_payload()
body.append(part_dict)
output["body"] = body
return output
def format_message_as_json(self, indent=0):
"""Outputs the message as json. This is essentially the same
as python's dict format, but we run it through, just so we
don't have to worry about the details."""
return json.dumps(self.format_message_internal())
def format_message_as_text(self, indent=0):
"""Outputs it in the old-fashioned notmuch text form. Will be
easy to change to a new format when the format changes."""
format = self.format_message_internal()
output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
% (format['id'], indent, format['match'], format['filename'])
output += "\n\fheader{"
#Todo: this date is supposed to be prettified, as in the index.
output += "\n%s (%s) (" % (format["headers"]["From"],
format["headers"]["Date"])
output += ", ".join(format["tags"])
output += ")"
output += "\nSubject: %s" % format["headers"]["Subject"]
output += "\nFrom: %s" % format["headers"]["From"]
output += "\nTo: %s" % format["headers"]["To"]
if format["headers"]["Cc"]:
output += "\nCc: %s" % format["headers"]["Cc"]
if format["headers"]["Bcc"]:
output += "\nBcc: %s" % format["headers"]["Bcc"]
output += "\nDate: %s" % format["headers"]["Date"]
output += "\n\fheader}"
output += "\n\fbody{"
parts = format["body"]
parts.sort(key=lambda(p): p["id"])
for p in parts:
if not p.has_key("filename"):
output += "\n\fpart{ "
output += "ID: %d, Content-type: %s\n" % (p["id"],
p["content-type"])
if p.has_key("content"):
output += "\n%s\n" % p["content"]
else:
output += "Non-text part: %s\n" % p["content-type"]
output += "\n\fpart}"
else:
output += "\n\fattachment{ "
output += "ID: %d, Content-type:%s\n" % (p["id"],
p["content-type"])
output += "Attachment: %s\n" % p["filename"]
output += "\n\fattachment}\n"
output += "\n\fbody}\n"
output += "\n\fmessage}"
return output
def __del__(self):
"""Close and free the notmuch Message"""
if self._msg is not None:
nmlib.notmuch_message_destroy (self._msg)

View file

@ -0,0 +1,126 @@
"""
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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
"""
from ctypes import c_char_p
from notmuch.globals import nmlib, STATUS, NotmuchError
#------------------------------------------------------------------------------
class Tags(object):
"""Represents a list of notmuch tags
This object provides an iterator over a list of notmuch tags. 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:`NotmuchError`
STATUS.NOT_INITIALIZED. 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.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 publically 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:`NotmuchError`
(STATUS.NULL_POINTER) 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 tags_p is None:
NotmuchError(STATUS.NULL_POINTER)
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
def next(self):
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_tags_valid(self._tags):
self._tags = None
raise StopIteration
tag = Tags._get (self._tags)
nmlib.notmuch_tags_move_to_next(self._tags)
return tag
def __len__(self):
"""len(:class:`Tags`) returns the number of contained 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:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
if self._tags is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
while nmlib.notmuch_tags_valid(self._msgs):
nmlib.notmuch_tags_move_to_next(self._msgs)
i += 1
self._tags = None
return i
def __str__(self):
"""The str() representation of Tags() is 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:`NotmuchError` STATUS.NOT_INITIALIZED on
subsequent attempts.
"""
return " ".join(self)
def __del__(self):
"""Close and free the notmuch tags"""
if self._tags is not None:
nmlib.notmuch_tags_destroy (self._tags)

View file

@ -0,0 +1,370 @@
"""
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 <http://www.gnu.org/licenses/>.
Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
"""
from ctypes import c_char_p, c_void_p, c_long
from notmuch.globals import nmlib, STATUS, NotmuchError
from notmuch.message import Messages
from notmuch.tag import Tags
from datetime import date
#------------------------------------------------------------------------------
class Threads(object):
"""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:`NotmuchError` STATUS.NOT_INITIALIZED. Also
note, that any function that uses iteration will also
exhaust the messages. So both::
for thread in threads: print thread
as well as::
number_of_msgs = len(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 and even len(threads) will raise an
# exception.
# 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.restype = c_void_p
def __init__(self, threads_p, parent=None):
"""
:param threads_p: A pointer to an underlying *notmuch_threads_t*
structure. These are not publically 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:`NotmuchError`
(STATUS.NULL_POINTER) 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 threads_p is None:
NotmuchError(STATUS.NULL_POINTER)
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
def next(self):
if self._threads is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
if not nmlib.notmuch_threads_valid(self._threads):
self._threads = None
raise StopIteration
thread = Thread(Threads._get (self._threads), self)
nmlib.notmuch_threads_move_to_next(self._threads)
return thread
def __len__(self):
"""len(:class:`Threads`) returns the number of contained Threads
.. note:: As this iterates over the threads, we will not be able to
iterate over them again! So this will fail::
#THIS FAILS
threads = Database().create_query('').search_threads()
if len(threads) > 0: #this 'exhausts' threads
# next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
for thread in threads: print thread
"""
if self._threads is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
i=0
# returns 'bool'. On out-of-memory it returns None
while nmlib.notmuch_threads_valid(self._threads):
nmlib.notmuch_threads_move_to_next(self._threads)
i += 1
# reset self._threads to mark as "exhausted"
self._threads = None
return i
def __del__(self):
"""Close and free the notmuch Threads"""
if self._threads is not None:
nmlib.notmuch_messages_destroy (self._threads)
#------------------------------------------------------------------------------
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.restype = c_char_p
"""notmuch_thread_get_authors"""
_get_authors = nmlib.notmuch_thread_get_authors
_get_authors.restype = c_char_p
"""notmuch_thread_get_subject"""
_get_subject = nmlib.notmuch_thread_get_subject
_get_subject.restype = c_char_p
"""notmuch_thread_get_toplevel_messages"""
_get_toplevel_messages = nmlib.notmuch_thread_get_toplevel_messages
_get_toplevel_messages.restype = c_void_p
_get_newest_date = nmlib.notmuch_thread_get_newest_date
_get_newest_date.restype = c_long
_get_oldest_date = nmlib.notmuch_thread_get_oldest_date
_get_oldest_date.restype = c_long
"""notmuch_thread_get_tags"""
_get_tags = nmlib.notmuch_thread_get_tags
_get_tags.restype = c_void_p
def __init__(self, thread_p, parent=None):
"""
:param thread_p: A pointer to an internal notmuch_thread_t
Structure. These are not publically 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:`NotmuchError`
(STATUS.NULL_POINTER) 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 thread_p is None:
NotmuchError(STATUS.NULL_POINTER)
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
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_thread_id(self._thread)
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`.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return nmlib.notmuch_thread_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.
To iterate over all messages in the thread, the caller will need to
iterate over the result of :meth:`Message.get_replies` for each
top-level message (and do that recursively for the resulting
messages, etc.).
:returns: :class:`Messages`
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if query is not inited
* STATUS.NULL_POINTER if search_messages failed
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
msgs_p = Thread._get_toplevel_messages(self._thread)
if msgs_p is None:
NotmuchError(STATUS.NULL_POINTER)
return Messages(msgs_p,self)
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`.
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the thread
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return nmlib.notmuch_thread_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 self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_authors(self._thread)
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 self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
return Thread._get_subject(self._thread)
def get_newest_date(self):
"""Returns time_t of the newest message date
:returns: A time_t timestamp.
:rtype: c_unit64
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
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
:exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
is not initialized.
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
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 explicitely deleted).
:returns: A :class:`Tags` iterator.
:exception: :exc:`NotmuchError`
* STATUS.NOT_INITIALIZED if the thread
is not initialized.
* STATUS.NULL_POINTER, on error
"""
if self._thread is None:
raise NotmuchError(STATUS.NOT_INITIALIZED)
tags_p = Thread._get_tags(self._thread)
if tags_p == None:
raise NotmuchError(STATUS.NULL_POINTER)
return Tags(tags_p, self)
def __str__(self):
"""A str(Thread()) is represented by a 1-line summary"""
thread = {}
thread['id'] = self.get_thread_id()
###TODO: How do we find out the current sort order of Threads?
###Add a "sort" attribute to the Threads() object?
#if (sort == NOTMUCH_SORT_OLDEST_FIRST)
# date = notmuch_thread_get_oldest_date (thread);
#else
# date = notmuch_thread_get_newest_date (thread);
thread['date'] = date.fromtimestamp(self.get_newest_date())
thread['matched'] = self.get_matched_messages()
thread['total'] = self.get_total_messages()
thread['authors'] = self.get_authors()
thread['subject'] = self.get_subject()
thread['tags'] = self.get_tags()
return "thread:%(id)s %(date)12s [%(matched)d/%(total)d] %(authors)s; %(subject)s (%(tags)s)" % (thread)
def __del__(self):
"""Close and free the notmuch Thread"""
if self._thread is not None:
nmlib.notmuch_thread_destroy (self._thread)

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

@ -0,0 +1,52 @@
#!/usr/bin/env python
from distutils.core import setup
from notmuch import __VERSION__
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='http://bitbucket.org/spaetz/cnotmuch/',
download_url='http://bitbucket.org/spaetz/cnotmuch/get/v'+__VERSION__+'.tar.gz',
packages=['notmuch'],
keywords = ["library", "email"],
long_description="""Overview
==============
The notmuch module provides an interface to the `notmuch <http://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 cnotmuch release can be `viewed online <http://packages.python.org/cnotmuch>`_.
The classes notmuch.Database, notmuch.Query provide most of the core functionality, returning notmuch.Messages and notmuch.Tags.
Installation and Deinstallation
-------------------------------
cnotmuch is packaged on http://pypi.python.org. This means you can do
"easy_install cnotmuch" (or using pip) on your linux box and it will
get installed into:
/usr/local/lib/python2.x/dist-packages/
For uninstalling, you will need to remove the "cnotmuch-0.1-py2.x.egg"
directory and delete one entry refering to cnotmuch in the
"easy-install.pth" file in that directory. There should be no trace
left of cnotmuch then.
Requirements
------------
You need to have notmuch installed (or rather libnotmuch.so.1). The release version 0.1 should work fine. Also, cnotmuch 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 :: 2 - Pre-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='http://www.gnu.org/licenses/gpl-3.0.txt',
)

542
bindings/python/test/notmuch-test Executable file
View file

@ -0,0 +1,542 @@
#!/bin/bash
set -e
find_notmuch_binary ()
{
dir=$1
while [ -n "$dir" ]; do
bin=$dir/notmuch
if [ -x $bin ]; then
echo $bin
return
fi
dir=$(dirname $dir)
if [ "$dir" = "/" ]; then
break
fi
done
echo notmuch
}
increment_mtime_amount=0
increment_mtime ()
{
dir=$1
increment_mtime_amount=$((increment_mtime_amount + 1))
touch -d "+${increment_mtime_amount} seconds" $dir
}
# Generate a new message in the mail directory, with a unique message
# ID and subject. The message is not added to the index.
#
# After this function returns, the filename of the generated message
# is available as $gen_msg_filename and the message ID is available as
# $gen_msg_id .
#
# This function supports named parameters with the bash syntax for
# assigning a value to an associative array ([name]=value). The
# supported parameters are:
#
# [dir]=directory/of/choice
#
# Generate the message in directory 'directory/of/choice' within
# the mail store. The directory will be created if necessary.
#
# [body]=text
#
# Text to use as the body of the email message
#
# '[from]="Some User <user@example.com>"'
# '[to]="Some User <user@example.com>"'
# '[subject]="Subject of email message"'
# '[date]="RFC 822 Date"'
#
# Values for email headers. If not provided, default values will
# be generated instead.
#
# '[cc]="Some User <user@example.com>"'
# [reply-to]=some-address
# [in-reply-to]=<message-id>
#
# Additional values for email headers. If these are not provided
# then the relevant headers will simply not appear in the
# message.
gen_msg_cnt=0
gen_msg_filename=""
gen_msg_id=""
generate_message ()
{
# This is our (bash-specific) magic for doing named parameters
local -A template="($@)"
local additional_headers
gen_msg_cnt=$((gen_msg_cnt + 1))
gen_msg_name=msg-$(printf "%03d" $gen_msg_cnt)
gen_msg_id="${gen_msg_name}@notmuch-test-suite"
if [ -z "${template[dir]}" ]; then
gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
else
gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
mkdir -p $(dirname $gen_msg_filename)
fi
if [ -z "${template[body]}" ]; then
template[body]="This is just a test message at ${gen_msg_filename}"
fi
if [ -z "${template[from]}" ]; then
template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
fi
if [ -z "${template[to]}" ]; then
template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
fi
if [ -z "${template[subject]}" ]; then
template[subject]="Test message ${gen_msg_filename}"
fi
if [ -z "${template[date]}" ]; then
template[date]="Tue, 05 Jan 2010 15:43:57 -0800"
fi
additional_headers=""
if [ ! -z "${template[reply-to]}" ]; then
additional_headers="Reply-To: ${template[reply-to]}
${additional_headers}"
fi
if [ ! -z "${template[in-reply-to]}" ]; then
additional_headers="In-Reply-To: ${template[in-reply-to]}
${additional_headers}"
fi
if [ ! -z "${template[cc]}" ]; then
additional_headers="Cc: ${template[cc]}
${additional_headers}"
fi
cat <<EOF >$gen_msg_filename
From: ${template[from]}
To: ${template[to]}
Message-Id: <${gen_msg_id}>
Subject: ${template[subject]}
Date: ${template[date]}
${additional_headers}
${template[body]}
EOF
# Ensure that the mtime of the containing directory is updated
increment_mtime $(dirname ${gen_msg_filename})
}
# Generate a new message and add it to the index.
#
# All of the arguments and return values supported by generate_message
# are alos supported here, so see that function for details.
add_message ()
{
generate_message "$@"
$NOTMUCH new > /dev/null
}
NOTMUCH_IGNORED_OUTPUT_REGEXP='^Processed [0-9]*( total)? file|Found [0-9]* total file'
NOTMUCH_THREAD_ID_SQUELCH='s/thread:................/thread:XXX/'
execute_expecting ()
{
args=$1
expected=$2
output=$($NOTMUCH $args | grep -v -E -e "$NOTMUCH_IGNORED_OUTPUT_REGEXP" | sed -e "$NOTMUCH_THREAD_ID_SQUELCH" || true)
if [ "$output" = "$expected" ]; then
echo " PASS"
else
echo " FAIL"
echo " Expected output: $expected"
echo " Actual output: $output"
fi
}
TEST_DIR=$(pwd)/test.$$
MAIL_DIR=${TEST_DIR}/mail
export NOTMUCH_CONFIG=${TEST_DIR}/notmuch-config
NOTMUCH=$(find_notmuch_binary $(pwd))
rm -rf ${TEST_DIR}
mkdir ${TEST_DIR}
cd ${TEST_DIR}
mkdir ${MAIL_DIR}
cat <<EOF > ${NOTMUCH_CONFIG}
[database]
path=${MAIL_DIR}
[user]
name=Notmuch Test Suite
primary_email=test_suite@notmuchmail.org
other_email=test_suite_other@notmuchmail.org
EOF
printf "Testing \"notmuch new\" in several variations:\n"
printf " No new messages...\t\t"
execute_expecting new "No new mail."
printf " Single new message...\t\t"
generate_message
execute_expecting new "Added 1 new message to the database."
printf " Multiple new messages...\t"
generate_message
generate_message
execute_expecting new "Added 2 new messages to the database."
printf " No new messages (non-empty DB)... "
execute_expecting new "No new mail."
printf " New directories...\t\t"
rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
mkdir ${MAIL_DIR}/def
mkdir ${MAIL_DIR}/ghi
generate_message [dir]=def
execute_expecting new "Added 1 new message to the database."
printf " Alternate inode order...\t"
rm -rf ${MAIL_DIR}/.notmuch
mv ${MAIL_DIR}/ghi ${MAIL_DIR}/abc
rm ${MAIL_DIR}/def/*
generate_message [dir]=abc
execute_expecting new "Added 1 new message to the database."
printf " Message moved in...\t\t"
rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
generate_message
tmp_msg_filename=tmp/$gen_msg_filename
mkdir -p $(dirname $tmp_msg_filename)
mv $gen_msg_filename $tmp_msg_filename
increment_mtime ${MAIL_DIR}
$NOTMUCH new > /dev/null
mv $tmp_msg_filename $gen_msg_filename
increment_mtime ${MAIL_DIR}
execute_expecting new "Added 1 new message to the database."
printf " Renamed message...\t\t"
generate_message
$NOTMUCH new > /dev/null
mv $gen_msg_filename ${gen_msg_filename}-renamed
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Detected 1 file rename."
printf " Deleted message...\t\t"
rm ${gen_msg_filename}-renamed
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Removed 1 message."
printf " Renamed directory...\t\t"
generate_message [dir]=dir
generate_message [dir]=dir
generate_message [dir]=dir
$NOTMUCH new > /dev/null
mv ${MAIL_DIR}/dir ${MAIL_DIR}/dir-renamed
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Detected 3 file renames."
printf " Deleted directory...\t\t"
rm -rf ${MAIL_DIR}/dir-renamed
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Removed 3 messages."
printf " New directory (at end of list)... "
generate_message [dir]=zzz
generate_message [dir]=zzz
generate_message [dir]=zzz
execute_expecting new "Added 3 new messages to the database."
printf " Deleted directory (end of list)... "
rm -rf ${MAIL_DIR}/zzz
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Removed 3 messages."
printf " New symlink to directory...\t"
rm -rf ${MAIL_DIR}/.notmuch
mv ${MAIL_DIR} ${TEST_DIR}/actual_maildir
mkdir ${MAIL_DIR}
ln -s ${TEST_DIR}/actual_maildir ${MAIL_DIR}/symlink
execute_expecting new "Added 1 new message to the database."
printf " New symlink to a file...\t"
generate_message
external_msg_filename=${TEST_DIR}/external/$(basename $gen_msg_filename)
mkdir -p $(dirname $external_msg_filename)
mv $gen_msg_filename $external_msg_filename
ln -s $external_msg_filename $gen_msg_filename
increment_mtime ${MAIL_DIR}
execute_expecting new "Added 1 new message to the database."
printf " New two-level directory...\t"
generate_message [dir]=two/levels
generate_message [dir]=two/levels
generate_message [dir]=two/levels
execute_expecting new "Added 3 new messages to the database."
printf " Deleted two-level directory... "
rm -rf ${MAIL_DIR}/two
increment_mtime ${MAIL_DIR}
execute_expecting new "No new mail. Removed 3 messages."
printf "\nTesting \"notmuch search\" in several variations:\n"
printf " Search body...\t\t\t"
add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest
execute_expecting "search bodysearchtest" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)"
printf " Search by from:...\t\t"
add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom
execute_expecting "search from:searchbyfrom" "thread:XXX 2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)"
printf " Search by to:...\t\t"
add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto
execute_expecting "search to:searchbyto" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)"
printf " Search by subject:...\t\t"
add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
execute_expecting "search subject:subjectsearchtest" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)"
printf " Search by id:...\t\t"
add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
execute_expecting "search id:${gen_msg_id}" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)"
printf " Search by tag:...\t\t"
add_message '[subject]="search by tag"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
$NOTMUCH tag +searchbytag id:${gen_msg_id}
execute_expecting "search tag:searchbytag" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)"
printf " Search by thread:...\t\t"
add_message '[subject]="search by thread"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
thread_id=$($NOTMUCH search id:${gen_msg_id} | sed -e 's/thread:\([a-f0-9]*\).*/\1/')
execute_expecting "search thread:${thread_id}" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)"
printf " Search body (phrase)...\t"
add_message '[subject]="body search (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="body search (phrase)"'
execute_expecting "search 'body search (phrase)'" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)"
printf " Search by from: (address)...\t"
add_message '[subject]="search by from (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom@example.com
execute_expecting "search from:searchbyfrom@example.com" "thread:XXX 2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)"
printf " Search by from: (name)...\t"
add_message '[subject]="search by from (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[from]="Search By From Name <test@example.com>"'
execute_expecting "search from:'Search By From Name'" "thread:XXX 2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)"
printf " Search by to: (address)...\t"
add_message '[subject]="search by to (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto@example.com
execute_expecting "search to:searchbyto@example.com" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)"
printf " Search by to: (name)...\t"
add_message '[subject]="search by to (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[to]="Search By To Name <test@example.com>"'
execute_expecting "search to:'Search By To Name'" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)"
printf " Search by subject: (phrase)...\t"
add_message '[subject]="subject search test (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
execute_expecting "search subject:'subject search test (phrase)'" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)"
printf "\nTesting \"notmuch reply\" in several variations:\n"
printf " Basic reply...\t\t\t"
add_message '[from]="Sender <sender@example.com>"' \
[to]=test_suite@notmuchmail.org \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="basic reply test"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <sender@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> basic reply test"
printf " Multiple recipients...\t\t"
add_message '[from]="Sender <sender@example.com>"' \
'[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="Multiple recipients"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <sender@example.com>, Someone Else <someone@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> Multiple recipients"
printf " Reply with CC...\t\t"
add_message '[from]="Sender <sender@example.com>"' \
[to]=test_suite@notmuchmail.org \
'[cc]="Other Parties <cc@example.com>"' \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="reply with CC"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <sender@example.com>
Cc: Other Parties <cc@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> reply with CC"
printf " Reply from alternate address..."
add_message '[from]="Sender <sender@example.com>"' \
[to]=test_suite_other@notmuchmail.org \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="reply from alternate address"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <sender@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> reply from alternate address"
printf " Support for Reply-To...\t"
add_message '[from]="Sender <sender@example.com>"' \
[to]=test_suite@notmuchmail.org \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="support for reply-to"' \
'[reply-to]="Sender <elsewhere@example.com>"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <elsewhere@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> support for reply-to"
printf " Un-munging Reply-To...\t\t"
add_message '[from]="Sender <sender@example.com>"' \
'[to]="Some List <list@example.com>"' \
[subject]=notmuch-reply-test \
'[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
'[body]="Un-munging Reply-To"' \
'[reply-to]="Evil Munging List <list@example.com>"'
execute_expecting "reply id:${gen_msg_id}" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
Subject: Re: notmuch-reply-test
To: Sender <sender@example.com>, Some List <list@example.com>
Bcc: test_suite@notmuchmail.org
In-Reply-To: <${gen_msg_id}>
References: <${gen_msg_id}>
On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
> Un-munging Reply-To"
printf "\nTesting handling of uuencoded data:\n"
add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
'[body]="This message is used to ensure that notmuch correctly handles a
message containing a block of uuencoded data. First, we have a marker
this content beforeuudata . Then we beging the uunencoded data itself:
begin 644 bogus-uuencoded-data
M0123456789012345678901234567890123456789012345678901234567890
MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUNECODED DATA.
MINSTEAD THIS IS JUST A WAY TO ENSURE THAT THIS BLOCK OF DATA
MIS CORRECTLY IGNORED WHEN NOTMUCH CREATES ITS INDEX. SO WE
MINCLUDE A DURINGUUDATA MARKER THAT SHOULD NOT RESULT IN ANY
MSEARCH RESULT.
\`
end
Finally, we have our afteruudata marker as well."'
printf " Ensure content before uu data is indexed..."
execute_expecting "search beforeuudata" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
printf " Ensure uu data is not indexed...\t"
execute_expecting "search DURINGUUDATA" ""
printf " Ensure content after uu data is indexed..."
execute_expecting "search afteruudata" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
printf "\nTesting \"notmuch dump\" and \"notmuch restore\":\n"
printf " Dumping all tags...\t\t"
$NOTMUCH dump dump.expected
echo " PASS"
printf " Clearing all tags...\t\t"
sed -e 's/(\([^(]*\))$/()/' < dump.expected > clear.expected
$NOTMUCH restore clear.expected
$NOTMUCH dump clear.actual
if diff clear.expected clear.actual > /dev/null; then
echo " PASS"
else
echo " FAIL"
echo " Expected output: See file clear.expected"
echo " Actual output: See file clear.actual"
fi
printf " Restoring original tags...\t"
$NOTMUCH restore dump.expected
$NOTMUCH dump dump.actual
if diff dump.expected dump.actual > /dev/null; then
echo " PASS"
else
echo " FAIL"
echo " Expected output: See file dump.expected"
echo " Actual output: See file dump.actual"
fi
printf " Restore with nothing to do...\t"
$NOTMUCH restore dump.expected
echo " PASS"
cat <<EOF
Notmuch test suite complete.
Intermediate state can be examined in:
${TEST_DIR}
EOF