Merge Sebastian Spaeth's python bindings into bindings/python
authorCarl Worth <cworth@cworth.org>
Thu, 22 Apr 2010 00:29:48 +0000 (17:29 -0700)
committerCarl Worth <cworth@cworth.org>
Thu, 22 Apr 2010 00:30:11 +0000 (17:30 -0700)
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.

105 files changed:
.gitignore [new file with mode: 0644]
AUTHORS [new file with mode: 0644]
COPYING [new file with mode: 0644]
COPYING-GPL-3 [new file with mode: 0644]
INSTALL [new file with mode: 0644]
Makefile [new file with mode: 0644]
Makefile.local [new file with mode: 0644]
NEWS [new file with mode: 0644]
README [new file with mode: 0644]
RELEASING [new file with mode: 0644]
TODO [new file with mode: 0644]
compat/Makefile [new file with mode: 0644]
compat/Makefile.local [new file with mode: 0644]
compat/README [new file with mode: 0644]
compat/compat.h [new file with mode: 0644]
compat/getdelim.c [new file with mode: 0644]
compat/getline.c [new file with mode: 0644]
compat/have_getline.c [new file with mode: 0644]
compat/have_strcasestr.c [new file with mode: 0644]
compat/strcasestr.c [new file with mode: 0644]
completion/Makefile [new file with mode: 0644]
completion/Makefile.local [new file with mode: 0644]
completion/README [new file with mode: 0644]
completion/notmuch-completion.bash [new file with mode: 0644]
completion/notmuch-completion.tcsh [new file with mode: 0644]
completion/notmuch-completion.zsh [new file with mode: 0644]
configure [new file with mode: 0755]
debian/.gitignore [new file with mode: 0644]
debian/README.Debian [new file with mode: 0644]
debian/changelog [new file with mode: 0644]
debian/compat [new file with mode: 0644]
debian/control [new file with mode: 0644]
debian/copyright [new file with mode: 0644]
debian/gbp.conf [new file with mode: 0644]
debian/libnotmuch-dev.install [new file with mode: 0644]
debian/libnotmuch1.install [new file with mode: 0644]
debian/notmuch.dirs [new file with mode: 0644]
debian/notmuch.emacsen-install [new file with mode: 0755]
debian/notmuch.emacsen-remove [new file with mode: 0755]
debian/notmuch.emacsen-startup [new file with mode: 0644]
debian/notmuch.install [new file with mode: 0644]
debian/rules [new file with mode: 0755]
debian/source/format [new file with mode: 0644]
debugger.c [new file with mode: 0644]
emacs/Makefile [new file with mode: 0644]
emacs/Makefile.local [new file with mode: 0644]
emacs/notmuch-lib.el [new file with mode: 0644]
emacs/notmuch-query.el [new file with mode: 0644]
emacs/notmuch-show.el [new file with mode: 0644]
emacs/notmuch-wash.el [new file with mode: 0644]
emacs/notmuch.el [new file with mode: 0644]
gmime-filter-headers.c [new file with mode: 0644]
gmime-filter-headers.h [new file with mode: 0644]
gmime-filter-reply.c [new file with mode: 0644]
gmime-filter-reply.h [new file with mode: 0644]
json.c [new file with mode: 0644]
lib/Makefile [new file with mode: 0644]
lib/Makefile.local [new file with mode: 0644]
lib/database-private.h [new file with mode: 0644]
lib/database.cc [new file with mode: 0644]
lib/directory.cc [new file with mode: 0644]
lib/index.cc [new file with mode: 0644]
lib/libsha1.c [new file with mode: 0644]
lib/libsha1.h [new file with mode: 0644]
lib/message-file.c [new file with mode: 0644]
lib/message.cc [new file with mode: 0644]
lib/messages.c [new file with mode: 0644]
lib/notmuch-private.h [new file with mode: 0644]
lib/notmuch.h [new file with mode: 0644]
lib/query.cc [new file with mode: 0644]
lib/sha1.c [new file with mode: 0644]
lib/tags.c [new file with mode: 0644]
lib/thread.cc [new file with mode: 0644]
lib/xutil.c [new file with mode: 0644]
lib/xutil.h [new file with mode: 0644]
notmuch-client.h [new file with mode: 0644]
notmuch-config.c [new file with mode: 0644]
notmuch-count.c [new file with mode: 0644]
notmuch-dump.c [new file with mode: 0644]
notmuch-new.c [new file with mode: 0644]
notmuch-reply.c [new file with mode: 0644]
notmuch-restore.c [new file with mode: 0644]
notmuch-search-tags.c [new file with mode: 0644]
notmuch-search.c [new file with mode: 0644]
notmuch-setup.c [new file with mode: 0644]
notmuch-show.c [new file with mode: 0644]
notmuch-tag.c [new file with mode: 0644]
notmuch-time.c [new file with mode: 0644]
notmuch.1 [new file with mode: 0644]
notmuch.c [new file with mode: 0644]
notmuch.desktop [new file with mode: 0644]
packaging/debian [new file with mode: 0644]
packaging/fedora/notmuch.spec [new file with mode: 0644]
query-string.c [new file with mode: 0644]
show-message.c [new file with mode: 0644]
test/notmuch-test [new file with mode: 0755]
version [new file with mode: 0644]
vim/Makefile [new file with mode: 0644]
vim/README [new file with mode: 0644]
vim/notmuch.yaml [new file with mode: 0644]
vim/plugin/notmuch.vim [new file with mode: 0644]
vim/syntax/notmuch-compose.vim [new file with mode: 0644]
vim/syntax/notmuch-folders.vim [new file with mode: 0644]
vim/syntax/notmuch-search.vim [new file with mode: 0644]
vim/syntax/notmuch-show.vim [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..217440d
--- /dev/null
@@ -0,0 +1,15 @@
+.first-build-message
+Makefile.config
+TAGS
+tags
+*cscope*
+.deps
+notmuch
+notmuch-shared
+notmuch.1.gz
+libnotmuch.so*
+*.[ao]
+*~
+.*.swp
+*.elc
+releases
diff --git a/AUTHORS b/AUTHORS
new file mode 100644 (file)
index 0000000..6d0f2de
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,30 @@
+Carl Worth <cworth@cworth.org> is the primary author of Notmuch.
+But there's really not much that he's done. There's been a lot of
+standing on shoulders here:
+
+William Morgan deserves credit for providing the primary inspiration
+for Notmuch with his program Sup (http://sup.rubyforge.org/).
+
+Some people have contributed code that has made it into Notmuch
+without their specific knowledge (but with their full permission
+thanks to the GNU General Public License). This includes:
+
+Brian Gladman (with Mikhail Gusarov <dottedmag@dottedmag.net>)
+       Implementation of SHA-1 (nice and small) (libsha1.c)
+
+Please see the various files in the Notmuch distribution for
+individual copyright statements.
+
+And of course, though their code isn't distributed here, Notmuch would
+be not much of anything without the contributors to Xapian, the search
+engine that does the really heavy lifting, as well as the various
+system libraries, compilers, and the kernel that make it all work
+(thanks GNU, thanks Linux). Thanks to everyone who has played a part!
+
+Here is an incomplete list of other people that have made
+contributions to Notmuch (whether by code, bug reporting/fixes,
+ideas, inspiration, testing or feedback):
+
+Martin Krafft
+Keith Packard
+Jamey Sharp
diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..6dea693
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,15 @@
+Notmuch is free software.
+
+You can redistribute it and/or modify it under the terms of the GNU
+General Public License as published by the Free Software Foundation,
+either version 3 of the License, or (at your option) any later
+version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program, (in the COPYING-GPL-3 file in this
+directory). If not, see http://www.gnu.org/licenses/ 
diff --git a/COPYING-GPL-3 b/COPYING-GPL-3
new file mode 100644 (file)
index 0000000..4432540
--- /dev/null
@@ -0,0 +1,676 @@
+
+                   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>.
+
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..bc7bc67
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,86 @@
+Build and install instructions for Notmuch.
+
+Compilation commands
+--------------------
+The process for compiling and installing Notmuch is the very standard
+sequence of:
+
+       ./configure
+       make
+       sudo make install
+
+In fact, if you don't plan to pass any arguments to the configure
+script, then you can skip that step and just start with "make", (which
+will call configure for you). See this command:
+
+       ./configure --help
+
+for detailed documentation of the things you can control at the
+configure stage.
+
+notmuch.el installation
+-----------------------
+Installing the notmuch.el emacs lisp function systemwide:
+
+       sudo make install-emacs
+
+Each user needs to add (require 'notmuch) in his ~/.emacs to make it
+available and then start emacs running notmuch with "emacs -f notmuch"
+or start notmuch from within emacs with "M-x notmuch".
+
+Dependencies
+------------
+Notmuch depends on three libraries: Xapian, GMime 2.4, and Talloc
+which are each described below:
+
+       Xapian
+       ------
+       Xapian is the search-engine library underlying Notmuch.
+
+       It provides all the real machinery of indexing and searching,
+       (including the very nice parsing of the query string).
+
+       Xapian is available from http://xapian.org
+
+       After installing Xapian, please ensure that you have a command
+       named "xapian-config" on your $PATH as notmuch expects. (At
+       least one notmuch user found that Xapian installed the config
+       program to /usr/local/bin/xapian-config-1.1 ).
+
+       GMime 2.4
+       ---------
+       GMime 2.4 provides decoding of MIME email messages for Notmuch.
+
+       Without GMime, Notmuch would not be able to extract and index
+       the actual text from email message encoded as BASE64, etc.
+
+       GMime 2.4 is available from http://spruce.sourceforge.net/gmime/
+
+       Talloc
+       ------
+       Talloc is a memory-pool allocator used by Notmuch.
+
+       Talloc is an extremely lightweight and easy-to-use tool for
+       allocating memory in a hierarchical fashion and then freeing
+       it with a single call of the top-level handle. Using it has
+       made development of Notmuch much easier and much less prone to
+       memory leaks.
+
+       Talloc is available from http://talloc.samba.org/
+
+On a modern, package-based operating system you can install all of the
+dependencies with a simple simple command line. For example:
+
+  For Debian and similar:
+
+        sudo apt-get install libxapian-dev libgmime-2.4-dev libtalloc-dev
+
+  For Fedora and similar:
+
+       sudo yum install xapian-core-devel gmime-devel libtalloc-devel
+
+On other systems, a similar command can be used, but the details of
+the package names may be different.
+
+       
+
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..619392d
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+# We want the all target to be the implicit target (if no target is
+# given explicitly on the command line) so mention it first.
+all:
+
+# List all subdirectories here. Each contains its own Makefile.local
+subdirs = compat completion emacs lib
+
+# We make all targets depend on the Makefiles themselves.
+global_deps = Makefile Makefile.config Makefile.local \
+       $(subdirs:%=%/Makefile) $(subdirs:%=%/Makefile.local)
+
+# Sub-directory Makefile.local fragments can append to these variables
+# to have directory-specific cflags as necessary.
+
+extra_cflags :=
+extra_cxxflags :=
+
+# Get settings from the output of configure by running it to generate
+# Makefile.config if it doesn't exist yet. And add Makefile.config to
+# our global dependency list.
+include Makefile.config
+Makefile.config: configure
+       @echo ""
+       @echo "Note: Calling ./configure with no command-line arguments. This is often fine,"
+       @echo "      but if you want to specify any arguments (such as an alternate prefix"
+       @echo "      into which to install), call ./configure explicitly and then make again."
+       @echo "      See \"./configure --help\" for more details."
+       @echo ""
+       ./configure
+
+# Finally, include all of the Makefile.local fragments where all the
+# real work is done.
+
+include $(subdirs:%=%/Makefile.local) Makefile.local
diff --git a/Makefile.local b/Makefile.local
new file mode 100644 (file)
index 0000000..5bb570b
--- /dev/null
@@ -0,0 +1,289 @@
+# -*- makefile -*-
+
+# Here's the (hopefully simple) versioning scheme.
+#
+# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We
+# increment the second digit for each release and increment the first
+# digit when we reach particularly major milestones of usability.
+#
+# Between releases, (such as when compiling notmuch from the git
+# repository), we let git append identification of the actual commit.
+PACKAGE=notmuch
+
+IS_GIT=$(shell if [ -d .git ] ; then echo yes ; else echo no; fi)
+
+VERSION:=$(shell cat version)
+ifneq ($(MAKECMDGOALS),release)
+ifneq ($(MAKECMDGOALS),release-message)
+ifeq ($(IS_GIT),yes)
+VERSION:=$(shell git describe --match '[0-9.]*')
+endif
+endif
+endif
+
+RELEASE_HOST=notmuchmail.org
+RELEASE_DIR=/srv/notmuchmail.org/www/releases
+RELEASE_URL=http://notmuchmail.org/releases
+TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz
+SHA1_FILE=$(TAR_FILE).sha1
+GPG_FILE=$(SHA1_FILE).asc
+
+# Smash together user's values with our extra values
+FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags)
+FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags)
+FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Llib -lnotmuch
+FINAL_NOTMUCH_LINKER = CC
+ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1)
+FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS)
+FINAL_NOTMUCH_LINKER = CXX
+endif
+FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(CONFIGURE_LDFLAGS)
+
+.PHONY: all
+all: notmuch notmuch-shared notmuch.1.gz
+ifeq ($(MAKECMDGOALS),)
+ifeq ($(shell cat .first-build-message 2>/dev/null),)
+       @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all
+       @echo ""
+       @echo "Compilation of notmuch is now complete. You can install notmuch with:"
+       @echo ""
+       @echo " make install"
+       @echo ""
+       @echo "Note that depending on the prefix to which you are installing"
+       @echo "you may need root permission (such as \"sudo make install\")."
+       @echo "See \"./configure --help\" for help on setting an alternate prefix."
+       @echo Printed > .first-build-message
+endif
+endif
+
+.PHONY: test
+test: all
+       @./test/notmuch-test
+
+$(TAR_FILE):
+       git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ HEAD > $(TAR_FILE).tmp
+       echo $(VERSION) > version.tmp
+       tar --append -f $(TAR_FILE).tmp --transform s_^_$(PACKAGE)-$(VERSION)/_  --transform 's_.tmp$$__' version.tmp
+       rm version.tmp
+       gzip < $(TAR_FILE).tmp > $(TAR_FILE)
+       @echo "Source is ready for release in $(TAR_FILE)"
+
+$(SHA1_FILE): $(TAR_FILE)
+       sha1sum $^ > $@
+
+$(GPG_FILE): $(SHA1_FILE)
+       @echo "Please enter your GPG password to sign the checksum."
+       gpg --armor --sign $^ 
+
+.PHONY: dist
+dist: $(TAR_FILE)
+
+# We invoke make recursively only to force ordering of our phony
+# targets in the case of parallel invocation of make (-j).
+#
+# We carefully ensure that our VERSION variable is passed down to any
+# sub-ordinate make invocations (which won't otherwhise know that they
+# are part of the release and need to take the version from the
+# version file).
+.PHONY: release
+release: verify-source-tree-and-version
+       $(MAKE) VERSION=$(VERSION) verify-newer
+       $(MAKE) VERSION=$(VERSION) test
+       rm -rf ./debian-build
+       git-buildpackage
+       cp debian-build/notmuch_$(VERSION).tar.gz notmuch-$(VERSION).tar.gz
+       $(MAKE) VERSION=$(VERSION) $(GPG_FILE)
+       scp $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) $(RELEASE_HOST):$(RELEASE_DIR)
+       ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-[0-9]* && ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(PACKAGE)-$(VERSION)"
+       mkdir -p releases
+       mv $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) releases
+       (cd debian-build; dput *.changes)
+       mv debian-build/* releases
+       rmdir debian-build
+       git tag -s -m "$(PACKAGE) $(VERSION) release" $(VERSION)
+       git push origin $(VERSION)
+       $(MAKE) VERSION=$(VERSION) release-message > $(PACKAGE)-$(VERSION).announce
+       @echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template."
+
+.PHONY: release-message
+release-message:
+       @echo "To: notmuch@notmuchmail.org"
+       @echo "Subject: $(PACKAGE) release $(VERSION) now available"
+       @echo ""
+       @echo "Where to obtain notmuch $(VERSION)"
+       @echo "==========================="
+       @echo "  $(RELEASE_URL)/$(TAR_FILE)"
+       @echo ""
+       @echo "Which can be verified with:"
+       @echo ""
+       @echo "  $(RELEASE_URL)/$(SHA1_FILE)"
+       @echo -n "  "
+       @cat releases/$(SHA1_FILE)
+       @echo ""
+       @echo "  $(RELEASE_URL)/$(GPG_FILE)"
+       @echo "  (signed by `getent passwd "$$USER" | cut -d: -f 5 | cut -d, -f 1`)"
+       @echo ""
+       @echo "What's new in notmuch $(VERSION)"
+       @echo "========================="
+       @sed -ne '/^[Nn]otmuch $(VERSION)/{n;n;b NEWS}; d; :NEWS /^===/q; {p;n;b NEWS}' < NEWS | head -n -2
+       @echo ""
+       @echo "What is notmuch"
+       @echo "==============="
+       @echo "Notmuch is a system for indexing, searching, reading, and tagging"
+       @echo "large collections of email messages in maildir or mh format. It uses"
+       @echo "the Xapian library to provide fast, full-text search with a convenient"
+       @echo "search syntax."
+       @echo ""
+       @echo "For more about notmuch, see http://notmuchmail.org"
+
+# This is a chain of dependencies rather than a simple list simply to
+# avoid the messages getting interleaved in the case of a parallel
+# make invocation.
+.PHONY: verify-source-tree-and-version
+verify-source-tree-and-version: verify-no-dirty-code
+
+.PHONY: verify-no-dirty-code
+verify-no-dirty-code: verify-version-debian
+ifeq ($(IS_GIT),yes)
+       @printf "Checking that source tree is clean..."
+ifneq ($(shell git ls-files -m),)
+       @echo "No"
+       @echo "The following files have been modified since the most recent git commit:"
+       @echo ""
+       @git ls-files -m
+       @echo ""
+       @echo "The release will be made from the committed state, but perhaps you meant"
+       @echo "to commit this code first? Please clean this up to make it more clear."
+       @false
+else
+       @echo "Good"
+endif
+endif
+
+.PHONY: verify-version-debian
+verify-version-debian: verify-version-components
+       @echo -n "Checking that Debian package version is $(VERSION)..."
+       @if [ "$(VERSION)" != $$(dpkg-parsechangelog | grep ^Version | awk '{print $$2}') ] ; then \
+               (echo "No." && \
+                echo "Please edit version and debian/changelog to have consistent versions." && false) \
+        fi
+       @echo "Good."
+
+.PHONY: verify-version-components
+verify-version-components:
+       @echo -n "Checking that $(VERSION) consists only of digits and periods..."
+       @if echo $(VERSION) | grep -q -v -x '[0-9.]*'; then \
+               (echo "No." && \
+                echo "Please follow the instructions in RELEASING to choose a version" && false) \
+        else :; fi
+       @echo "Good."
+
+.PHONY: verify-newer
+verify-newer:
+       @echo -n "Checking that no $(VERSION) release already exists..."
+       @ssh $(RELEASE_HOST) test ! -e $(RELEASE_DIR)/$(TAR_FILE) \
+               || (echo "Ouch." && echo "Found: $(RELEASE_HOST):$(RELEASE_DIR)/$(TAR_FILE)" \
+               && echo "Refusing to replace an existing release." \
+               && echo "Don't forget to update \"version\" as described in RELEASING before release." && false)
+       @echo "Good."
+
+# The user has not set any verbosity, default to quiet mode and inform the
+# user how to enable verbose compiles.
+ifeq ($(V),)
+quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n"
+quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+endif
+# The user has explicitly enabled quiet compilation.
+ifeq ($(V),0)
+quiet = @printf "$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//'))
+endif
+# Otherwise, print the full command line.
+quiet ?= $($(shell echo $1 | sed -e s'/ .*//'))
+
+%.o: %.cc $(global_deps)
+       $(call quiet,CXX $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@
+
+%.o: %.c $(global_deps)
+       $(call quiet,CC $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@
+
+.deps/%.d: %.c $(global_deps)
+       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
+       $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \
+       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
+       rm -f $@.$$$$
+
+.deps/%.d: %.cc $(global_deps)
+       @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \
+       $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \
+       sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \
+       rm -f $@.$$$$
+
+DEPS := $(SRCS:%.c=.deps/%.d)
+DEPS := $(DEPS:%.cc=.deps/%.d)
+-include $(DEPS)
+
+.PHONY : clean
+clean:
+       rm -f $(CLEAN); rm -rf .deps
+
+# We don't (yet) have any distributed files not in the upstream repository.
+# So distclean is currently identical to clean.
+.PHONY: distclean
+distclean: clean
+
+notmuch_client_srcs =          \
+       debugger.c              \
+       gmime-filter-reply.c    \
+       gmime-filter-headers.c  \
+       notmuch.c               \
+       notmuch-config.c        \
+       notmuch-count.c         \
+       notmuch-dump.c          \
+       notmuch-new.c           \
+       notmuch-reply.c         \
+       notmuch-restore.c       \
+       notmuch-search.c        \
+       notmuch-search-tags.c   \
+       notmuch-setup.c         \
+       notmuch-show.c          \
+       notmuch-tag.c           \
+       notmuch-time.c          \
+       query-string.c          \
+       show-message.c          \
+       json.c
+
+notmuch_client_modules = $(notmuch_client_srcs:.c=.o)
+
+notmuch: $(notmuch_client_modules) lib/libnotmuch.a
+       $(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@
+
+notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME)
+       $(call quiet,$(FINAL_NOTMUCH_LINKER) $(CFLAGS)) $(notmuch_client_modules) $(FINAL_NOTMUCH_LDFLAGS) -o $@
+
+notmuch.1.gz: notmuch.1
+       gzip --stdout $^ > $@
+
+.PHONY: install
+install: all notmuch.1.gz
+       mkdir -p $(DESTDIR)$(mandir)/man1
+       install -m0644 notmuch.1.gz $(DESTDIR)$(mandir)/man1/
+       mkdir -p $(DESTDIR)$(prefix)/bin/
+       install notmuch-shared $(DESTDIR)$(prefix)/bin/notmuch
+ifeq ($(MAKECMDGOALS), install)
+       @echo ""
+       @echo "Notmuch is now installed to $(DESTDIR)$(prefix)"
+       @echo ""
+       @echo "To run notmuch from emacs, each user should add the following line to ~/.emacs:"
+       @echo ""
+       @echo " (require 'notmuch)"
+       @echo ""
+       @echo "And should then run \"M-x notmuch\" from within emacs or run \"emacs -f notmuch\""
+endif
+
+.PHONY: install-desktop
+install-desktop:
+       mkdir -p $(DESTDIR)$(desktop_dir)
+       desktop-file-install --mode 0644 --dir $(DESTDIR)$(desktop_dir) notmuch.desktop
+
+SRCS  := $(SRCS) $(notmuch_client_srcs)
+CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc notmuch.1.gz
diff --git a/NEWS b/NEWS
new file mode 100644 (file)
index 0000000..eba0fd5
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,150 @@
+Notmuch 0.2 (2010-04-16)
+========================
+This is the second release of the notmuch mail system, with actual
+detailed release notes this time!
+
+This release consists of a number of minor new features that make
+notmuch more pleasant to use, and a few fairly major bug fixes.
+
+We didn't quite hit our release target of "about a week" from the 0.1
+release, (0.2 is happening 11 days after 0.1), but we hope to do
+better for next week. Look forward to some major features coming to
+notmuch in subsequent releases.
+
+-Carl
+
+General features
+----------------
+Better guessing of From: header.
+
+  Notmuch now tries harder to guess which configured address should be
+  used as the From: line in a "notmuch reply". It will examine the
+  Received: headers if it fails to find any configured address in To:
+  or Cc:. This allows it to often choose the correct address even when
+  replying to a message sent to a mailing list, and not directly to a
+  configured address.
+
+Make "notmuch count" with no arguments count all messages
+
+  Previously, it was hard to construct a search term that was
+  guaranteed to match all messages.
+
+Provide a new special-case search term of "*" to match all messages.
+
+  This can be used in any command accepting a search term, such as
+  "notmuch search '*'". Note that you'll want to take care that the
+  shell doesn't expand * against the current files. And note that the
+  support for "*" is a special case. It's only meaningful as a single
+  search term and loses its special meaning when combined with any
+  other search terms.
+
+Automatically detect thread connections even when a parent message is
+missing.
+
+  Previously, if two or more message were received with a common
+  parent, but that parent was not received, then these messages would
+  not be recognized as belonging to the same thread. This is now fixed
+  so that such messages are properly connected in a thread.
+
+General bug fixes
+-----------------
+Fix potential data loss in "notmuch new" with SIGINT
+
+  One code path in "notmuch new" was not properly handling
+  SIGINT. Previously, this could lead to messages being removed from
+  the database (and their tags being lost) if the user pressed
+  Control-C while "notmuch new" was working.
+
+Fix segfault when a message includes a MIME part that is empty.
+
+Fix handling of non-ASCII characters with --format=json
+
+  Previously, characters outside the range of 7-bit ASCII were
+  silently dropped from the JSON output. This led to corrupted display
+  of utf-8 content in the upcoming notmuch web-based frontends.
+
+Fix headers to be properly decoded in "notmuch reply"
+
+  Previously, the user might see:
+
+       Subject: Re: =?iso-8859-2?q?Rozlu=E8ka?=
+
+  rather than:
+
+       Subject: Re: Rozlučka
+
+  The former text is properly encoded to be RFC-compliant SMTP, will
+  be sent correctly, and will be properly decoded by the
+  recipient. But the user trying to edit the reply would likely be
+  unable to read or edit that field in its encoded form.
+
+Emacs client features
+---------------------
+Show the last few lines of citations as well as the first few lines.
+
+  It's often the case that the last sentence of a citation is what is
+  being replied to directly, so the last few lines are often much more
+  important. The number of lines shown at the beginning and end of any
+  citation can be configured, (notmuch-show-citation-lines-prefix and
+  notmuch-show-citation-lines-suffix).
+
+The '+' and '-' commands in the search view can now add and remove
+tags by region.
+
+  Selective bulk tagging is now possible by selecting a region of
+  threads and then using either the '+' or '-' keybindings. Bulk
+  tagging is still available for all threads matching the current
+  search with th '*' binding.
+
+More meaningful buffer names for thread-view buffers.
+
+  Notmuch now uses the Subject of the thread as the buffer
+  name. Previously it was using the thread ID, which is a meaningless
+  number to the user.
+
+Provide for customized colors of threads in search view based on tags.
+
+  See the documentation of notmuch-search-line-faces, (or us "M-x
+  customize" and browse to the "notmuch" group within "Applications"
+  and "Mail"), for details on how to configure this colorization.
+
+Build-system features
+---------------------
+Add support to properly build libnotmuch on Darwin systems (OS X).
+
+Add support to configure for many standard options.
+
+  We include actual support for:
+
+       --includedir --mandir --sysconfdir
+
+  And accept and silently ignore several more:
+
+       --build --infodir --libexecdir --localstatedir
+       --disable-maintainer-mode --disable-dependency-tracking
+
+Install emacs client in "make install" rather than requiring a
+separate "make install-emacs".
+
+Automatically compute versions numbers between releases.
+
+  This support uses the git-describe notation, so a version such as
+  0.1-144-g43cbbfc indicates a version that is 144 commits since the
+  0.1 release and is available as git commit "43cbbfc".
+
+Add a new "make test" target to run the test suite and actually verify
+its results.
+
+Notmuch 0.1 (2010-04-05)
+========================
+This is the first release of the notmuch mail system.
+
+It includes the libnotmuch library, the notmuch command-line
+interface, and an emacs-based interface to notmuch.
+
+Note: Notmuch will work best with Xapian 1.0.18 (or later) or Xapian
+1.1.4 (or later). Previous versions of Xapian (whether 1.0 or 1.1) had
+a performance bug that made notmuch very slow when modifying
+tags. This would cause distracting pauses when reading mail while
+notmuch would wait for Xapian when removing the "inbox" and "unread"
+tags from messages in a thread.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..5f029c8
--- /dev/null
+++ b/README
@@ -0,0 +1,71 @@
+Notmuch - thread-based email index, search and tagging.
+
+Notmuch is a system for indexing, searching, reading, and tagging
+large collections of email messages in maildir or mh format. It uses
+the Xapian library to provide fast, full-text search with a convenient
+search syntax.
+
+Notmuch is free software, released under the GNU General Public
+License version 3 (or later).
+
+Building notmuch
+----------------
+See the INSTALL file for notes on compiling and installing notmuch.
+
+Contacting users and developers
+-------------------------------
+The website for Notmuch is:
+
+       http://notmuchmail.org
+
+The mailing list address for the notmuch community is:
+
+       notmuch@notmuchmail.org
+
+We welcome any sort of questions, comments, kudos, or code there.
+
+Subscription is not required, (but if you do subscribe you'll avoid
+any delay due to moderation). See the website for subscription
+information.
+
+There is also an IRC channel dedicated to talk about using and
+developing notmuch:
+
+       IRC server:     irc.freenode.net
+       Channel:        #notmuch
+
+Interface options
+-----------------
+Notmuch includes a "notmuch" command-line interface to the system.
+This is not a very convenient interface and it is not expected that
+users will find it satisfying. Instead, there are two option for
+obtaining a more sophisticated interface:
+
+     1. Build on top of the "notmuch" command-line interface.
+
+       This might be a reasonable option for a very text-oriented
+       client environment. For example, an emacs-based interface for
+       notmuch is already under development and is available in the
+       notmuch.el file in this distribution.
+
+       If someone were to write a curses-based interface, or similar,
+       it might also be reasonable to build on the "notmuch"
+       command-line interface.
+
+     2. Build on top of the notmuch library interface.
+
+       This is a better choice for developing an interface that has
+       full control of the presentation of email threads and
+       messages. It is expected that anyone integrating Notmuch into
+       an existing, graphical email program use the notmuch library
+       interface.
+
+       The public interface to the notmuch library is contained in
+       the notmuch.h header file. The "notmuch" command-line program
+       in notmuch.c can be used as good example code, since it is a
+       simple program that is written on top of the library
+       interface.
+
+As can be seen, alternate interfaces to the Notmuch mail system are
+still in development. We would appreciate any contributions to these
+efforts.
diff --git a/RELEASING b/RELEASING
new file mode 100644 (file)
index 0000000..d314a6c
--- /dev/null
+++ b/RELEASING
@@ -0,0 +1,105 @@
+Here are the steps to follow to create a new notmuch release.
+
+These steps assume that a process (not described here) has already
+been followed to determine the features and bug fixes to be included
+in a release, and that adequate testing by the community has already
+been performed. The little bit of testing performed here is a safety
+check, and not a substitute for wider testing.
+
+OK, so the code to be released is present and committed to your git
+repository. From here, there are just a few steps to release:
+
+1) Verify that the NEWS file is up to date.
+
+       Read through the entry at the top of the NEWS file and see if
+       you are aware of any major features recently added that are
+       not mentioned there. If so, pleas add them, (and ask the
+       authors of the commits to update NEWS in the future).
+
+2) Verify that the library version in lib/Makefile.local is correct
+
+       See the instructions there for how to increment it.
+
+       The version should have been updated with any commits that
+       added API, but do check that that is the case. The command
+       below can be useful for inspecting header-file changes since
+       the last release X.Y:
+
+               git diff X.Y..HEAD -- lib/notmuch.h
+
+       Note: We currently don't plan to increment
+       LIBNOTMUCH_VERSION_MAJOR beyond 1, so if there *are*
+       incompatible changes to the library interface, then
+       stop. Don't release. Figure out the plan on the notmuch
+       mailing list.
+
+       Commit this change, if any.
+
+3) Upgrade the version in the file "version"
+
+       The scheme for the release number is as follows:
+
+       A major milestone in usability causes an increase in the major
+       number, yielding a two-component version with a minor number
+       of 0, (such as "1.0" or "2.0").
+
+       Otherwise, releases with changes in features cause an increase
+       in the minor number, yielding a two-component version, (such
+       as "1.1" or "1.2").
+
+       Finally, releases that do not change "features" but are merely
+       bug fixes either add increase the micro number or add it
+       (starting at ".1" if not present). So a bug-fix release from
+       "1.0" would be "1.0.1" and a subsequent bug-fix release would
+       be "1.0.2" etc.
+
+       Commit this change.
+
+4) Create an entry for the new release in debian/changelog
+
+       The syntax of this file is tightly restricted, but the
+       available emacs mode (see the dpkg-dev-el package helps).  The
+       entries here will be the Debian-relevant single-line
+       description of changes from the NEWS entry. And the version
+       must match the version in the next step.
+
+       Commit this change.
+
+       XXX: It would be great if this step were automated as part of
+       release, (taking entries from NEWS and the version from the
+       version file, and creating a new commit, etc.)
+
+5) Run "make release" which will perform the following steps.
+
+   Note: If any problem occurs during the process, (such as a lintian
+   warning that you decide should be fixed), you can abort at the
+   prompt for your GPG passphrase and nothing will have been uploaded
+   yet.
+
+       * Ensure that the version consists only of digits and periods
+       * Ensure that version and debian/changelog have the same version
+       * Verify that the source tree is clean
+       * Compile the current notmuch code (aborting release if it fails)
+       * Run the notmuch test suite (aborting release if it fails)
+       * Compile a Debian package
+       * Copy the tar file from what was made for Debian package
+       * Generate a .sha1 sum file for the tar file
+       * Sign the sha1sum using your GPG setup (asks for your GPG password)
+       * Check that no release exists with the current version
+       * scp the three files to appear on http://notmuchmail.org/releases
+       * Create a LATEST-notmuch-version file (after deleting any old one)
+       * Place local copies of the tar, sha1, and gpg files into releases
+       * Upload the Debian package
+       * Place a local copy of the Debian package files in releases
+       * Tag the entire source tree with a tag of the form X.Y.Z, and sign
+         the tag with your GPG key (asks for your GPG password, and you
+         may need to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL to match
+         your public-key's setting or this fails.)
+       * Push that tag
+       * Provide some text for the release announcement (see below).
+
+6) Send a message to notmuch@notmuchmail.org to announce the release.
+
+       Use the text provided from "make release" above, (if for some
+       reason you lose this message, "make release-message" prints
+       it again for you.
diff --git a/TODO b/TODO
new file mode 100644 (file)
index 0000000..8312db1
--- /dev/null
+++ b/TODO
@@ -0,0 +1,293 @@
+Fix the things that are causing the most pain to new users
+----------------------------------------------------------
+1. A new import is tagging all messages as "inbox" -- total pain
+
+2. Allow an easy way to get tags from directory names (if the user has them)
+
+Emacs interface (notmuch.el)
+----------------------------
+Enhance '+' and '-' in the search view to operate on an entire region
+if set.
+
+Fix '*' to work by simply calling '+' or '-' on a region consisting of
+the entire buffer.
+
+Add a global keybinding table for notmuch, and then view-specific
+tables that add to it.
+       
+Add a '|' binding from the search view.
+
+Add support for choosing from one of the user's configured email
+addresses for the From line.
+
+Make 'notmuch-show-pipe-message have a private history.
+
+Add support for a delete keybinding that adds a "deleted" tag to the
+current message/thread and make searches not return deleted messages
+by default, (unless the user asks explicitly for deleted messags in
+the search query).
+
+Add support to "mute" a thread (add a "muted" tag and then don't
+display threads in searches by default where any message of the thread
+has the "muted" tag).
+
+Fix i-search to open up invisible citations as necessary.
+
+Make '=' count from the end rather than from the beginning if more
+than half-way through the buffer.
+
+Fix to automatically wrap long headers (for RFC compliance) before
+sending. This should probably just be fixed in message-mode itself,
+(but perhaps we can have a notmuch-message-mode that layers this on
+top).
+
+Implement Fcc and use it for all messages, (whether a new composition,
+a reply, or a forward). This again may require a notmuch-message-mode
+that extends message-mode.
+
+Stop hiding the headers so much in the thread-view mode.
+
+Allow opening a message in thread-view mode by clicking on either
+line.
+
+Automatically open a message when navigating to it with N or P.
+
+Change 'a' command in thread-view mode to only archive open messages.
+
+Add a binding to open all closed messages.
+
+Emacs saved-search interface
+----------------------------
+Here's a proposal Carl wrote (id:87einafy4u.fsf@yoom.home.cworth.org):
+
+  So what I'm imagining for the default notmuch view is something like
+  this:
+
+          Welcome to notmuch.
+
+              Notmuch search: _________________________________________
+
+          Saved searches:
+
+              55,342      All messages
+                  22      Inbox
+
+          Recent searches:
+
+                   1      from:"someone special" and tag:unread
+                  34      tag:notmuch and tag:todo
+
+          Click (or press Enter) on any search to see the results.
+          Right-click (or press Space) on any recent search to save it.
+
+  So the "saved searches" portion of the view is basically just what
+  notmuch-folder displays now. Above that there's an obvious place to
+  start a new search, (in a slightly more "web-browser-like" way than the
+  typical mini-buffer approach).
+
+  All recent searches appear in the list at the bottom automatically, and
+  there's the documented mechanism for saving a search, (giving it a name
+  and having it appear above).
+
+Portability
+-----------
+Fix configure script to test each compiler warning we want to use.
+
+Completion
+----------
+Fix bash completion to complete multiple search options (both --first
+and *then* --max-threads), and also complete value for --sort=
+(oldest-first or newest-first).
+
+notmuch command-line tool
+-------------------------
+Fix the --format=json option to not imply --entire-thread.
+
+Implement "notmuch search --exclude-threads=<search-terms>" to allow
+for excluding muted threads, (and any other negative, thread-based
+filtering that the user wants to do).
+
+Fix "notmuch show" so that the UI doesn't fail to show a thread that
+is visible in a search buffer, but happens to no longer match the
+current search. (Perhaps add a --matching=<secondary-search-terms>
+option (or similar) to "notmuch show".) For now, this is being worked
+around in the emacs interface by noticing that "notmuch show" returns
+nothing and re-rerunning the command without the extra arguments.
+
+Teach "notmuch search" to return many different kinds of results. Some
+ideas:
+
+       notmuch search --output=threads # Default if no --output is given
+       notmuch search --output=messages
+       notmuch search --output=tags
+       notmuch search --output=addresses
+       notmuch search --output=terms
+
+Add a "--format" option to "notmuch search", (something printf-like
+for selecting what gets printed).
+
+Add a "--count-only" (or so?) option to "notmuch search" for returning
+the count of search results.
+
+Give "notmuch restore" some progress indicator.
+
+Fix "notmuch restore" to operate in a single pass much like "notmuch
+dump" does, rather than doing N searches into the database, each
+matching 1/N messages.
+
+Add a "-f <filename>" option to select an alternate configuration
+file.
+
+Allow configuration for filename patterns that should be ignored when
+indexing.
+
+Replace the "notmuch part --part=id" command with "notmuch show
+--part=id", (David Edmonson wants to rewrite some of "notmuch show" to
+provide more MIME-structure information in its output first).
+
+Replace the "notmuch search-tags" command with "notmuch search
+--output=tags".
+
+Fix to avoid this ugly message:
+
+       (process:17197): gmime-CRITICAL **: g_mime_message_get_mime_part: assertion `GMIME_IS_MESSAGE (message)' failed
+       Warning: Not indexing empty mime part.
+
+  This probably means adding a test case to generate that message,
+  filing an upstream bug against GMime, and then silencing the
+  notmuch-generated portion of the warning (so that once GMime is
+  fixed, this is all silent).
+
+Simplify notmuch-reply to simply print the headers (we have the
+original values) rather than calling GMime (which encodes) and adding
+the confusing gmime-filter-headers.c code (which decodes).
+
+notmuch library
+---------------
+Add an interface to accept a "key" and a byte stream, rather than a
+filename.
+
+Provide a sane syntax for date ranges. First, we don't want to require
+both endpoints to be specified. For example it would be nice to be
+able to say things like "since:2009-01-1" or "until:2009-01-1" and
+have the other endpoint be implicit. Second we'd like to support
+relative specifications of time such as "since:'2 months ago'". To do
+any of this we're probably going to need to break down an write our
+own parser for the query string rather than using Xapian's QueryParser
+class.
+
+Make failure to read a file (such as a permissions problem) a warning
+rather than an error (should be similar to the existing warning for a
+non-mail file).
+
+Actually compile and install a libnotmuch shared library.
+
+Fix to use the *last* Message-ID header if multiple such headers are
+encountered, (I noticed this is one thing that kept me from seeing the
+same message-ID values as sup).
+
+Add support for configuring "virtual tags" which are a tuple of
+(tag-name, search-specification). The database is responsible for
+ensuring that the virtual tag is always consistent.
+
+Indicate to the user if two files with the same message ID have
+content that is actually different in some interesting way. Perhaps
+notmuch initially sees all changes as interesting, and quickly learns
+from the user which changes are not interesting (such as the very
+common mailing-list footer).
+
+Fix notmuch_query_count_messages to share code with
+notmuch_query_search_messages rather than duplicating code. (And
+consider renaming it as well.)
+
+Provide a mechanism for doing automatic address completion based on
+notmuch searches. Here was one proposal made in IRC:
+
+       <cworth> I guess all it would really have to be would be a way
+                to configure a series of searches to try in turn,
+                (presenting ambiguities at a given single level, and
+                advancing to the next level only if one level
+                returned no matches).
+       <cworth> So then I might have a series that looks like this:
+       <cworth> notmuch search --output=address_from tag:address_book_alias
+       <cworth> notmuch search --output=address_to tag:sent
+       <cworth> notmuch search --output=address_from
+       <cworth> I think I might like that quite a bit.
+       <cworth> And then we have a story for an address book for
+                non-emacs users.
+
+Provide a ~me Xapian synonym for all of the user's configured email
+addresses.
+
+Add symbol hiding so that we don't risk leaking any private symbols
+into the shared-library interface.
+
+Audit all libnotmuch entry points to ensure that all Xapian calls are
+wrapped in a try/catch block.
+
+Search syntax
+-------------
+Implement support for "tag:*" to expand to all tags.
+
+Fix "notmuch search to:" to be less confusing. Many users expect this
+to search for all messages with a To: header, but it instead searches
+for all messages with the word "to". If we don't provide the first
+behavior, perhaps we should exit on an error when a configured prefix
+is provided with no value?
+
+Support "*" in all cases and not just as a special case. That is, "* "
+should also work, as well as "* and tag:inbox".
+
+Implement a syntax for requesting set-theoertic operations on results
+of multiple searches. For example, I would like to do:
+
+       "tag:inbox" SET-SUBTRACT "tag:muted"
+
+    as well as:
+
+       "tag:notmuch and <date-range>" SET-INTERSECT
+       "tag:notmuch and not (tag:merged or tag:postponed)"
+
+    See id:3wdpr282yz2.fsf@testarossa.amd.com for more details on the
+    use cases of the above.
+
+Database changes
+----------------
+Store a reference term for every message-id that appears in
+References. We just started doing this for newly-added documents, but
+at the next convenient database-schema upgrade, we should go back and
+fix old messages to be consistent.
+
+Start indexing the List-Id header, (and re-index this header for
+existing messages at the next database upgrade).
+
+Start indexing the message file's directory ana make it available for
+search as "folder:" (and re-index this value for existing messages at
+the next database upgrade).
+
+Add support for the user to specify custom headers to be indexed (and
+re-index these for existing messages at the next database upgrade).
+
+Test suite
+----------
+Start testing --format=json.
+
+Achieve 100% test coverage with the test suite.
+
+Modularize test suite (to be able to run individual tests).
+
+Summarize test results at the end.
+
+Fix the insane quoting nightmare of the test suite, (and once we do
+that we can actually test the implicit-phrase search feature such as
+"notmuch search 'body search (phrase)'"
+
+Test "notmuch reply" choosing the correct email address from the
+Received header when no configured email address appears in To or Cc.
+
+General
+-------
+Audit everything for dealing with out-of-memory (and drop xutil.c).
+
+Investigate why the notmuch database is slightly larger than the sup
+database for the same corpus of email.
diff --git a/compat/Makefile b/compat/Makefile
new file mode 100644 (file)
index 0000000..fa25832
--- /dev/null
@@ -0,0 +1,5 @@
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/compat/Makefile.local b/compat/Makefile.local
new file mode 100644 (file)
index 0000000..653fc71
--- /dev/null
@@ -0,0 +1,14 @@
+# -*- makefile -*-
+
+dir := compat
+extra_cflags += -I$(dir)
+
+notmuch_compat_srcs :=
+
+ifneq ($(HAVE_GETLINE),1)
+notmuch_compat_srcs += $(dir)/getline.c $(dir)/getdelim.c
+endif
+
+ifneq ($(HAVE_STRCASESTR),1)
+notmuch_compat_srcs += $(dir)/strcasestr.c
+endif
diff --git a/compat/README b/compat/README
new file mode 100644 (file)
index 0000000..cd32c56
--- /dev/null
@@ -0,0 +1,16 @@
+notmuch/comapt
+
+This directory consists of two things:
+
+1. Small programs used by the notmuch configure script to test for the
+   availability of certain system features, (library functions, etc.).
+
+   For example: have_getline.c
+
+2. Compatibility implementations of those system features for systems
+   that don't provide their own versions.
+
+   For example: getline.c
+
+   The compilation of these files is made conditional on the output of
+   the test programs from [1].
diff --git a/compat/compat.h b/compat/compat.h
new file mode 100644 (file)
index 0000000..173ef68
--- /dev/null
@@ -0,0 +1,45 @@
+/* notmuch - Not much of an email library, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+/* This header file defines functions that will only be conditionally
+ * compiled for compatibility on systems that don't provide their own
+ * implementations of the functions.
+ */
+
+#ifndef NOTMUCH_COMPAT_H
+#define NOTMUCH_COMPAT_H
+
+#if !HAVE_GETLINE
+#include <stdio.h>
+#include <unistd.h>
+
+ssize_t
+getline (char **lineptr, size_t *n, FILE *stream);
+
+ssize_t
+getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp);
+
+#endif /* !HAVE_GETLINE */
+
+#if !HAVE_STRCASESTR
+char* strcasestr(const char *haystack, const char *needle);
+#endif /* !HAVE_STRCASESTR */
+
+#endif /* NOTMUCH_COMPAT_H */
diff --git a/compat/getdelim.c b/compat/getdelim.c
new file mode 100644 (file)
index 0000000..407f3d0
--- /dev/null
@@ -0,0 +1,133 @@
+/* getdelim.c --- Implementation of replacement getdelim function.
+   Copyright (C) 1994, 1996, 1997, 1998, 2001, 2003, 2005, 2006, 2007,
+   2008, 2009 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 3, or (at
+   your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+   02110-1301, USA.  */
+
+/* Ported from glibc by Simon Josefsson. */
+
+#include "compat.h"
+
+#include <stdio.h>
+
+#include <limits.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#ifndef SSIZE_MAX
+# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2))
+#endif
+
+#if USE_UNLOCKED_IO
+# include "unlocked-io.h"
+# define getc_maybe_unlocked(fp)       getc(fp)
+#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED
+# undef flockfile
+# undef funlockfile
+# define flockfile(x) ((void) 0)
+# define funlockfile(x) ((void) 0)
+# define getc_maybe_unlocked(fp)       getc(fp)
+#else
+# define getc_maybe_unlocked(fp)       getc_unlocked(fp)
+#endif
+
+/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and
+   NUL-terminate it).  *LINEPTR is a pointer returned from malloc (or
+   NULL), pointing to *N characters of space.  It is realloc'ed as
+   necessary.  Returns the number of characters read (not including
+   the null terminator), or -1 on error or EOF.  */
+
+ssize_t
+getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp)
+{
+  ssize_t result = -1;
+  size_t cur_len = 0;
+
+  if (lineptr == NULL || n == NULL || fp == NULL)
+    {
+      errno = EINVAL;
+      return -1;
+    }
+
+  flockfile (fp);
+
+  if (*lineptr == NULL || *n == 0)
+    {
+      char *new_lineptr;
+      *n = 120;
+      new_lineptr = (char *) realloc (*lineptr, *n);
+      if (new_lineptr == NULL)
+       {
+         result = -1;
+         goto unlock_return;
+       }
+      *lineptr = new_lineptr;
+    }
+
+  for (;;)
+    {
+      int i;
+
+      i = getc_maybe_unlocked (fp);
+      if (i == EOF)
+       {
+         result = -1;
+         break;
+       }
+
+      /* Make enough space for len+1 (for final NUL) bytes.  */
+      if (cur_len + 1 >= *n)
+       {
+         size_t needed_max =
+           SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX;
+         size_t needed = 2 * *n + 1;   /* Be generous. */
+         char *new_lineptr;
+
+         if (needed_max < needed)
+           needed = needed_max;
+         if (cur_len + 1 >= needed)
+           {
+             result = -1;
+             errno = EOVERFLOW;
+             goto unlock_return;
+           }
+
+         new_lineptr = (char *) realloc (*lineptr, needed);
+         if (new_lineptr == NULL)
+           {
+             result = -1;
+             goto unlock_return;
+           }
+
+         *lineptr = new_lineptr;
+         *n = needed;
+       }
+
+      (*lineptr)[cur_len] = i;
+      cur_len++;
+
+      if (i == delimiter)
+       break;
+    }
+  (*lineptr)[cur_len] = '\0';
+  result = cur_len ? (ssize_t) cur_len : result;
+
+ unlock_return:
+  funlockfile (fp); /* doesn't set errno */
+
+  return result;
+}
diff --git a/compat/getline.c b/compat/getline.c
new file mode 100644 (file)
index 0000000..222e0f6
--- /dev/null
@@ -0,0 +1,29 @@
+/* getline.c --- Implementation of replacement getline function.
+   Copyright (C) 2005, 2006, 2007 Free Software Foundation, Inc.
+
+   This program is free software; you can redistribute it and/or
+   modify it under the terms of the GNU General Public License as
+   published by the Free Software Foundation; either version 3, or (at
+   your option) any later version.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+   02110-1301, USA.  */
+
+/* Written by Simon Josefsson. */
+
+#include "compat.h"
+
+#include <stdio.h>
+
+ssize_t
+getline (char **lineptr, size_t *n, FILE *stream)
+{
+  return getdelim (lineptr, n, '\n', stream);
+}
diff --git a/compat/have_getline.c b/compat/have_getline.c
new file mode 100644 (file)
index 0000000..a8bcd17
--- /dev/null
@@ -0,0 +1,13 @@
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <sys/types.h>
+
+int main()
+{
+    ssize_t count = 0;
+    size_t n = 0;
+    char **lineptr = NULL;
+    FILE *stream = NULL;
+
+    count = getline(lineptr, &n, stream);
+}
diff --git a/compat/have_strcasestr.c b/compat/have_strcasestr.c
new file mode 100644 (file)
index 0000000..c0fb762
--- /dev/null
@@ -0,0 +1,10 @@
+#define _GNU_SOURCE
+#include <strings.h>
+
+int main()
+{
+    char *found;
+    const char *haystack, *needle;
+
+    found = strcasestr(haystack, needle);
+}
diff --git a/compat/strcasestr.c b/compat/strcasestr.c
new file mode 100644 (file)
index 0000000..62a3a54
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * slow simplistic reimplementation of strcasestr for systems that
+ * don't include it in their library
+ *
+ * based on a GPL implementation in OpenTTD found under GPL v2
+
+   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, version 2.
+
+   This program is distributed in the hope that it will be useful, but
+   WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+   General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program; if not, write to the Free Software
+   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+   02110-1301, USA.  */
+
+/* Imported into notmuch by Dirk Hohndel - original author unknown. */
+
+#include <string.h>
+
+#include "compat.h"
+
+char *strcasestr(const char *haystack, const char *needle)
+{
+       size_t hay_len = strlen(haystack);
+       size_t needle_len = strlen(needle);
+       while (hay_len >= needle_len) {
+               if (strncasecmp(haystack, needle, needle_len) == 0)
+                   return (char *) haystack;
+
+               haystack++;
+               hay_len--;
+       }
+
+       return NULL;
+}
diff --git a/completion/Makefile b/completion/Makefile
new file mode 100644 (file)
index 0000000..b6859ea
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makfefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/completion/Makefile.local b/completion/Makefile.local
new file mode 100644 (file)
index 0000000..6a6012d
--- /dev/null
@@ -0,0 +1,18 @@
+# -*- makefile -*-
+
+dir := completion
+
+# The dir variable will be re-assigned to later, so we can't use it
+# directly in any shell commands. Instead we save its value in other,
+# private variables that we can use in the commands.
+bash_script := $(dir)/notmuch-completion.bash
+zsh_script := $(dir)/notmuch-completion.zsh
+
+install: install-$(dir)
+
+install-$(dir):
+       @echo $@
+       mkdir -p $(DESTDIR)$(bash_completion_dir)
+       install -m0644 $(bash_script) $(DESTDIR)$(bash_completion_dir)/notmuch
+       mkdir -p $(DESTDIR)$(zsh_completion_dir)
+       install -m0644 $(zsh_script) $(DESTDIR)$(zsh_completion_dir)/notmuch
diff --git a/completion/README b/completion/README
new file mode 100644 (file)
index 0000000..40a30e5
--- /dev/null
@@ -0,0 +1,10 @@
+notmuch completion
+
+This directory contains support for various shells to automatically
+complete partially entered notmuch command lines.
+
+notmuch-completion.bash        Command-line completion for the bash shell
+
+notmuch-completion.tcsh        Command-line completion for the tcsh shell
+
+notmuch-completion.zsh Command-line completion for the zsh shell
diff --git a/completion/notmuch-completion.bash b/completion/notmuch-completion.bash
new file mode 100644 (file)
index 0000000..8665268
--- /dev/null
@@ -0,0 +1,71 @@
+# Bash completion for notmuch
+#
+# Copyright © 2009 Carl Worth
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see http://www.gnu.org/licenses/ .
+#
+# Author: Carl Worth <cworth@cworth.org>
+#
+# Based on "notmuch help" as follows:
+#
+# Usage: notmuch <command> [args...]
+#
+# Where <command> and [args...] are as follows:
+#
+#      setup
+#
+#      new
+#
+#      search [options] <search-term> [...]
+#
+#      show <search-terms>
+#
+#      reply <search-terms>
+#
+#      tag +<tag>|-<tag> [...] [--] <search-terms> [...]
+#
+#      dump [<filename>]
+#
+#      restore <filename>
+#
+#      help [<command>]
+
+_notmuch()
+{
+    local current previous commands help_options
+
+    previous=${COMP_WORDS[COMP_CWORD-1]}
+    current="${COMP_WORDS[COMP_CWORD]}"
+
+    commands="setup new search show reply tag dump restore help"
+    help_options="setup new search show reply tag dump restore search-terms"
+    search_options="--max-threads= --first= --sort="
+
+    COMPREPLY=()
+
+    case $COMP_CWORD in
+        1)
+            COMPREPLY=( $(compgen -W "${commands}" -- ${current}) ) ;;
+        2)
+            case $previous in
+                help)
+                    COMPREPLY=( $(compgen -W "${help_options}" -- ${current}) ) ;;
+                search)
+                    COMPREPLY=( $(compgen -W "${search_options}" -- ${current}) ) ;;
+            esac
+            ;;
+    esac
+}
+
+complete -o default -o bashdefault -F _notmuch notmuch
diff --git a/completion/notmuch-completion.tcsh b/completion/notmuch-completion.tcsh
new file mode 100644 (file)
index 0000000..c0d3a44
--- /dev/null
@@ -0,0 +1,2 @@
+set NOTMUCH_CMD=`notmuch help | awk '/\t/' | cut -f2 |grep -v '^$'`
+complete notmuch 'p/1/$NOTMUCH_CMD/'
diff --git a/completion/notmuch-completion.zsh b/completion/notmuch-completion.zsh
new file mode 100644 (file)
index 0000000..67a9aba
--- /dev/null
@@ -0,0 +1,74 @@
+#compdef notmuch
+
+# ZSH completion for `notmuch`
+# Copyright © 2009 Ingmar Vanhassel <ingmar@exherbo.org>
+
+_notmuch_commands()
+{
+  local -a notmuch_commands
+  notmuch_commands=(
+    'setup:interactively set up notmuch for first use'
+    'new:find and import any new message to the database'
+    'search:search for messages matching the search terms, display matching threads as results'
+    'reply:constructs a reply template for a set of messages'
+    'show:show all messages matching the search terms'
+    'tag:add or remove tags for all messages matching the search terms'
+    'dump:creates a plain-text dump of the tags of each message'
+    'restore:restores the tags from the given file'
+    'help:show details on a command'
+  )
+
+  _describe -t command 'command' notmuch_commands
+}
+
+_notmuch_dump()
+{
+  _files
+}
+
+_notmuch_help_topics()
+{
+  local -a notmuch_help_topics
+  notmuch_help_topics=(
+    'search-terms:show common search-terms syntax'
+  )
+  _describe -t notmuch-help-topics 'topic' notmuch_help_topics
+}
+
+_notmuch_help()
+{
+  _alternative \
+    _notmuch_commands \
+    _notmuch_help_topics
+}
+
+_notmuch_restore()
+{
+  _files
+}
+
+_notmuch_search()
+{
+  _arguments -s : \
+    '--max-threads=[display only the first x threads from the search results]:number of threads to show: ' \
+    '--first=[omit the first x threads from the search results]:number of threads to omit: ' \
+    '--sort=[sort results]:sorting:((newest-first\:"reverse chronological order" oldest-first\:"chronological order"))'
+}
+
+_notmuch()
+{
+  if (( CURRENT > 2 )) ; then
+    local cmd=${words[2]}
+    curcontext="${curcontext%:*:*}:notmuch-$cmd"
+    (( CURRENT-- ))
+    shift words
+    _call_function ret _notmuch_$cmd
+    return ret
+  else
+    _notmuch_commands
+  fi
+}
+
+_notmuch "$@"
+
+# vim: set sw=2 sts=2 ts=2 et ft=zsh :
diff --git a/configure b/configure
new file mode 100755 (executable)
index 0000000..ec8c3fd
--- /dev/null
+++ b/configure
@@ -0,0 +1,452 @@
+#! /bin/sh
+
+# Set several defaults (optionally specified by the user in
+# environemnt variables)
+CC=${CC:-gcc}
+CXX=${CXX:-g++}
+CFLAGS=${CFLAGS:--O2}
+CXXFLAGS=${CXXFLAGS:-\$(CFLAGS)}
+XAPIAN_CONFIG=${XAPIAN_CONFIG:-xapian-config-1.1 xapian-config}
+
+# We don't allow the EMACS or GZIP Makefile variables inherit values
+# from the environment as we do with CC and CXX above. The reason is
+# that these names as environment variables have existing uses other
+# than the program name that we want. (EMACS is set to 't' when a
+# shell is running within emacs and GZIP specifies arguments to pass
+# on the gzip command line).
+
+# Set the defaults for values the user can specify with command-line
+# options.
+PREFIX=/usr/local
+LIBDIR=
+
+usage ()
+{
+    cat <<EOF
+Usage: ./configure [options]...
+
+This script configures notmuch to build on your system.
+
+It verifies that dependencies are available, determines flags needed
+to compile and link against various required libraries, and identifies
+whether various system functions can be used or if locally-provided
+replacements will be built instead.
+
+Finally, it allows you to control various aspects of the build and
+installation process.
+
+First, some common variables can specified via environment variables:
+
+       CC              The C compiler to use
+       CFLAGS          Flags to pass to the C compiler
+       CXX             The C++ compiler to use
+       CXXFLAGS        Flags to pass to the C compiler
+       LDFLAGS         Flags to pass when linking
+
+Each of these values can further be controlled by specifying them
+later on the "make" command line.
+
+Other environment variables can be used to control configure itself,
+(and for which there is no equivalent build-time control):
+
+       XAPIAN_CONFIG   The program to use to determine flags for
+                       compiling and linking against the Xapian
+                       library. [$XAPIAN_CONFIG]
+
+Additionally, various options can be specified on the configure
+command line.
+
+       --prefix=PREFIX Install files in PREFIX [$PREFIX]
+
+By default, "make install" will install the resulting program to
+$PREFIX/bin, documentation to $PREFIX/man, etc. You can
+specify an installation prefix other than $PREFIX using
+--prefix, for instance:
+
+       ./configure --prefix=\$HOME
+
+Fine tuning of some installation directories is available:
+
+       --libdir=DIR            Install libraries to DIR [PREFIX/lib]
+       --includedir=DIR        Install header files to DIR [PREFIX/include]
+       --mandir=DIR            Install man pages to DIR [PREFIX/share/man]
+       --sysconfdir=DIR        Read-only single-machine data [PREFIX/etc]
+       --emacslispdir=DIR      Emacs code [PREFIX/share/emacs/site-lisp]
+
+Additional options are accepted for compatibility with other
+configure-script calling conventions, but don't do anything yet:
+
+       --build=<cpu>-<vendor>-<os>     Currently ignored
+       --infodir=DIR                   Currently ignored
+       --localstatedir=DIR             Currently ignored
+       --libexecdir=DIR                Currently ignored
+       --disable-maintainer-mode       Currently ignored
+       --disable-dependency-tracking   Currently ignored
+
+EOF
+}
+
+# Parse command-line options
+for option; do
+    if [ "${option}" = '--help' ] ; then
+       usage
+       exit 0
+    elif [ "${option%%=*}" = '--prefix' ] ; then
+       PREFIX="${option#*=}"
+    elif [ "${option%%=*}" = '--libdir' ] ; then
+       LIBDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--includedir' ] ; then
+       INCLUDEDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--mandir' ] ; then
+       MANDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--sysconfdir' ] ; then
+       SYSCONFDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--emacslispdir' ] ; then
+       EMACSLISPDIR="${option#*=}"
+    elif [ "${option%%=*}" = '--build' ] ; then
+       build_option="${option#*=}"
+       case ${build_option} in
+           *-*-*) ;;
+           *)
+               echo "Unrecognized value for --build option: ${build_option}"
+               echo "Should be: <cpu>-<vendor>-<os>"
+               echo "See:"
+               echo "  $0 --help"
+               echo ""
+               exit 1
+       esac
+       build_cpu=${build_option%%-*}
+       build_option=${build_option#*-}
+       build_vendor=${build_option%%-*}
+       build_os=${build_option#*-}
+    elif [ "${option%%=*}" = '--infodir' ] ; then
+       true
+    elif [ "${option%%=*}" = '--localstatedir' ] ; then
+       true
+    elif [ "${option%%=*}" = '--libexecdir' ] ; then
+       true
+    elif [ "${option}" = '--disable-maintainer-mode' ] ; then
+       true
+    elif [ "${option}" = '--disable-dependency-tracking' ] ; then
+       true
+    else
+       echo "Unrecognized option: ${option}"
+       echo "See:"
+       echo "  $0 --help"
+       echo ""
+       exit 1
+    fi
+done
+
+cat <<EOF
+Welcome to Notmuch, a system for indexing, searching and tagging your email.
+
+We hope that the process of building and installing notmuch is quick
+and smooth so that you can soon be reading and processing your email
+more efficiently than ever.
+
+If anything goes wrong in the configure process, you can override any
+decisions it makes by manually editing the Makefile.config file that
+it creates. Also please do as much as you can to figure out what could
+be different on your machine compared to those of the notmuch
+developers. Then, please email those details to the Notmuch list
+(notmuch@notmuchmail.org) so that we can hopefully make future
+versions of notmuch easier for you to use.
+
+We'll now investigate your system to verify that all required
+dependencies are available:
+
+EOF
+
+errors=0
+
+if pkg-config --version > /dev/null 2>&1; then
+    have_pkg_config=1
+else
+    have_pkg_config=0
+fi
+
+printf "Checking for Xapian development files... "
+have_xapian=0
+for xapian_config in ${XAPIAN_CONFIG}; do
+    if ${xapian_config} --version > /dev/null 2>&1; then
+       printf "Yes (%s).\n" $(${xapian_config} --version | sed -e 's/.* //')
+       have_xapian=1
+       xapian_cxxflags=$(${xapian_config} --cxxflags)
+       xapian_ldflags=$(${xapian_config} --libs)
+       break
+    fi
+done
+if [ ${have_xapian} = "0" ]; then
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+printf "Checking for GMime development files... "
+have_gmime=0
+for gmimepc in gmime-2.6 gmime-2.4; do
+    if pkg-config --modversion $gmimepc > /dev/null 2>&1; then
+       printf "Yes ($gmimepc).\n"
+       have_gmime=1
+       gmime_cflags=$(pkg-config --cflags $gmimepc)
+       gmime_ldflags=$(pkg-config --libs $gmimepc)
+    fi
+done
+if [ "$have_gmime" = "0" ]; then
+    printf "No.\n"
+    errors=$((errors + 1))
+fi
+
+printf "Checking for talloc development files... "
+if pkg-config --modversion talloc > /dev/null 2>&1; then
+    printf "Yes.\n"
+    have_talloc=1
+    talloc_cflags=$(pkg-config --cflags talloc)
+    talloc_ldflags=$(pkg-config --libs talloc)
+else
+    printf "No.\n"
+    have_talloc=0
+    talloc_cflags=
+    errors=$((errors + 1))
+fi
+
+printf "Checking for valgrind development files... "
+if pkg-config --modversion valgrind > /dev/null 2>&1; then
+    printf "Yes.\n"
+    have_valgrind=1
+    valgrind_cflags=$(pkg-config --cflags valgrind)
+else
+    printf "No (but that's fine).\n"
+    have_valgrind=0
+fi
+
+if [ -z "${EMACSLISPDIR}" ]; then
+    if pkg-config --modversion emacs > /dev/null 2>&1; then
+       EMACSLISPDIR=$(pkg-config emacs --variable sitepkglispdir)
+    else
+       EMACSLISPDIR='$(prefix)/share/emacs/site-lisp'
+    fi
+fi
+
+printf "Checking if emacs is available... "
+if emacs --quick --batch > /dev/null 2>&1; then
+    printf "Yes.\n"
+    have_emacs=1
+else
+    printf "No (so will not byte-compile emacs code)\n"
+    have_emacs=0
+fi
+
+printf "Checking for Mac OS X (for shared library)... "
+if [ `uname` = "Darwin" ] ; then
+    printf "Yes.\n"
+    mac_os_x=1
+    linker_resolves_library_dependencies=0
+else
+    printf "No.\n"
+    mac_os_x=0
+    linker_resolves_library_dependencies=1
+fi
+
+if [ $errors -gt 0 ]; then
+    cat <<EOF
+
+*** Error: The dependencies of notmuch could not be satisfied. You will
+need to install the following packages before being able to compile
+notmuch:
+
+EOF
+    if [ $have_xapian -eq 0 ]; then
+       echo "  Xapian library (including development files such as headers)"
+       echo "  http://xapian.org/"
+    fi
+    if [ $have_gmime -eq 0 ]; then
+       echo "  GMime 2.4 library (including development files such as headers)"
+       echo "  http://spruce.sourceforge.net/gmime/"
+    fi
+    if [ $have_talloc -eq 0 ]; then
+       echo "  The talloc library (including development files such as headers)"
+       echo "  http://talloc.samba.org/"
+    fi
+    cat <<EOF
+
+With any luck, you're using a modern, package-based operating system
+that has all of these packages available in the distribution. In that
+case a simple command will install everything you need. For example:
+
+On Debian and similar systems:
+
+       sudo apt-get install libxapian-dev libgmime-2.4-dev libtalloc-dev
+
+Or on Fedora and similar systems:
+
+       sudo yum install xapian-core-devel gmime-devel libtalloc-devel
+
+On other systems, similar commands can be used, but the details of the
+package names may be different.
+
+EOF
+    if [ $have_pkg_config -eq 0 ]; then
+cat <<EOF
+Note: the pkg-config program is not available. This configure script
+uses pkg-config to find the compilation flags required to link against
+the various libraries needed by notmuch. It's possible you simply need
+to install pkg-config with a command such as:
+
+       sudo apt-get install pkg-config
+Or:
+       sudo yum install pkgconfig
+
+But if pkg-config is not available for your system, then you will need
+to modify the configure script to manually set the cflags and ldflags
+variables to the correct values to link against each library in each
+case that pkg-config could not be used to determine those values.
+
+EOF
+    fi
+cat <<EOF
+When you have installed the necessary dependencies, you can run
+configure again to ensure the packages can be found, or simply run
+"make" to compile notmuch.
+
+EOF
+    exit 1
+fi
+
+printf "Checking for getline... "
+if ${CC} -o compat/have_getline compat/have_getline.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_getline=1
+else
+    printf "No (will use our own instead).\n"
+    have_getline=0
+fi
+rm -f compat/have_getline
+
+printf "Checking for strcasestr... "
+if ${CC} -o compat/have_strcasestr compat/have_strcasestr.c > /dev/null 2>&1
+then
+    printf "Yes.\n"
+    have_strcasestr=1
+else
+    printf "No (will use our own instead).\n"
+    have_strcasestr=0
+fi
+rm -f compat/have_strcasestr
+
+cat <<EOF
+
+All required packages were found. You may now run the following
+commands to compile and install notmuch:
+
+       make
+       sudo make install
+
+EOF
+
+# construct the Makefile.config
+cat > Makefile.config <<EOF
+# This Makefile.config was automatically generated by the ./configure
+# script of notmuch. If the configure script identified anything
+# incorrectly, then you can edit this file to try to correct things,
+# but be warned that if configure is run again it will destroy your
+# changes, (and this could happen by simply calling "make" if the
+# configure script is updated).
+
+# The C compiler to use
+CC = ${CC}
+
+# The C++ compiler to use
+CXX = ${CXX}
+
+# Command to execute emacs from Makefiles
+EMACS = emacs --quick
+
+# Default FLAGS for C compiler (can be overridden by user such as "make CFLAGS=-g")
+CFLAGS = ${CFLAGS}
+
+# Default FLAGS for C++ compiler (can be overridden by user such as "make CXXFLAGS=-g")
+CXXFLAGS = ${CXXFLAGS}
+
+# Flags to enable warnings when using the C++ compiler
+WARN_CXXFLAGS=-Wall -Wextra -Wwrite-strings -Wswitch-enum
+
+# Flags to enable warnings when using the C compiler
+WARN_CFLAGS=\$(WARN_CXXFLAGS) -Wmissing-declarations
+
+# The prefix to which notmuch should be installed
+prefix = ${PREFIX}
+
+# The directory to which libraries should be installed
+libdir = ${LIBDIR:=\$(prefix)/lib}
+
+# The directory to which header files should be installed
+includedir = ${INCLUDEDIR:=\$(prefix)/include}
+
+# The directory to which man pages should be installed
+mandir = ${MANDIR:=\$(prefix)/share/man}
+
+# The directory to which read-only (configuration) filesshould be installed
+sysconfdir = ${SYSCONFDIR:=\$(prefix)/etc}
+
+# The directory to which emacs lisp files should be installed
+emacslispdir=${EMACSLISPDIR}
+
+# Whether there's an emacs binary available for byte-compiling
+HAVE_EMACS = ${have_emacs}
+
+# The directory to which desktop files should be installed
+desktop_dir = \$(prefix)/share/applications
+
+# The directory to which bash completions files should be installed
+bash_completion_dir = \$(sysconfdir)/bash_completion.d
+
+# The directory to which zsh completions files should be installed
+zsh_completion_dir = \$(prefix)/share/zsh/functions/Completion/Unix
+
+# Whether the getline function is available (if not, then notmuch will
+# build its own version)
+HAVE_GETLINE = ${have_getline}
+
+# Whether the strcasestr function is available (if not, then notmuch will
+# build its own version)
+HAVE_STRCASESTR = ${have_strcasestr}
+
+# Whether we are building on OS X.  This will affect how we build the
+# shared library.
+MAC_OS_X = ${mac_os_x}
+
+# Whether the linker will automatically resolve the dependency of one
+# library on another (if not, then linking a binary requires linking
+# directly against both)
+LINKER_RESOLVES_LIBRARY_DEPENDENCIES = ${linker_resolves_library_dependencies}
+
+# Flags needed to compile and link against Xapian
+XAPIAN_CXXFLAGS = ${xapian_cxxflags}
+XAPIAN_LDFLAGS = ${xapian_ldflags}
+
+# Flags needed to compile and link against GMime-2.4
+GMIME_CFLAGS = ${gmime_cflags}
+GMIME_LDFLAGS = ${gmime_ldflags}
+
+# Flags needed to compile and link against talloc
+TALLOC_CFLAGS = ${talloc_cflags}
+TALLOC_LDFLAGS = ${talloc_ldflags}
+
+# Whether valgrind header files are available
+HAVE_VALGRIND = ${have_valgrind}
+
+# And if so, flags needed at compile time for valgrind macros
+VALGRIND_CFLAGS = ${valgrind_cflags}
+
+# Combined flags for compiling and linking against all of the above
+CONFIGURE_CFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS)      \\
+                  \$(TALLOC_CFLAGS) -DHAVE_VALGRIND=\$(HAVE_VALGRIND)   \\
+                  \$(VALGRIND_CFLAGS) -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)
+CONFIGURE_CXXFLAGS = -DHAVE_GETLINE=\$(HAVE_GETLINE) \$(GMIME_CFLAGS)    \\
+                    \$(TALLOC_CFLAGS) -DHAVE_VALGRIND=\$(HAVE_VALGRIND) \\
+                    \$(VALGRIND_CFLAGS) \$(XAPIAN_CXXFLAGS)             \\
+                     -DHAVE_STRCASESTR=\$(HAVE_STRCASESTR)
+CONFIGURE_LDFLAGS =  \$(GMIME_LDFLAGS) \$(TALLOC_LDFLAGS) \$(XAPIAN_LDFLAGS)
+EOF
diff --git a/debian/.gitignore b/debian/.gitignore
new file mode 100644 (file)
index 0000000..801ca02
--- /dev/null
@@ -0,0 +1,3 @@
+*.debhelper
+*.debhelper.log
+*.substvars
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644 (file)
index 0000000..f07b499
--- /dev/null
@@ -0,0 +1,6 @@
+notmuch for Debian
+==================
+
+To use the vim plugin, please install it using vim-addons(1)
+
+ -- martin f. krafft <madduck@debian.org>  Thu, 21 Jan 2010 14:50:19 +1300
diff --git a/debian/changelog b/debian/changelog
new file mode 100644 (file)
index 0000000..42a9f6d
--- /dev/null
@@ -0,0 +1,52 @@
+notmuch (0.2) unstable; urgency=low
+
+  * Better guessing of From: header.
+  * Make "notmuch count" with no arguments count all messages
+  * Provide a new special-case search term of "*" to match all messages.
+  * Detect thread connections when a parent message is missing.
+  * Fix potential data loss in "notmuch new" with SIGINT
+  * Fix segfault when a message includes a MIME part that is empty.
+  * Fix handling of non-ASCII characters with --format=json
+  * Fix headers to be properly decoded in "notmuch reply"
+  * emacs: Show the last few lines of citations as well as the first few lines.
+  * emacs: The '+' and '-' commands can now add and remove tags by region.
+  * emacs: More meaningful buffer names for thread-view buffers. 
+  * emacs: Customized colors of threads in search view based on tags.
+
+ -- Carl Worth <cworth@debian.org>  Fri, 16 Apr 2010 10:20:23 -0700
+
+notmuch (0.1-1) unstable; urgency=low
+
+  [ martin f. krafft ]
+  * Add suggestion to vim-addon-manager.
+
+  [ Carl Worth ]
+  * Improve package description (closes: #566282).
+  * New upstream version (0.1) (closes: #576647).
+  * New versioning to track upstream version scheme.
+  * Split packaging into notmuch, libnotmuch1, and libnotmuch-dev.
+  * Update to advertise conformance with policy 3.8.4 (no changes).
+  * Add a debian/watch file to notice upstream tar files.
+
+ -- Carl Worth <cworth@debian.org>  Tue, 06 Apr 2010 18:27:49 -0700
+
+notmuch (0.0+201001211401) unstable; urgency=low
+
+  * Upload to Debian (closes: #557354).
+  * New versioning scheme.
+  * Added emacs build dependency.
+  * Added Vcs-Browser field to debian/control.
+  * Downgrade recommendation for emacs to suggestion.
+  * Add vim to suggestions and enhancements.
+  * Put debian/* under separate copyright.
+  * Make Carl the maintainer.
+  * Add myself to uploaders.
+  * Install the vim plugin (using vim-addons).
+
+ -- martin f. krafft <madduck@debian.org>  Thu, 21 Jan 2010 14:00:54 +1300
+
+notmuch (0.0-1) unstable; urgency=low
+
+  * New Debian package.
+
+ -- Jameson Graef Rollins <jrollins@finestructure.net>  Fri, 27 Nov 2009 13:39:09 -0500
diff --git a/debian/compat b/debian/compat
new file mode 100644 (file)
index 0000000..7f8f011
--- /dev/null
@@ -0,0 +1 @@
+7
diff --git a/debian/control b/debian/control
new file mode 100644 (file)
index 0000000..e4c61ab
--- /dev/null
@@ -0,0 +1,51 @@
+Source: notmuch
+Section: mail
+Priority: extra
+Maintainer: Carl Worth <cworth@debian.org>
+Uploaders: Jameson Graef Rollins <jrollins@finestructure.net>, martin f. krafft <madduck@debian.org>
+Build-Depends: debhelper (>= 7.0.50~), pkg-config, libxapian-dev, libgmime-2.4-dev, libtalloc-dev, libz-dev, emacs (>= 23~)
+Standards-Version: 3.8.4
+Homepage: http://notmuchmail.org/
+Vcs-Git: git://notmuchmail.org/git/notmuch
+Vcs-Browser: http://git.notmuchmail.org/git/notmuch
+Dm-Upload-Allowed: yes
+
+Package: notmuch
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Enhances: emacs, vim
+Suggests: emacs, vim, vim-addon-manager
+Description: thread-based email index, search and tagging
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package contains the notmuch command-line interface as well as
+ emacs and vim interfaces for notmuch.
+
+Package: libnotmuch1
+Section: libs
+Architecture: any
+Depends: ${shlibs:Depends}, ${misc:Depends}
+Description: thread-based email index, search and tagging (runtime)
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package contains the runtime library, necessary to run
+ applications using libnotmuch.
+
+Package: libnotmuch-dev
+Section: libdevel
+Architecture: any
+Depends: ${misc:Depends}, libnotmuch1 (= ${binary:Version})
+Description: thread-based email index, search and tagging (development)
+ Notmuch is a system for indexing, searching, reading, and tagging
+ large collections of email messages in maildir or mh format. It uses
+ the Xapian library to provide fast, full-text search with a very
+ convenient search syntax.
+ .
+ This package provides the necessary development libraries and header
+ files to allow you to develop new software using libnotmuch.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644 (file)
index 0000000..1cc7a15
--- /dev/null
@@ -0,0 +1,55 @@
+Format-Specification: http://svn.debian.org/wsvn/dep/web/deps/dep5.mdwn?op=file&rev=59
+Debianized-By: Jameson Graef Rollins <jrollins@finestructure.net>
+Debianized-Date: Fri Nov 28 18:00:00 EDT 2009
+Original-Source: git://notmuchmail.org/git/notmuch
+
+Files: *
+Copyright:  Copyright 2009 Carl Worth <cworth@cworth.org>
+ Bart Trojanowski <bart@jukie.net>
+ Keith Packard <keithp@keithp.com>
+ Alexander Botero-Lowry <alex.boterolowry@gmail.com>
+ Ingmar Vanhassel <ingmar@exherbo.org>
+ Jed Brown <jed@59A2.org>
+ Jan Janak <jan@ryngle.com>
+ Chris Wilson <chris@chris-wilson.co.uk>
+ Keith Amidon <keith@nicira.com>
+ Aneesh Kumar K.V <aneesh.kumar@linux.vnet.ibm.com>
+ Mikhail Gusarov <dottedmag@dottedmag.net>
+ Jeffrey C. Ollie <jeff@ocjtech.us>
+ Jameson Graef Rollins <jrollins@finestructure.net>
+ Stewart Smith <stewart@flamingspork.com>
+ Adrian Perez <aperez@igalia.com>
+ Kan-Ru Chen <kanru@kanru.info>
+ James Rowe <jnrowe@gmail.com>
+ Eric Anholt <eric@anholt.net>
+ Alec Berryman <alec@thened.net>
+ Tassilo Horn <tassilo@member.fsf.org>
+ Stefan Schmidt <stefan@datenfreihafen.org>
+ Rolland Santimano <rollandsantimano@yahoo.com>
+ Peter Wang <novalazy@gmail.com>
+ Lars Kellogg-Stedman <lars@seas.harvard.edu>
+ Holger Freyther <zecke@selfish.org>
+ David Bremner <bremner@unb.ca>
+ Alexander Botero-Lowry <alexbl@fortitudo.(none)>
+
+License: GPL-3+
+ 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 3 of the License, or
+ (at your option) any later version.
+ .
+ On Debian systems, the complete text of the GNU General Public License
+ version 3 can be found in file "/usr/share/common-licenses/GPL-3".
+
+Files: debian/*
+Copyright:  Copyright 2010 Jameson Graef Rollins <jrollins@finestructure.net>
+ martin f. krafft <madduck@debian.org>
+
+License: GPL-3+
+ 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 3 of the License, or
+ (at your option) any later version.
+ .
+ On Debian systems, the complete text of the GNU General Public License
+ version 3 can be found in file "/usr/share/common-licenses/GPL-3".
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644 (file)
index 0000000..dba526f
--- /dev/null
@@ -0,0 +1,14 @@
+# Configuration file for git-buildpackage
+
+[DEFAULT]
+# The default branch for upstream sources
+upstream-branch = master
+
+# The default branch for the debian patch (no patch in our case)
+debian-branch = master
+
+# Directory for performing the build
+export-dir = ./debian-build
+
+# Format for the debian tag
+debian-tag = debian-%(version)s
diff --git a/debian/libnotmuch-dev.install b/debian/libnotmuch-dev.install
new file mode 100644 (file)
index 0000000..185dba4
--- /dev/null
@@ -0,0 +1,2 @@
+usr/include
+usr/lib/libnotmuch.so
diff --git a/debian/libnotmuch1.install b/debian/libnotmuch1.install
new file mode 100644 (file)
index 0000000..da4fc25
--- /dev/null
@@ -0,0 +1 @@
+usr/lib/libnotmuch.so.*
diff --git a/debian/notmuch.dirs b/debian/notmuch.dirs
new file mode 100644 (file)
index 0000000..ce7f239
--- /dev/null
@@ -0,0 +1,6 @@
+usr/bin
+usr/share/emacs/site-lisp/notmuch
+usr/share/vim/registry
+usr/share/vim/addons/plugin
+usr/share/vim/addons/syntax
+etc/bash_completion.d
diff --git a/debian/notmuch.emacsen-install b/debian/notmuch.emacsen-install
new file mode 100755 (executable)
index 0000000..98ec128
--- /dev/null
@@ -0,0 +1,47 @@
+#! /bin/sh -e
+# /usr/lib/emacsen-common/packages/install/notmuch
+
+# Written by Jim Van Zandt <jrv@debian.org>, borrowing heavily
+# from the install scripts for gettext by Santiago Vila
+# <sanvila@ctv.es> and octave by Dirk Eddelbuettel <edd@debian.org>.
+
+FLAVOR=$1
+PACKAGE=notmuch
+
+if [ ${FLAVOR} = emacs ]; then exit 0; fi
+
+# We know that the notmuch emacs code doesn't work with emacs before emacs23
+if [ ${FLAVOR} = emacs21 ]; then exit 0; fi
+if [ ${FLAVOR} = emacs22 ]; then exit 0; fi
+if [ ${FLAVOR} = xemacs21 ]; then exit 0; fi
+if [ ${FLAVOR} = xemacs22 ]; then exit 0; fi
+
+echo install/${PACKAGE}: Handling install for emacsen flavor ${FLAVOR}
+
+#FLAVORTEST=`echo $FLAVOR | cut -c-6`
+#if [ ${FLAVORTEST} = xemacs ] ; then
+#    SITEFLAG="-no-site-file"
+#else
+#    SITEFLAG="--no-site-file"
+#fi
+#FLAGS="${SITEFLAG} -q -batch -l path.el -f batch-byte-compile"
+FLAGS="--no-site-file -q -batch -l path.el -f batch-byte-compile"
+
+ELDIR=/usr/share/emacs/site-lisp/${PACKAGE}
+ELCDIR=/usr/share/${FLAVOR}/site-lisp/${PACKAGE}
+
+install -m 755 -d ${ELCDIR}
+cd ${ELDIR}
+FILES=`echo *.el`
+cd ${ELCDIR}
+for file in ${FILES}; do
+       ln -sf ${ELDIR}/${file} .
+done
+
+cat << EOF > path.el
+(setq load-path (cons "." load-path) byte-compile-warnings nil)
+EOF
+${FLAVOR} ${FLAGS} ${FILES}
+rm -f *.el
+
+exit 0
diff --git a/debian/notmuch.emacsen-remove b/debian/notmuch.emacsen-remove
new file mode 100755 (executable)
index 0000000..c35e8a4
--- /dev/null
@@ -0,0 +1,10 @@
+#!/bin/sh -e
+# /usr/lib/emacsen-common/packages/remove/notmuch
+
+FLAVOR=$1
+PACKAGE=notmuch
+
+if [ ${FLAVOR} != emacs ]; then
+    echo remove/${PACKAGE}: purging byte-compiled files for ${FLAVOR}
+    rm -rf /usr/share/${FLAVOR}/site-lisp/${PACKAGE}
+fi
diff --git a/debian/notmuch.emacsen-startup b/debian/notmuch.emacsen-startup
new file mode 100644 (file)
index 0000000..25a527f
--- /dev/null
@@ -0,0 +1,23 @@
+;; -*-emacs-lisp-*-
+;;
+;; Emacs startup file, e.g.  /etc/emacs/site-start.d/50notmuch.el
+;; for the Debian notmuch package
+
+;; The notmuch package follows the Debian/GNU Linux 'emacsen' policy and
+;; byte-compiles its elisp files for each 'emacs flavor' (emacs19,
+;; xemacs19, emacs20, xemacs20...).  The compiled code is then
+;; installed in a subdirectory of the respective site-lisp directory.
+
+(cond
+ ((not (file-exists-p "/usr/share/emacs/site-lisp/notmuch"))
+  (message "Package notmuch removed but not purged.  Skipping setup."))
+ ((not (file-exists-p (concat "/usr/share/"
+                             (symbol-name debian-emacs-flavor)
+                             "/site-lisp/notmuch")))
+  (message "Package notmuch not fully installed.  Skipping setup."))
+ (t
+  (debian-pkg-add-load-path-item
+   (concat "/usr/share/"
+          (symbol-name debian-emacs-flavor)
+          "/site-lisp/notmuch"))
+  ))
diff --git a/debian/notmuch.install b/debian/notmuch.install
new file mode 100644 (file)
index 0000000..dd310fa
--- /dev/null
@@ -0,0 +1,5 @@
+usr/bin
+usr/share/emacs/site-lisp/notmuch/*.el
+usr/share/man/man1
+etc/bash_completion.d
+usr/share/zsh
diff --git a/debian/rules b/debian/rules
new file mode 100755 (executable)
index 0000000..a4b526c
--- /dev/null
@@ -0,0 +1,17 @@
+#!/usr/bin/make -f
+
+%:
+       dh $@
+
+override_dh_auto_configure:
+       dh_auto_configure -- --emacslispdir=/usr/share/emacs/site-lisp/notmuch
+
+override_dh_installdocs:
+       dh_installdocs
+       install -m644 vim/README debian/notmuch/usr/share/doc/notmuch/README.vim
+
+override_dh_install:
+       dh_install
+       install -m644 vim/plugin/notmuch.vim debian/notmuch/usr/share/vim/addons/plugin
+       install -m644 vim/syntax/notmuch-*.vim debian/notmuch/usr/share/vim/addons/syntax
+       install -m644 vim/notmuch.yaml debian/notmuch/usr/share/vim/registry
diff --git a/debian/source/format b/debian/source/format
new file mode 100644 (file)
index 0000000..89ae9db
--- /dev/null
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/debugger.c b/debugger.c
new file mode 100644 (file)
index 0000000..e8b9378
--- /dev/null
@@ -0,0 +1,47 @@
+/* debugger.c - Some debugger utilities for the notmuch mail library
+ *
+ * Copyright © 2009 Chris Wilson
+ *
+ * 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/ .
+ *
+ * Author: Chris Wilson <chris@chris-wilson.co.uk>
+ */
+
+#include "notmuch-client.h"
+
+#include <libgen.h>
+
+#if HAVE_VALGRIND
+#include <valgrind.h>
+#else
+#define RUNNING_ON_VALGRIND 0
+#endif
+
+notmuch_bool_t
+debugger_is_active (void)
+{
+    char buf[1024];
+
+    if (RUNNING_ON_VALGRIND)
+       return TRUE;
+
+    sprintf (buf, "/proc/%d/exe", getppid ());
+    if (readlink (buf, buf, sizeof (buf)) != -1 &&
+       strncmp (basename (buf), "gdb", 3) == 0)
+    {
+       return TRUE;
+    }
+
+    return FALSE;
+}
diff --git a/emacs/Makefile b/emacs/Makefile
new file mode 100644 (file)
index 0000000..b6859ea
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makfefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/emacs/Makefile.local b/emacs/Makefile.local
new file mode 100644 (file)
index 0000000..f759c0d
--- /dev/null
@@ -0,0 +1,30 @@
+# -*- makefile -*-
+
+dir := emacs
+emacs_sources := \
+       $(dir)/notmuch-lib.el \
+       $(dir)/notmuch.el \
+       $(dir)/notmuch-query.el \
+       $(dir)/notmuch-show.el \
+       $(dir)/notmuch-wash.el
+
+emacs_bytecode := $(subst .el,.elc,$(emacs_sources))
+
+%.elc: %.el
+       $(call quiet,EMACS) --directory emacs -batch -f batch-byte-compile $<
+
+ifeq ($(HAVE_EMACS),1)
+all: $(emacs_bytecode)
+endif
+
+install: install-emacs
+
+.PHONY: install-emacs
+install-emacs:
+       mkdir -p $(DESTDIR)$(emacslispdir)
+       install -m0644 $(emacs_sources) $(DESTDIR)$(emacslispdir)
+ifeq ($(HAVE_EMACS),1)
+       install -m0644 $(emacs_bytecode) $(DESTDIR)$(emacslispdir)
+endif
+
+CLEAN := $(CLEAN) $(emacs_bytecode)
diff --git a/emacs/notmuch-lib.el b/emacs/notmuch-lib.el
new file mode 100644 (file)
index 0000000..cb9be30
--- /dev/null
@@ -0,0 +1,42 @@
+;; notmuch-lib.el --- common variables, functions and function declarations
+;;
+;; Copyright © Carl Worth
+;;
+;; 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/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+
+;; This is an part of an emacs-based interface to the notmuch mail system.
+
+(defvar notmuch-command "notmuch"
+  "Command to run the notmuch binary.")
+
+;; XXX: This should be a generic function in emacs somewhere, not
+;; here.
+(defun point-invisible-p ()
+  "Return whether the character at point is invisible.
+
+Here visibility is determined by `buffer-invisibility-spec' and
+the invisible property of any overlays for point. It doesn't have
+anything to do with whether point is currently being displayed
+within the current window."
+  (let ((prop (get-char-property (point) 'invisible)))
+    (if (eq buffer-invisibility-spec t)
+       prop
+      (or (memq prop buffer-invisibility-spec)
+         (assq prop buffer-invisibility-spec)))))
+
+(provide 'notmuch-lib)
diff --git a/emacs/notmuch-query.el b/emacs/notmuch-query.el
new file mode 100644 (file)
index 0000000..0d6e775
--- /dev/null
@@ -0,0 +1,81 @@
+;; notmuch-query.el --- provide an emacs api to query notmuch
+;;
+;; Copyright © David Bremner
+;;
+;; 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/>.
+;;
+;; Authors: David Bremner <david@tethera.net>
+
+(require 'notmuch-lib)
+(require 'json)
+
+(defun notmuch-query-get-threads (search-terms &rest options)
+  "Return a list of threads of messages matching SEARCH-TERMS.
+
+A thread is a forest or list of trees. A tree is a two element
+list where the first element is a message, and the second element
+is a possibly empty forest of replies.
+"
+  (let  ((args (append '("show" "--format=json") search-terms))
+        (json-object-type 'plist)
+        (json-array-type 'list)
+        (json-false 'nil))
+    (with-temp-buffer
+      (progn
+       (apply 'call-process (append (list notmuch-command nil t nil) args))
+       (goto-char (point-min))
+       (json-read)))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Mapping functions across collections of messages.
+
+(defun notmuch-query-map-aux  (mapper function seq)
+  "private function to do the actual mapping and flattening"
+  (apply 'append
+        (mapcar
+          (lambda (tree)
+            (funcall mapper fn tree))
+          seq)))
+
+(defun notmuch-query-map-threads (fn threads)
+  "apply FN to every thread in  THREADS. Flatten results to a list.
+
+See the function notmuch-query-get-threads for more information."
+  (notmuch-query-map-aux 'notmuch-query-map-forest fn threads))
+
+(defun notmuch-query-map-forest (fn forest)
+  "apply function to every message in a forest. Flatten results to a list.
+
+See the function notmuch-query-get-threads for more information.
+"
+  (notmuch-query-map-aux 'notmuch-query-map-tree fn forest))
+
+(defun notmuch-query-map-tree (fn tree)
+  "Apply function FN to every message in TREE. Flatten results to a list
+
+See the function notmuch-query-get-threads for more information."
+  (cons (funcall fn (car tree)) (notmuch-query-map-forest fn (cadr tree))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;; Predefined queries
+
+(defun notmuch-query-get-message-ids (&rest search-terms)
+  "Return a list of message-ids of messages that match SEARCH-TERMS"
+  (notmuch-query-map-threads
+   (lambda (msg) (plist-get msg :id))
+   (notmuch-query-get-threads search-terms)))
+
+(provide 'notmuch-query)
diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el
new file mode 100644 (file)
index 0000000..916b39e
--- /dev/null
@@ -0,0 +1,1002 @@
+;; notmuch-show.el --- displaying notmuch forests.
+;;
+;; Copyright © Carl Worth
+;; Copyright © David Edmondson
+;;
+;; 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/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+;;          David Edmondson <dme@dme.org>
+
+(require 'cl)
+(require 'mm-view)
+(require 'message)
+(require 'mm-decode)
+(require 'mailcap)
+
+(require 'notmuch-lib)
+(require 'notmuch-query)
+(require 'notmuch-wash)
+
+(declare-function notmuch-call-notmuch-process "notmuch" (&rest args))
+(declare-function notmuch-reply "notmuch" (query-string))
+(declare-function notmuch-fontify-headers "notmuch" nil)
+(declare-function notmuch-select-tag-with-completion "notmuch" (prompt &rest search-terms))
+(declare-function notmuch-search-show-thread "notmuch" nil)
+
+(defvar notmuch-show-headers '("Subject" "To" "Cc" "From" "Date")
+  "Headers that should be shown in a message, in this order. Note
+that if this order is changed the headers shown when a message is
+collapsed will change.")
+
+(defvar notmuch-show-markup-headers-hook '(notmuch-show-colour-headers)
+  "A list of functions called to decorate the headers listed in
+`notmuch-show-headers'.")
+
+(defvar notmuch-show-hook '(notmuch-show-pretty-hook)
+  "A list of functions called after populating a
+`notmuch-show' buffer.")
+
+(defvar notmuch-show-insert-text/plain-hook '(notmuch-wash-text/plain-citations)
+  "A list of functions called to clean up text/plain body parts.")
+
+(defun notmuch-show-pretty-hook ()
+  (goto-address-mode 1)
+  (visual-line-mode))
+
+(defmacro with-current-notmuch-show-message (&rest body)
+  "Evaluate body with current buffer set to the text of current message"
+  `(save-excursion
+     (let ((filename (notmuch-show-get-filename)))
+       (let ((buf (generate-new-buffer (concat "*notmuch-msg-" filename "*"))))
+         (with-current-buffer buf
+           (insert-file-contents filename nil nil nil t)
+           ,@body)
+        (kill-buffer buf)))))
+
+(defun notmuch-show-view-all-mime-parts ()
+  "Use external viewers to view all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   ; We ovverride the mm-inline-media-tests to indicate which message
+   ; parts are already sufficiently handled by the original
+   ; presentation of the message in notmuch-show mode. These parts
+   ; will be inserted directly into the temporary buffer of
+   ; with-current-notmuch-show-message and silently discarded.
+   ;
+   ; Any MIME part not explicitly mentioned here will be handled by an
+   ; external viewer as configured in the various mailcap files.
+   (let ((mm-inline-media-tests '(
+                                 ("text/.*" ignore identity)
+                                 ("application/pgp-signature" ignore identity)
+                                 ("multipart/alternative" ignore identity)
+                                 ("multipart/mixed" ignore identity)
+                                 ("multipart/related" ignore identity)
+                                )))
+     (mm-display-parts (mm-dissect-buffer)))))
+
+(defun notmuch-foreach-mime-part (function mm-handle)
+  (cond ((stringp (car mm-handle))
+         (dolist (part (cdr mm-handle))
+           (notmuch-foreach-mime-part function part)))
+        ((bufferp (car mm-handle))
+         (funcall function mm-handle))
+        (t (dolist (part mm-handle)
+             (notmuch-foreach-mime-part function part)))))
+
+(defun notmuch-count-attachments (mm-handle)
+  (let ((count 0))
+    (notmuch-foreach-mime-part
+     (lambda (p)
+       (let ((disposition (mm-handle-disposition p)))
+         (and (listp disposition)
+              (or (equal (car disposition) "attachment")
+                  (and (equal (car disposition) "inline")
+                       (assq 'filename disposition)))
+              (incf count))))
+     mm-handle)
+    count))
+
+(defun notmuch-save-attachments (mm-handle &optional queryp)
+  (notmuch-foreach-mime-part
+   (lambda (p)
+     (let ((disposition (mm-handle-disposition p)))
+       (and (listp disposition)
+            (or (equal (car disposition) "attachment")
+                (and (equal (car disposition) "inline")
+                     (assq 'filename disposition)))
+            (or (not queryp)
+                (y-or-n-p
+                 (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
+            (mm-save-part p))))
+   mm-handle))
+
+(defun notmuch-show-save-attachments ()
+  "Save all attachments from the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   (let ((mm-handle (mm-dissect-buffer)))
+     (notmuch-save-attachments
+      mm-handle (> (notmuch-count-attachments mm-handle) 1))))
+  (message "Done"))
+
+(defun notmuch-show-fontify-header ()
+  (let ((face (cond
+              ((looking-at "[Tt]o:")
+               'message-header-to)
+              ((looking-at "[Bb]?[Cc][Cc]:")
+               'message-header-cc)
+              ((looking-at "[Ss]ubject:")
+               'message-header-subject)
+              ((looking-at "[Ff]rom:")
+               'message-header-from)
+              (t
+               'message-header-other))))
+
+    (overlay-put (make-overlay (point) (re-search-forward ":"))
+                'face 'message-header-name)
+    (overlay-put (make-overlay (point) (re-search-forward ".*$"))
+                'face face)))
+
+(defun notmuch-show-colour-headers ()
+  "Apply some colouring to the current headers."
+  (goto-char (point-min))
+  (while (looking-at "^[A-Za-z][-A-Za-z0-9]*:")
+    (notmuch-show-fontify-header)
+    (forward-line)))
+
+(defun notmuch-show-spaces-n (n)
+  "Return a string comprised of `n' spaces."
+  (make-string n ? ))
+
+(defun notmuch-show-update-tags (tags)
+  "Update the displayed tags of the current message."
+  (save-excursion
+    (goto-char (notmuch-show-message-top))
+    (if (re-search-forward "(\\([^()]*\\))$" (line-end-position) t)
+       (let ((inhibit-read-only t))
+         (replace-match (concat "("
+                                (mapconcat 'identity tags " ")
+                                ")"))))))
+
+(defun notmuch-show-insert-headerline (headers date tags depth)
+  "Insert a notmuch style headerline based on HEADERS for a
+message at DEPTH in the current thread."
+  (let ((start (point)))
+    (insert (notmuch-show-spaces-n depth)
+           (plist-get headers :From)
+           " ("
+           date
+           ") ("
+           (mapconcat 'identity tags " ")
+           ")\n")
+    (overlay-put (make-overlay start (point)) 'face 'notmuch-message-summary-face)))
+
+(defun notmuch-show-insert-header (header header-value)
+  "Insert a single header."
+  (insert header ": " header-value "\n"))
+
+(defun notmuch-show-insert-headers (headers)
+  "Insert the headers of the current message."
+  (let ((start (point)))
+    (mapc '(lambda (header)
+            (let* ((header-symbol (intern (concat ":" header)))
+                   (header-value (plist-get headers header-symbol)))
+              (if (and header-value
+                       (not (string-equal "" header-value)))
+                  (notmuch-show-insert-header header header-value))))
+         notmuch-show-headers)
+    (save-excursion
+      (save-restriction
+       (narrow-to-region start (point-max))
+       (run-hooks 'notmuch-show-markup-headers-hook)))))
+
+(define-button-type 'notmuch-show-part-button-type
+  'action 'notmuch-show-part-button-action
+  'follow-link t
+  'face 'message-mml)
+
+(defun notmuch-show-insert-part-header (nth content-type declared-type &optional name)
+  (insert-button
+   (concat "[ "
+          (if name (concat name ": ") "")
+          declared-type
+          (if (not (string-equal declared-type content-type))
+              (concat " (as " content-type ")")
+            "")
+          " ]\n")
+   :type 'notmuch-show-part-button-type
+   :notmuch-part nth
+   :notmuch-filename name))
+
+;; Functions handling particular MIME parts.
+
+(defun notmuch-show-save-part (message-id nth &optional filename)
+  (with-temp-buffer
+    ;; Always acquires the part via `notmuch part', even if it is
+    ;; available in the JSON output.
+    (insert (notmuch-show-get-bodypart-internal message-id nth))
+    (let ((file (read-file-name
+                "Filename to save as: "
+                (or mailcap-download-directory "~/")
+                nil nil
+                filename))
+         (require-final-newline nil)
+          (coding-system-for-write 'no-conversion))
+      (write-region (point-min) (point-max) file))))
+
+(defun notmuch-show-mm-display-part-inline (msg part content-type content)
+  "Use the mm-decode/mm-view functions to display a part in the
+current buffer, if possible."
+  (let ((display-buffer (current-buffer)))
+    (with-temp-buffer
+      (insert content)
+      (let ((handle (mm-make-handle (current-buffer) (list content-type))))
+       (set-buffer display-buffer)
+       (if (and (mm-inlinable-p handle)
+                (mm-inlined-p handle))
+           (progn
+             (mm-display-part handle)
+             t)
+         nil)))))
+
+(defun notmuch-show-insert-part-text/plain (msg part content-type nth depth declared-type)
+  (let ((start (point)))
+    ;; If this text/plain part is not the first part in the message,
+    ;; insert a header to make this clear.
+    (if (> nth 1)
+       (notmuch-show-insert-part-header nth declared-type content-type (plist-get part :filename)))
+    (insert (notmuch-show-get-bodypart-content msg part nth))
+    (save-excursion
+      (save-restriction
+       (narrow-to-region start (point-max))
+       (run-hook-with-args 'notmuch-show-insert-text/plain-hook depth))))
+  t)
+
+(defun notmuch-show-insert-part-application/octet-stream (msg part content-type nth depth declared-type)
+  ;; If we can deduce a MIME type from the filename of the attachment,
+  ;; do so and pass it on to the handler for that type.
+  (if (plist-get part :filename)
+      (let ((extension (file-name-extension (plist-get part :filename)))
+           mime-type)
+       (if extension
+           (progn
+             (mailcap-parse-mimetypes)
+             (setq mime-type (mailcap-extension-to-mime extension))
+             (if (and mime-type
+                      (not (string-equal mime-type "application/octet-stream")))
+                 (notmuch-show-insert-bodypart-internal msg part mime-type nth depth content-type)
+               nil))
+         nil))))
+
+(defun notmuch-show-insert-part-*/* (msg part content-type nth depth declared-type)
+  ;; This handler _must_ succeed - it is the handler of last resort.
+  (notmuch-show-insert-part-header nth content-type declared-type (plist-get part :filename))
+  (let ((content (notmuch-show-get-bodypart-content msg part nth)))
+    (if content
+       (notmuch-show-mm-display-part-inline msg part content-type content)))
+  t)
+
+;; Functions for determining how to handle MIME parts.
+
+(defun notmuch-show-split-content-type (content-type)
+  (split-string content-type "/"))
+
+(defun notmuch-show-handlers-for (content-type)
+  "Return a list of content handlers for a part of type CONTENT-TYPE."
+  (let (result)
+    (mapc (lambda (func)
+           (if (functionp func)
+               (push func result)))
+         ;; Reverse order of prefrence.
+         (list (intern (concat "notmuch-show-insert-part-*/*"))
+               (intern (concat
+                        "notmuch-show-insert-part-"
+                        (car (notmuch-show-split-content-type content-type))
+                        "/*"))
+               (intern (concat "notmuch-show-insert-part-" content-type))))
+    result))
+
+;; Helper for parts which are generally not included in the default
+;; JSON output.
+
+(defun notmuch-show-get-bodypart-internal (message-id part-number)
+  (with-temp-buffer
+    (let ((coding-system-for-read 'no-conversion))
+      (call-process notmuch-command nil t nil
+                   "part" (format "--part=%s" part-number) message-id)
+      (buffer-string))))
+
+(defun notmuch-show-get-bodypart-content (msg part nth)
+  (or (plist-get part :content)
+      (notmuch-show-get-bodypart-internal (concat "id:" (plist-get msg :id)) nth)))
+
+;; \f
+
+(defun notmuch-show-insert-bodypart-internal (msg part content-type nth depth declared-type)
+  (let ((handlers (notmuch-show-handlers-for content-type)))
+    ;; Run the content handlers until one of them returns a non-nil
+    ;; value.
+    (while (and handlers
+               (not (funcall (car handlers) msg part content-type nth depth declared-type)))
+      (setq handlers (cdr handlers))))
+  t)
+
+(defun notmuch-show-insert-bodypart (msg part depth)
+  "Insert the body part PART at depth DEPTH in the current thread."
+  (let ((content-type (downcase (plist-get part :content-type)))
+       (nth (plist-get part :id)))
+    (notmuch-show-insert-bodypart-internal msg part content-type nth depth content-type))
+  ;; Some of the body part handlers leave point somewhere up in the
+  ;; part, so we make sure that we're down at the end.
+  (goto-char (point-max))
+  ;; Ensure that the part ends with a carriage return.
+  (if (not (bolp))
+      (insert "\n")))
+
+(defun notmuch-show-insert-body (msg body depth)
+  "Insert the body BODY at depth DEPTH in the current thread."
+  (mapc '(lambda (part) (notmuch-show-insert-bodypart msg part depth)) body))
+
+(defun notmuch-show-make-symbol (type)
+  (make-symbol (concat "notmuch-show-" type)))
+
+(defun notmuch-show-insert-msg (msg depth)
+  "Insert the message MSG at depth DEPTH in the current thread."
+  (let ((headers (plist-get msg :headers))
+       ;; Indentation causes the buffer offset of the start/end
+       ;; points to move, so we must use markers.
+       message-start message-end
+       content-start content-end
+       headers-start headers-end
+       body-start body-end
+       (headers-invis-spec (notmuch-show-make-symbol "header"))
+       (message-invis-spec (notmuch-show-make-symbol "message")))
+
+    (setq message-start (point-marker))
+
+    (notmuch-show-insert-headerline headers
+                                   (or (plist-get msg :date_relative)
+                                       (plist-get headers :Date))
+                                   (plist-get msg :tags) depth)
+
+    (setq content-start (point-marker))
+
+    ;; Set `headers-start' to point after the 'Subject:' header to be
+    ;; compatible with the existing implementation. This just sets it
+    ;; to after the first header.
+    (notmuch-show-insert-headers headers)
+    ;; Headers should include a blank line (backwards compatibility).
+    (insert "\n")
+    (save-excursion
+      (goto-char content-start)
+      (forward-line 1)
+      (setq headers-start (point-marker)))
+    (setq headers-end (point-marker))
+
+    (setq body-start (point-marker))
+    (notmuch-show-insert-body msg (plist-get msg :body) depth)
+    ;; Ensure that the body ends with a newline.
+    (if (not (bolp))
+       (insert "\n"))
+    (setq body-end (point-marker))
+    (setq content-end (point-marker))
+
+    ;; Indent according to the depth in the thread.
+    (indent-rigidly content-start content-end depth)
+
+    (setq message-end (point-max-marker))
+
+    ;; Save the extents of this message over the whole text of the
+    ;; message.
+    (put-text-property message-start message-end :notmuch-message-extent (cons message-start message-end))
+
+    (plist-put msg :headers-invis-spec headers-invis-spec)
+    (overlay-put (make-overlay headers-start headers-end) 'invisible headers-invis-spec)
+
+    (plist-put msg :message-invis-spec message-invis-spec)
+    (overlay-put (make-overlay body-start body-end) 'invisible message-invis-spec)
+
+    ;; Save the properties for this message. Currently this saves the
+    ;; entire message (augmented it with other stuff), which seems
+    ;; like overkill. We might save a reduced subset (for example, not
+    ;; the content).
+    (notmuch-show-set-message-properties msg)
+
+    ;; Headers are hidden by default.
+    (notmuch-show-headers-visible msg nil)
+
+    ;; Message visibility depends on whether it matched the search
+    ;; criteria.
+    (notmuch-show-message-visible msg (plist-get msg :match))))
+
+(defun notmuch-show-insert-tree (tree depth)
+  "Insert the message tree TREE at depth DEPTH in the current thread."
+  (let ((msg (car tree))
+       (replies (cadr tree)))
+    (notmuch-show-insert-msg msg depth)
+    (notmuch-show-insert-thread replies (1+ depth))))
+
+(defun notmuch-show-insert-thread (thread depth)
+  "Insert the thread THREAD at depth DEPTH in the current forest."
+  (mapc '(lambda (tree) (notmuch-show-insert-tree tree depth)) thread))
+
+(defun notmuch-show-insert-forest (forest)
+  "Insert the forest of threads FOREST."
+  (mapc '(lambda (thread) (notmuch-show-insert-thread thread 0)) forest))
+
+(defvar notmuch-show-parent-buffer nil)
+
+;;;###autoload
+(defun notmuch-show (thread-id &optional parent-buffer query-context buffer-name)
+  "Run \"notmuch show\" with the given thread ID and display results.
+
+The optional PARENT-BUFFER is the notmuch-search buffer from
+which this notmuch-show command was executed, (so that the
+next thread from that buffer can be show when done with this
+one).
+
+The optional QUERY-CONTEXT is a notmuch search term. Only
+messages from the thread matching this search term are shown if
+non-nil.
+
+The optional BUFFER-NAME provides the neame of the buffer in
+which the message thread is shown. If it is nil (which occurs
+when the command is called interactively) the argument to the
+function is used. "
+  (interactive "sNotmuch show: ")
+  (let ((buffer (get-buffer-create (generate-new-buffer-name
+                                   (or buffer-name
+                                       (concat "*notmuch-" thread-id "*")))))
+       (inhibit-read-only t))
+    (switch-to-buffer buffer)
+    (notmuch-show-mode)
+    (set (make-local-variable 'notmuch-show-parent-buffer) parent-buffer)
+    (erase-buffer)
+    (goto-char (point-min))
+    (save-excursion
+      (let* ((basic-args (list thread-id))
+            (args (if query-context
+                      (append basic-args (list "and (" query-context ")"))
+                    basic-args)))
+       (notmuch-show-insert-forest (notmuch-query-get-threads args))
+       ;; If the query context reduced the results to nothing, run
+       ;; the basic query.
+       (when (and (eq (buffer-size) 0)
+                  query-context)
+         (notmuch-show-insert-forest
+          (notmuch-query-get-threads basic-args))))
+      (run-hooks 'notmuch-show-hook))
+
+    ;; Move straight to the first open message
+    (if (not (notmuch-show-message-visible-p))
+       (notmuch-show-next-open-message))
+    (notmuch-show-mark-read)))
+
+(defvar notmuch-show-stash-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "c" 'notmuch-show-stash-cc)
+    (define-key map "d" 'notmuch-show-stash-date)
+    (define-key map "F" 'notmuch-show-stash-filename)
+    (define-key map "f" 'notmuch-show-stash-from)
+    (define-key map "i" 'notmuch-show-stash-message-id)
+    (define-key map "s" 'notmuch-show-stash-subject)
+    (define-key map "T" 'notmuch-show-stash-tags)
+    (define-key map "t" 'notmuch-show-stash-to)
+    map)
+  "Submap for stash commands")
+(fset 'notmuch-show-stash-map notmuch-show-stash-map)
+
+(defvar notmuch-show-mode-map
+      (let ((map (make-sparse-keymap)))
+       (define-key map "?" 'notmuch-help)
+       (define-key map "q" 'kill-this-buffer)
+       (define-key map (kbd "M-TAB") 'notmuch-show-previous-button)
+       (define-key map (kbd "TAB") 'notmuch-show-next-button)
+       (define-key map "s" 'notmuch-search)
+       (define-key map "m" 'message-mail)
+       (define-key map "f" 'notmuch-show-forward-message)
+       (define-key map "r" 'notmuch-show-reply)
+       (define-key map "|" 'notmuch-show-pipe-message)
+       (define-key map "w" 'notmuch-show-save-attachments)
+       (define-key map "V" 'notmuch-show-view-raw-message)
+       (define-key map "v" 'notmuch-show-view-all-mime-parts)
+       (define-key map "c" 'notmuch-show-stash-map)
+       (define-key map "h" 'notmuch-show-toggle-headers)
+       (define-key map "-" 'notmuch-show-remove-tag)
+       (define-key map "+" 'notmuch-show-add-tag)
+       (define-key map "x" 'notmuch-show-archive-thread-then-exit)
+       (define-key map "a" 'notmuch-show-archive-thread)
+       (define-key map "N" 'notmuch-show-next-message)
+       (define-key map "P" 'notmuch-show-previous-message)
+       (define-key map "n" 'notmuch-show-next-open-message)
+       (define-key map "p" 'notmuch-show-previous-open-message)
+       (define-key map (kbd "DEL") 'notmuch-show-rewind)
+       (define-key map " " 'notmuch-show-advance-and-archive)
+       (define-key map (kbd "RET") 'notmuch-show-toggle-message)
+       map)
+      "Keymap for \"notmuch show\" buffers.")
+(fset 'notmuch-show-mode-map notmuch-show-mode-map)
+
+;;;###autoload
+(defun notmuch-show-mode ()
+  "Major mode for viewing a thread with notmuch.
+
+This buffer contains the results of the \"notmuch show\" command
+for displaying a single thread of email from your email archives.
+
+By default, various components of email messages, (citations,
+signatures, already-read messages), are hidden. You can make
+these parts visible by clicking with the mouse button or by
+pressing RET after positioning the cursor on a hidden part, (for
+which \\[notmuch-show-next-button] and
+\\[notmuch-show-previous-button] are helpful).
+
+Reading the thread sequentially is well-supported by pressing
+\\[notmuch-show-advance-and-archive]. This will scroll the
+current message (if necessary), advance to the next message, or
+advance to the next thread (if already on the last message of a
+thread).
+
+Other commands are available to read or manipulate the thread
+more selectively, (such as '\\[notmuch-show-next-message]' and
+'\\[notmuch-show-previous-message]' to advance to messages
+without removing any tags, and '\\[notmuch-show-archive-thread]'
+to archive an entire thread without scrolling through with
+\\[notmuch-show-advance-and-archive]).
+
+You can add or remove arbitary tags from the current message with
+'\\[notmuch-show-add-tag]' or '\\[notmuch-show-remove-tag]'.
+
+All currently available key bindings:
+
+\\{notmuch-show-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (use-local-map notmuch-show-mode-map)
+  (setq major-mode 'notmuch-show-mode
+       mode-name "notmuch-show")
+  (setq buffer-read-only t))
+
+(defun notmuch-show-move-to-message-top ()
+  (goto-char (notmuch-show-message-top)))
+
+(defun notmuch-show-move-to-message-bottom ()
+  (goto-char (notmuch-show-message-bottom)))
+
+(defun notmuch-show-message-adjust ()
+  (recenter 0))
+
+;; Movement related functions.
+
+;; There's some strangeness here where a text property applied to a
+;; region a->b is not found when point is at b. We walk backwards
+;; until finding the property.
+(defun notmuch-show-message-extent ()
+  (let (r)
+    (save-excursion
+      (while (not (setq r (get-text-property (point) :notmuch-message-extent)))
+       (backward-char)))
+    r))
+
+(defun notmuch-show-message-top ()
+  (car (notmuch-show-message-extent)))
+
+(defun notmuch-show-message-bottom ()
+  (cdr (notmuch-show-message-extent)))
+
+(defun notmuch-show-goto-message-next ()
+  (let ((start (point)))
+    (notmuch-show-move-to-message-bottom)
+    (if (not (eobp))
+       t
+      (goto-char start)
+      nil)))
+
+(defun notmuch-show-goto-message-previous ()
+  (notmuch-show-move-to-message-top)
+  (if (bobp)
+      nil
+    (backward-char)
+    (notmuch-show-move-to-message-top)
+    t))
+
+(defun notmuch-show-move-past-invisible-forward ()
+  (while (point-invisible-p)
+    (forward-char)))
+
+(defun notmuch-show-move-past-invisible-backward ()
+  (while (point-invisible-p)
+    (backward-char)))
+
+;; Functions relating to the visibility of messages and their
+;; components.
+
+(defun notmuch-show-element-visible (props visible-p spec-property)
+  (let ((spec (plist-get props spec-property)))
+    (if visible-p
+       (remove-from-invisibility-spec spec)
+      (add-to-invisibility-spec spec))))
+
+(defun notmuch-show-message-visible (props visible-p)
+  (if visible-p
+      ;; When making the message visible, the headers may or not be
+      ;; visible. So we check that property separately.
+      (let ((headers-visible (plist-get props :headers-visible)))
+       (notmuch-show-element-visible props headers-visible :headers-invis-spec)
+       (notmuch-show-element-visible props t :message-invis-spec))
+    (notmuch-show-element-visible props nil :headers-invis-spec)
+    (notmuch-show-element-visible props nil :message-invis-spec))
+
+  (notmuch-show-set-prop :message-visible visible-p props))
+
+(defun notmuch-show-headers-visible (props visible-p)
+  (if (plist-get props :message-visible)
+      (notmuch-show-element-visible props visible-p :headers-invis-spec))
+  (notmuch-show-set-prop :headers-visible visible-p props))
+
+;; Functions for setting and getting attributes of the current
+;; message.
+
+(defun notmuch-show-set-message-properties (props)
+  (save-excursion
+    (notmuch-show-move-to-message-top)
+    (put-text-property (point) (+ (point) 1) :notmuch-message-properties props)))
+
+(defun notmuch-show-get-message-properties ()
+  (save-excursion
+    (notmuch-show-move-to-message-top)
+    (get-text-property (point) :notmuch-message-properties)))
+
+(defun notmuch-show-set-prop (prop val &optional props)
+  (let ((inhibit-read-only t)
+       (props (or props
+                  (notmuch-show-get-message-properties))))
+    (plist-put props prop val)
+    (notmuch-show-set-message-properties props)))
+
+(defun notmuch-show-get-prop (prop &optional props)
+  (let ((props (or props
+                  (notmuch-show-get-message-properties))))
+    (plist-get props prop)))
+
+(defun notmuch-show-get-message-id ()
+  "Return the message id of the current message."
+  (concat "id:" (notmuch-show-get-prop :id)))
+
+;; dme: Would it make sense to use a macro for many of these?
+
+(defun notmuch-show-get-filename ()
+  "Return the filename of the current message."
+  (notmuch-show-get-prop :filename))
+
+(defun notmuch-show-get-header (header)
+  "Return the named header of the current message, if any."
+  (plist-get (notmuch-show-get-prop :headers) header))
+
+(defun notmuch-show-get-cc ()
+  (notmuch-show-get-header :Cc))
+
+(defun notmuch-show-get-date ()
+  (notmuch-show-get-header :Date))
+
+(defun notmuch-show-get-from ()
+  (notmuch-show-get-header :From))
+
+(defun notmuch-show-get-subject ()
+  (notmuch-show-get-header :Subject))
+
+(defun notmuch-show-get-to ()
+  (notmuch-show-get-header :To))
+
+(defun notmuch-show-set-tags (tags)
+  "Set the tags of the current message."
+  (notmuch-show-set-prop :tags tags)
+  (notmuch-show-update-tags tags))
+
+(defun notmuch-show-get-tags ()
+  "Return the tags of the current message."
+  (notmuch-show-get-prop :tags))
+
+(defun notmuch-show-message-visible-p ()
+  "Is the current message visible?"
+  (notmuch-show-get-prop :message-visible))
+
+(defun notmuch-show-headers-visible-p ()
+  "Are the headers of the current message visible?"
+  (notmuch-show-get-prop :headers-visible))
+
+(defun notmuch-show-mark-read ()
+  "Mark the current message as read."
+  (notmuch-show-remove-tag "unread"))
+
+;; Commands typically bound to keys.
+
+(defun notmuch-show-advance-and-archive ()
+  "Advance through thread and archive.
+
+This command is intended to be one of the simplest ways to
+process a thread of email. It does the following:
+
+If the current message in the thread is not yet fully visible,
+scroll by a near screenful to read more of the message.
+
+Otherwise, (the end of the current message is already within the
+current window), advance to the next open message.
+
+Finally, if there is no further message to advance to, and this
+last message is already read, then archive the entire current
+thread, (remove the \"inbox\" tag from each message). Also kill
+this buffer, and display the next thread from the search from
+which this thread was originally shown."
+  (interactive)
+  (let ((end-of-this-message (notmuch-show-message-bottom)))
+    (cond
+     ;; Ideally we would test `end-of-this-message' against the result
+     ;; of `window-end', but that doesn't account for the fact that
+     ;; the end of the message might be hidden, so we have to actually
+     ;; go to the end, walk back over invisible text and then see if
+     ;; point is visible.
+     ((save-excursion
+       (goto-char (- end-of-this-message 1))
+       (notmuch-show-move-past-invisible-backward)
+       (> (point) (window-end)))
+      ;; The bottom of this message is not visible - scroll.
+      (scroll-up nil))
+
+     ((not (= end-of-this-message (point-max)))
+      ;; This is not the last message - move to the next visible one.
+      (notmuch-show-next-open-message))
+
+     (t
+      ;; This is the last message - archive the thread.
+      (notmuch-show-archive-thread)))))
+
+(defun notmuch-show-rewind ()
+  "Backup through the thread, (reverse scrolling compared to \\[notmuch-show-advance-and-archive]).
+
+Specifically, if the beginning of the previous email is fewer
+than `window-height' lines from the current point, move to it
+just like `notmuch-show-previous-message'.
+
+Otherwise, just scroll down a screenful of the current message.
+
+This command does not modify any message tags, (it does not undo
+any effects from previous calls to
+`notmuch-show-advance-and-archive'."
+  (interactive)
+  (let ((start-of-message (notmuch-show-message-top))
+       (start-of-window (window-start)))
+    (cond
+      ;; Either this message is properly aligned with the start of the
+      ;; window or the start of this message is not visible on the
+      ;; screen - scroll.
+     ((or (= start-of-message start-of-window)
+         (< start-of-message start-of-window))
+      (scroll-down)
+      ;; If a small number of lines from the previous message are
+      ;; visible, realign so that the top of the current message is at
+      ;; the top of the screen.
+      (if (< (count-lines (window-start) (notmuch-show-message-top))
+            next-screen-context-lines)
+         (progn
+           (goto-char (notmuch-show-message-top))
+           (notmuch-show-message-adjust)))
+      ;; Move to the top left of the window.
+      (goto-char (window-start)))
+     (t
+      ;; Move to the previous message.
+      (notmuch-show-previous-message)))))
+
+(defun notmuch-show-reply ()
+  "Reply to the current message."
+  (interactive)
+  (notmuch-reply (notmuch-show-get-message-id)))
+
+(defun notmuch-show-forward-message ()
+  "Forward the current message."
+  (interactive)
+  (with-current-notmuch-show-message
+   (message-forward)))
+
+(defun notmuch-show-next-message ()
+  "Show the next message."
+  (interactive)
+  (notmuch-show-goto-message-next)
+  (notmuch-show-mark-read)
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-previous-message ()
+  "Show the previous message."
+  (interactive)
+  (notmuch-show-goto-message-previous)
+  (notmuch-show-mark-read)
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-next-open-message ()
+  "Show the next message."
+  (interactive)
+  (while (and (notmuch-show-goto-message-next)
+             (not (notmuch-show-message-visible-p))))
+  (notmuch-show-mark-read)
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-previous-open-message ()
+  "Show the previous message."
+  (interactive)
+  (while (and (notmuch-show-goto-message-previous)
+             (not (notmuch-show-message-visible-p))))
+  (notmuch-show-mark-read)
+  (notmuch-show-message-adjust))
+
+(defun notmuch-show-view-raw-message ()
+  "View the file holding the current message."
+  (interactive)
+  (view-file (notmuch-show-get-filename)))
+
+(defun notmuch-show-pipe-message (command)
+  "Pipe the contents of the current message to the given command.
+
+The given command will be executed with the raw contents of the
+current email message as stdin. Anything printed by the command
+to stdout or stderr will appear in the *Messages* buffer."
+  (interactive "sPipe message to command: ")
+  (apply 'start-process-shell-command "notmuch-pipe-command" "*notmuch-pipe*"
+        (list command " < "
+              (shell-quote-argument (notmuch-show-get-filename)))))
+
+(defun notmuch-show-add-tag (&rest toadd)
+  "Add a tag to the current message."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to add: ")))
+  (apply 'notmuch-call-notmuch-process
+        (append (cons "tag"
+                      (mapcar (lambda (s) (concat "+" s)) toadd))
+                (cons (notmuch-show-get-message-id) nil)))
+  (notmuch-show-set-tags (sort (union toadd (notmuch-show-get-tags) :test 'string=) 'string<)))
+
+(defun notmuch-show-remove-tag (&rest toremove)
+  "Remove a tag from the current message."
+  (interactive
+   (list (notmuch-select-tag-with-completion
+         "Tag to remove: " (notmuch-show-get-message-id))))
+  (let ((tags (notmuch-show-get-tags)))
+    (if (intersection tags toremove :test 'string=)
+       (progn
+         (apply 'notmuch-call-notmuch-process
+                (append (cons "tag"
+                              (mapcar (lambda (s) (concat "-" s)) toremove))
+                        (cons (notmuch-show-get-message-id) nil)))
+         (notmuch-show-set-tags (sort (set-difference tags toremove :test 'string=) 'string<))))))
+
+(defun notmuch-show-toggle-headers ()
+  "Toggle the visibility of the current message headers."
+  (interactive)
+  (let ((props (notmuch-show-get-message-properties)))
+    (notmuch-show-headers-visible
+     props
+     (not (plist-get props :headers-visible))))
+  (force-window-update))
+
+(defun notmuch-show-toggle-message ()
+  "Toggle the visibility of the current message."
+  (interactive)
+  (let ((props (notmuch-show-get-message-properties)))
+    (notmuch-show-message-visible
+     props
+     (not (plist-get props :message-visible))))
+  (force-window-update))
+
+(defun notmuch-show-next-button ()
+  "Advance point to the next button in the buffer."
+  (interactive)
+  (forward-button 1))
+
+(defun notmuch-show-previous-button ()
+  "Move point back to the previous button in the buffer."
+  (interactive)
+  (backward-button 1))
+
+(defun notmuch-show-archive-thread-internal (show-next)
+  ;; Remove the tag from the current set of messages.
+  (goto-char (point-min))
+  (loop do (notmuch-show-remove-tag "inbox")
+       until (not (notmuch-show-goto-message-next)))
+  ;; Move to the next item in the search results, if any.
+  (let ((parent-buffer notmuch-show-parent-buffer))
+    (kill-this-buffer)
+    (if parent-buffer
+       (progn
+         (switch-to-buffer parent-buffer)
+         (forward-line)
+         (if show-next
+             (notmuch-search-show-thread))))))
+
+(defun notmuch-show-archive-thread ()
+  "Archive each message in thread, then show next thread from search.
+
+Archive each message currently shown by removing the \"inbox\"
+tag from each. Then kill this buffer and show the next thread
+from the search from which this thread was originally shown.
+
+Note: This command is safe from any race condition of new messages
+being delivered to the same thread. It does not archive the
+entire thread, but only the messages shown in the current
+buffer."
+  (interactive)
+  (notmuch-show-archive-thread-internal t))
+
+(defun notmuch-show-archive-thread-then-exit ()
+  "Archive each message in thread, then exit back to search results."
+  (interactive)
+  (notmuch-show-archive-thread-internal nil))
+
+(defun notmuch-show-do-stash (text)
+  (kill-new text)
+  (message "Saved: %s" text))
+
+(defun notmuch-show-stash-cc ()
+  "Copy CC field of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-cc)))
+
+(defun notmuch-show-stash-date ()
+  "Copy date of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-date)))
+
+(defun notmuch-show-stash-filename ()
+  "Copy filename of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-filename)))
+
+(defun notmuch-show-stash-from ()
+  "Copy From address of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-from)))
+
+(defun notmuch-show-stash-message-id ()
+  "Copy message ID of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-message-id)))
+
+(defun notmuch-show-stash-subject ()
+  "Copy Subject field of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-subject)))
+
+(defun notmuch-show-stash-tags ()
+  "Copy tags of current message to kill-ring as a comma separated list."
+  (interactive)
+  (notmuch-show-do-stash (mapconcat 'identity (notmuch-show-get-tags) ",")))
+
+(defun notmuch-show-stash-to ()
+  "Copy To address of current message to kill-ring."
+  (interactive)
+  (notmuch-show-do-stash (notmuch-show-get-to)))
+
+;; Commands typically bound to buttons.
+
+(defun notmuch-show-part-button-action (button)
+  (let ((nth (button-get button :notmuch-part)))
+    (if nth
+       (notmuch-show-save-part (notmuch-show-get-message-id) nth
+                               (button-get button :notmuch-filename))
+      (message "Not a valid part (is it a fake part?)."))))
+
+;;
+
+(provide 'notmuch-show)
diff --git a/emacs/notmuch-wash.el b/emacs/notmuch-wash.el
new file mode 100644 (file)
index 0000000..54a380a
--- /dev/null
@@ -0,0 +1,150 @@
+;; notmuch-wash.el --- cleaning up message bodies
+;;
+;; Copyright © Carl Worth
+;;
+;; 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/>.
+;;
+;; Authors: Carl Worth <cworth@cworth.org>
+
+(defvar notmuch-wash-signature-regexp
+  "^\\(-- ?\\|_+\\)$"
+  "Pattern to match a line that separates content from signature.")
+
+(defvar notmuch-wash-citation-regexp
+  "\\(^[[:space:]]*>.*\n\\)+"
+  "Pattern to match citation lines.")
+
+(defvar notmuch-wash-signature-button-format
+  "[ %d-line signature. Click/Enter to toggle visibility. ]"
+  "String used to construct button text for hidden signatures.
+Can use up to one integer format parameter, i.e. %d")
+
+(defvar notmuch-wash-citation-button-format
+  "[ %d more citation lines. Click/Enter to toggle visibility. ]"
+  "String used to construct button text for hidden citations.
+Can use up to one integer format parameter, i.e. %d")
+
+(defvar notmuch-wash-signature-lines-max 12
+  "Maximum length of signature that will be hidden by default.")
+
+(defvar notmuch-wash-citation-lines-prefix 3
+  "Always show at least this many lines from the start of a citation.
+
+If there is one more line than the sum of
+`notmuch-wash-citation-lines-prefix' and
+`notmuch-wash-citation-lines-suffix', show that, otherwise
+collapse the remaining lines into a button.")
+
+(defvar notmuch-wash-citation-lines-suffix 3
+  "Always show at least this many lines from the end of a citation.
+
+If there is one more line than the sum of
+`notmuch-wash-citation-lines-prefix' and
+`notmuch-wash-citation-lines-suffix', show that, otherwise
+collapse the remaining lines into a button.")
+
+(defun notmuch-wash-toggle-invisible-action (cite-button)
+  (let ((invis-spec (button-get cite-button 'invisibility-spec)))
+    (if (invisible-p invis-spec)
+       (remove-from-invisibility-spec invis-spec)
+      (add-to-invisibility-spec invis-spec)))
+  (force-window-update)
+  (redisplay t))
+
+(define-button-type 'notmuch-wash-button-invisibility-toggle-type
+  'action 'notmuch-wash-toggle-invisible-action
+  'follow-link t
+  'face 'font-lock-comment-face)
+
+(define-button-type 'notmuch-wash-button-citation-toggle-type
+  'help-echo "mouse-1, RET: Show citation"
+  :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
+(define-button-type 'notmuch-wash-button-signature-toggle-type
+  'help-echo "mouse-1, RET: Show signature"
+  :supertype 'notmuch-wash-button-invisibility-toggle-type)
+
+(defun notmuch-wash-region-to-button (beg end type prefix button-text)
+  "Auxilary function to do the actual making of overlays and buttons
+
+BEG and END are buffer locations. TYPE should a string, either
+\"citation\" or \"signature\". PREFIX is some arbitrary text to
+insert before the button, probably for indentation.  BUTTON-TEXT
+is what to put on the button."
+
+  ;; This uses some slightly tricky conversions between strings and
+  ;; symbols because of the way the button code works. Note that
+  ;; replacing intern-soft with make-symbol will cause this to fail,
+  ;; since the newly created symbol has no plist.
+
+  (let ((overlay (make-overlay beg end))
+       (invis-spec (make-symbol (concat "notmuch-" type "-region")))
+       (button-type (intern-soft (concat "notmuch-wash-button-"
+                                         type "-toggle-type"))))
+    (add-to-invisibility-spec invis-spec)
+    (overlay-put overlay 'invisible invis-spec)
+    (goto-char (1+ end))
+    (save-excursion
+      (goto-char (1- beg))
+      (insert prefix)
+      (insert-button button-text
+                    'invisibility-spec invis-spec
+                    :type button-type))))
+
+(defun notmuch-wash-text/plain-citations (depth)
+  "Markup citations, and up to one signature in the buffer."
+  (goto-char (point-min))
+  (beginning-of-line)
+  (while (and (< (point) (point-max))
+             (re-search-forward notmuch-wash-citation-regexp nil t))
+    (let* ((cite-start (match-beginning 0))
+          (cite-end (match-end 0))
+          (cite-lines (count-lines cite-start cite-end)))
+      (overlay-put (make-overlay cite-start cite-end) 'face 'message-cited-text-face)
+      (when (> cite-lines (+ notmuch-wash-citation-lines-prefix
+                            notmuch-wash-citation-lines-suffix
+                            1))
+       (goto-char cite-start)
+       (forward-line notmuch-wash-citation-lines-prefix)
+       (let ((hidden-start (point-marker)))
+         (goto-char cite-end)
+         (forward-line (- notmuch-wash-citation-lines-suffix))
+         (notmuch-wash-region-to-button
+          hidden-start (point-marker)
+          "citation" "\n"
+          (format notmuch-wash-citation-button-format
+                  (- cite-lines
+                     notmuch-wash-citation-lines-prefix
+                     notmuch-wash-citation-lines-suffix)))))))
+  (if (and (not (eobp))
+          (re-search-forward notmuch-wash-signature-regexp nil t))
+      (let* ((sig-start (match-beginning 0))
+            (sig-end (match-end 0))
+            (sig-lines (1- (count-lines sig-start (point-max)))))
+       (if (<= sig-lines notmuch-wash-signature-lines-max)
+           (let ((sig-start-marker (make-marker))
+                 (sig-end-marker (make-marker)))
+             (set-marker sig-start-marker sig-start)
+             (set-marker sig-end-marker (point-max))
+             (overlay-put (make-overlay sig-start-marker sig-end-marker) 'face 'message-cited-text-face)
+             (notmuch-wash-region-to-button
+              sig-start-marker sig-end-marker
+              "signature" "\n"
+              (format notmuch-wash-signature-button-format sig-lines)))))))
+
+;;
+
+(provide 'notmuch-wash)
diff --git a/emacs/notmuch.el b/emacs/notmuch.el
new file mode 100644 (file)
index 0000000..57b7fcf
--- /dev/null
@@ -0,0 +1,932 @@
+; notmuch.el --- run notmuch within emacs
+;
+; Copyright © Carl Worth
+;
+; 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/>.
+;
+; Authors: Carl Worth <cworth@cworth.org>
+
+; This is an emacs-based interface to the notmuch mail system.
+;
+; You will first need to have the notmuch program installed and have a
+; notmuch database built in order to use this. See
+; http://notmuchmail.org for details.
+;
+; To install this software, copy it to a directory that is on the
+; `load-path' variable within emacs (a good candidate is
+; /usr/local/share/emacs/site-lisp). If you are viewing this from the
+; notmuch source distribution then you can simply run:
+;
+;      sudo make install-emacs
+;
+; to install it.
+;
+; Then, to actually run it, add:
+;
+;      (require 'notmuch)
+;
+; to your ~/.emacs file, and then run "M-x notmuch" from within emacs,
+; or run:
+;
+;      emacs -f notmuch
+;
+; Have fun, and let us know if you have any comment, questions, or
+; kudos: Notmuch list <notmuch@notmuchmail.org> (subscription is not
+; required, but is available from http://notmuchmail.org).
+
+(require 'cl)
+(require 'mm-view)
+(require 'message)
+
+(require 'notmuch-lib)
+(require 'notmuch-show)
+
+(defcustom notmuch-search-authors-width 20
+  "Number of columns to use to display authors in a notmuch-search buffer."
+  :type 'integer
+  :group 'notmuch)
+
+(defcustom notmuch-search-result-format
+  `(("date" . "%s ")
+    ("count" . "%-7s ")
+    ("authors" . ,(format "%%-%ds " notmuch-search-authors-width))
+    ("subject" . "%s ")
+    ("tags" . "(%s)"))
+  "Search result formating. Supported fields are:
+       date, count, authors, subject, tags
+For example:
+       (setq notmuch-search-result-format \(\(\"authors\" . \"%-40s\"\)
+                                            \(\"subject\" . \"%s\"\)\)\)"
+  :type '(alist :key-type (string) :value-type (string))
+  :group 'notmuch)
+
+(defun notmuch-select-tag-with-completion (prompt &rest search-terms)
+  (let ((tag-list
+        (with-output-to-string
+          (with-current-buffer standard-output
+            (apply 'call-process notmuch-command nil t nil "search-tags" search-terms)))))
+    (completing-read prompt (split-string tag-list "\n+" t) nil nil nil)))
+
+(defun notmuch-foreach-mime-part (function mm-handle)
+  (cond ((stringp (car mm-handle))
+         (dolist (part (cdr mm-handle))
+           (notmuch-foreach-mime-part function part)))
+        ((bufferp (car mm-handle))
+         (funcall function mm-handle))
+        (t (dolist (part mm-handle)
+             (notmuch-foreach-mime-part function part)))))
+
+(defun notmuch-count-attachments (mm-handle)
+  (let ((count 0))
+    (notmuch-foreach-mime-part
+     (lambda (p)
+       (let ((disposition (mm-handle-disposition p)))
+         (and (listp disposition)
+              (or (equal (car disposition) "attachment")
+                  (and (equal (car disposition) "inline")
+                       (assq 'filename disposition)))
+              (incf count))))
+     mm-handle)
+    count))
+
+(defun notmuch-save-attachments (mm-handle &optional queryp)
+  (notmuch-foreach-mime-part
+   (lambda (p)
+     (let ((disposition (mm-handle-disposition p)))
+       (and (listp disposition)
+            (or (equal (car disposition) "attachment")
+                (and (equal (car disposition) "inline")
+                     (assq 'filename disposition)))
+            (or (not queryp)
+                (y-or-n-p
+                 (concat "Save '" (cdr (assq 'filename disposition)) "' ")))
+            (mm-save-part p))))
+   mm-handle))
+
+(defun notmuch-reply (query-string)
+  (switch-to-buffer (generate-new-buffer "notmuch-draft"))
+  (call-process notmuch-command nil t nil "reply" query-string)
+  (message-insert-signature)
+  (goto-char (point-min))
+  (if (re-search-forward "^$" nil t)
+      (progn
+       (insert "--text follows this line--")
+       (forward-line)))
+  (message-mode))
+
+(defun notmuch-documentation-first-line (symbol)
+  "Return the first line of the documentation string for SYMBOL."
+  (let ((doc (documentation symbol)))
+    (if doc
+       (with-temp-buffer
+         (insert (documentation symbol t))
+         (goto-char (point-min))
+         (let ((beg (point)))
+           (end-of-line)
+           (buffer-substring beg (point))))
+      "")))
+
+(defun notmuch-prefix-key-description (key)
+  "Given a prefix key code, return a human-readable string representation.
+
+This is basically just `format-kbd-macro' but we also convert ESC to M-."
+  (let ((desc (format-kbd-macro (vector key))))
+    (if (string= desc "ESC")
+       "M-"
+      (concat desc " "))))
+
+; I would think that emacs would have code handy for walking a keymap
+; and generating strings for each key, and I would prefer to just call
+; that. But I couldn't find any (could be all implemented in C I
+; suppose), so I wrote my own here.
+(defun notmuch-substitute-one-command-key-with-prefix (prefix binding)
+  "For a key binding, return a string showing a human-readable
+representation of the prefixed key as well as the first line of
+documentation from the bound function.
+
+For a mouse binding, return nil."
+  (let ((key (car binding))
+       (action (cdr binding)))
+    (if (mouse-event-p key)
+       nil
+      (if (keymapp action)
+         (let ((substitute (apply-partially 'notmuch-substitute-one-command-key-with-prefix (notmuch-prefix-key-description key)))
+               (as-list))
+           (map-keymap (lambda (a b)
+                         (push (cons a b) as-list))
+                       action)
+           (mapconcat substitute as-list "\n"))
+       (concat prefix (format-kbd-macro (vector key))
+               "\t"
+               (notmuch-documentation-first-line action))))))
+
+(defalias 'notmuch-substitute-one-command-key
+  (apply-partially 'notmuch-substitute-one-command-key-with-prefix nil))
+
+(defun notmuch-substitute-command-keys (doc)
+  "Like `substitute-command-keys' but with documentation, not function names."
+  (let ((beg 0))
+    (while (string-match "\\\\{\\([^}[:space:]]*\\)}" doc beg)
+      (let ((map (substring doc (match-beginning 1) (match-end 1))))
+       (setq doc (replace-match (mapconcat 'notmuch-substitute-one-command-key
+                                           (cdr (symbol-value (intern map))) "\n") 1 1 doc)))
+      (setq beg (match-end 0)))
+    doc))
+
+(defun notmuch-help ()
+  "Display help for the current notmuch mode."
+  (interactive)
+  (let* ((mode major-mode)
+        (doc (substitute-command-keys (notmuch-substitute-command-keys (documentation mode t)))))
+    (with-current-buffer (generate-new-buffer "*notmuch-help*")
+      (insert doc)
+      (goto-char (point-min))
+      (set-buffer-modified-p nil)
+      (view-buffer (current-buffer) 'kill-buffer-if-not-modified))))
+
+(defgroup notmuch nil
+  "Notmuch mail reader for Emacs."
+  :group 'mail)
+
+(defcustom notmuch-search-hook '(hl-line-mode)
+  "List of functions to call when notmuch displays the search results."
+  :type 'hook
+  :options '(hl-line-mode)
+  :group 'notmuch)
+
+(defvar notmuch-search-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "q" 'kill-this-buffer)
+    (define-key map "x" 'kill-this-buffer)
+    (define-key map (kbd "<DEL>") 'notmuch-search-scroll-down)
+    (define-key map "b" 'notmuch-search-scroll-down)
+    (define-key map " " 'notmuch-search-scroll-up)
+    (define-key map "<" 'notmuch-search-first-thread)
+    (define-key map ">" 'notmuch-search-last-thread)
+    (define-key map "p" 'notmuch-search-previous-thread)
+    (define-key map "n" 'notmuch-search-next-thread)
+    (define-key map "r" 'notmuch-search-reply-to-thread)
+    (define-key map "m" 'message-mail)
+    (define-key map "s" 'notmuch-search)
+    (define-key map "o" 'notmuch-search-toggle-order)
+    (define-key map "=" 'notmuch-search-refresh-view)
+    (define-key map "t" 'notmuch-search-filter-by-tag)
+    (define-key map "f" 'notmuch-search-filter)
+    (define-key map [mouse-1] 'notmuch-search-show-thread)
+    (define-key map "*" 'notmuch-search-operate-all)
+    (define-key map "a" 'notmuch-search-archive-thread)
+    (define-key map "-" 'notmuch-search-remove-tag)
+    (define-key map "+" 'notmuch-search-add-tag)
+    (define-key map (kbd "RET") 'notmuch-search-show-thread)
+    map)
+  "Keymap for \"notmuch search\" buffers.")
+(fset 'notmuch-search-mode-map notmuch-search-mode-map)
+
+(defvar notmuch-search-query-string)
+(defvar notmuch-search-target-thread)
+(defvar notmuch-search-target-line)
+(defvar notmuch-search-oldest-first t
+  "Show the oldest mail first in the search-mode")
+
+(defvar notmuch-search-disjunctive-regexp      "\\<[oO][rR]\\>")
+
+(defun notmuch-search-scroll-up ()
+  "Move forward through search results by one window's worth."
+  (interactive)
+  (condition-case nil
+      (scroll-up nil)
+    ((end-of-buffer) (notmuch-search-last-thread))))
+
+(defun notmuch-search-scroll-down ()
+  "Move backward through the search results by one window's worth."
+  (interactive)
+  ; I don't know why scroll-down doesn't signal beginning-of-buffer
+  ; the way that scroll-up signals end-of-buffer, but c'est la vie.
+  ;
+  ; So instead of trapping a signal we instead check whether the
+  ; window begins on the first line of the buffer and if so, move
+  ; directly to that position. (We have to count lines since the
+  ; window-start position is not the same as point-min due to the
+  ; invisible thread-ID characters on the first line.
+  (if (equal (count-lines (point-min) (window-start)) 0)
+      (goto-char (point-min))
+    (scroll-down nil)))
+
+(defun notmuch-search-next-thread ()
+  "Select the next thread in the search results."
+  (interactive)
+  (forward-line 1))
+
+(defun notmuch-search-previous-thread ()
+  "Select the previous thread in the search results."
+  (interactive)
+  (forward-line -1))
+
+(defun notmuch-search-last-thread ()
+  "Select the last thread in the search results."
+  (interactive)
+  (goto-char (point-max))
+  (forward-line -2))
+
+(defun notmuch-search-first-thread ()
+  "Select the first thread in the search results."
+  (interactive)
+  (goto-char (point-min)))
+
+(defface notmuch-message-summary-face
+ '((((class color) (background light)) (:background "#f0f0f0"))
+   (((class color) (background dark)) (:background "#303030")))
+ "Face for the single-line message summary in notmuch-show-mode."
+ :group 'notmuch)
+
+(defface notmuch-tag-face
+  '((((class color)
+      (background dark))
+     (:foreground "OliveDrab1"))
+    (((class color)
+      (background light))
+     (:foreground "navy blue" :bold t))
+    (t
+     (:bold t)))
+  "Notmuch search mode face used to highligh tags."
+  :group 'notmuch)
+
+;;;###autoload
+(defun notmuch-search-mode ()
+  "Major mode displaying results of a notmuch search.
+
+This buffer contains the results of a \"notmuch search\" of your
+email archives. Each line in the buffer represents a single
+thread giving a summary of the thread (a relative date, the
+number of matched messages and total messages in the thread,
+participants in the thread, a representative subject line, and
+any tags).
+
+Pressing \\[notmuch-search-show-thread] on any line displays that thread. The '\\[notmuch-search-add-tag]' and '\\[notmuch-search-remove-tag]'
+keys can be used to add or remove tags from a thread. The '\\[notmuch-search-archive-thread]' key
+is a convenience for archiving a thread (removing the \"inbox\"
+tag). The '\\[notmuch-search-operate-all]' key can be used to add or remove a tag from all
+threads in the current buffer.
+
+Other useful commands are '\\[notmuch-search-filter]' for filtering the current search
+based on an additional query string, '\\[notmuch-search-filter-by-tag]' for filtering to include
+only messages with a given tag, and '\\[notmuch-search]' to execute a new, global
+search.
+
+Complete list of currently available key bindings:
+
+\\{notmuch-search-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (make-local-variable 'notmuch-search-query-string)
+  (make-local-variable 'notmuch-search-oldest-first)
+  (make-local-variable 'notmuch-search-target-thread)
+  (make-local-variable 'notmuch-search-target-line)
+  (set (make-local-variable 'scroll-preserve-screen-position) t)
+  (add-to-invisibility-spec 'notmuch-search)
+  (use-local-map notmuch-search-mode-map)
+  (setq truncate-lines t)
+  (setq major-mode 'notmuch-search-mode
+       mode-name "notmuch-search")
+  (setq buffer-read-only t))
+
+(defun notmuch-search-properties-in-region (property beg end)
+  (save-excursion
+    (let ((output nil)
+         (last-line (line-number-at-pos end))
+         (max-line (- (line-number-at-pos (point-max)) 2)))
+      (goto-char beg)
+      (beginning-of-line)
+      (while (<= (line-number-at-pos) (min last-line max-line))
+       (setq output (cons (get-text-property (point) property) output))
+       (forward-line 1))
+      output)))
+
+(defun notmuch-search-find-thread-id ()
+  "Return the thread for the current thread"
+  (get-text-property (point) 'notmuch-search-thread-id))
+
+(defun notmuch-search-find-thread-id-region (beg end)
+  "Return a list of threads for the current region"
+  (notmuch-search-properties-in-region 'notmuch-search-thread-id beg end))
+
+(defun notmuch-search-find-authors ()
+  "Return the authors for the current thread"
+  (get-text-property (point) 'notmuch-search-authors))
+
+(defun notmuch-search-find-authors-region (beg end)
+  "Return a list of authors for the current region"
+  (notmuch-search-properties-in-region 'notmuch-search-authors beg end))
+
+(defun notmuch-search-find-subject ()
+  "Return the subject for the current thread"
+  (get-text-property (point) 'notmuch-search-subject))
+
+(defun notmuch-search-find-subject-region (beg end)
+  "Return a list of authors for the current region"
+  (notmuch-search-properties-in-region 'notmuch-search-subject beg end))
+
+(defun notmuch-search-show-thread ()
+  "Display the currently selected thread."
+  (interactive)
+  (let ((thread-id (notmuch-search-find-thread-id))
+       (subject (notmuch-search-find-subject)))
+    (if (> (length thread-id) 0)
+       (notmuch-show thread-id
+                     (current-buffer)
+                     notmuch-search-query-string
+                     ;; name the buffer based on notmuch-search-find-subject
+                     (if (string-match "^[ \t]*$" subject)
+                         "[No Subject]"
+                       (truncate-string-to-width
+                        (concat "*"
+                                (truncate-string-to-width subject 32 nil nil t)
+                                "*")
+                        32 nil nil t)))
+      (error "End of search results"))))
+
+(defun notmuch-search-reply-to-thread ()
+  "Begin composing a reply to the entire current thread in a new buffer."
+  (interactive)
+  (let ((message-id (notmuch-search-find-thread-id)))
+    (notmuch-reply message-id)))
+
+(defun notmuch-call-notmuch-process (&rest args)
+  "Synchronously invoke \"notmuch\" with the given list of arguments.
+
+Output from the process will be presented to the user as an error
+and will also appear in a buffer named \"*Notmuch errors*\"."
+  (let ((error-buffer (get-buffer-create "*Notmuch errors*")))
+    (with-current-buffer error-buffer
+       (erase-buffer))
+    (if (eq (apply 'call-process notmuch-command nil error-buffer nil args) 0)
+       (point)
+      (progn
+       (with-current-buffer error-buffer
+         (let ((beg (point-min))
+               (end (- (point-max) 1)))
+           (error (buffer-substring beg end))
+           ))))))
+
+(defun notmuch-search-set-tags (tags)
+  (save-excursion
+    (end-of-line)
+    (re-search-backward "(")
+    (forward-char)
+    (let ((beg (point))
+         (inhibit-read-only t))
+      (re-search-forward ")")
+      (backward-char)
+      (let ((end (point)))
+       (delete-region beg end)
+       (insert (propertize (mapconcat  'identity tags " ")
+                           'font-lock-face 'notmuch-tag-face))))))
+
+(defun notmuch-search-get-tags ()
+  (save-excursion
+    (end-of-line)
+    (re-search-backward "(")
+    (let ((beg (+ (point) 1)))
+      (re-search-forward ")")
+      (let ((end (- (point) 1)))
+       (split-string (buffer-substring beg end))))))
+
+(defun notmuch-search-get-tags-region (beg end)
+  (save-excursion
+    (let ((output nil)
+         (last-line (line-number-at-pos end))
+         (max-line (- (line-number-at-pos (point-max)) 2)))
+      (goto-char beg)
+      (while (<= (line-number-at-pos) (min last-line max-line))
+       (setq output (append output (notmuch-search-get-tags)))
+       (forward-line 1))
+      output)))
+
+(defun notmuch-search-add-tag-thread (tag)
+  (notmuch-search-add-tag-region tag (point) (point)))
+
+(defun notmuch-search-add-tag-region (tag beg end)
+  (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
+    (notmuch-call-notmuch-process "tag" (concat "+" tag) search-id-string)
+    (save-excursion
+      (let ((last-line (line-number-at-pos end))
+           (max-line (- (line-number-at-pos (point-max)) 2)))
+       (goto-char beg)
+       (while (<= (line-number-at-pos) (min last-line max-line))
+         (notmuch-search-set-tags (delete-dups (sort (cons tag (notmuch-search-get-tags)) 'string<)))
+         (forward-line))))))
+
+(defun notmuch-search-remove-tag-thread (tag)
+  (notmuch-search-remove-tag-region tag (point) (point)))
+
+(defun notmuch-search-remove-tag-region (tag beg end)
+  (let ((search-id-string (mapconcat 'identity (notmuch-search-find-thread-id-region beg end) " or ")))
+    (notmuch-call-notmuch-process "tag" (concat "-" tag) search-id-string)
+    (save-excursion
+      (let ((last-line (line-number-at-pos end))
+           (max-line (- (line-number-at-pos (point-max)) 2)))
+       (goto-char beg)
+       (while (<= (line-number-at-pos) (min last-line max-line))
+         (notmuch-search-set-tags (delete tag (notmuch-search-get-tags)))
+         (forward-line))))))
+
+(defun notmuch-search-add-tag (tag)
+  "Add a tag to the currently selected thread or region.
+
+The tag is added to all messages in the currently selected thread
+or threads in the current region."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Tag to add: ")))
+  (save-excursion
+    (if (region-active-p)
+       (let* ((beg (region-beginning))
+              (end (region-end)))
+         (notmuch-search-add-tag-region tag beg end))
+      (notmuch-search-add-tag-thread tag))))
+
+(defun notmuch-search-remove-tag (tag)
+  "Remove a tag from the currently selected thread or region.
+
+The tag is removed from all messages in the currently selected
+thread or threads in the current region."
+  (interactive
+   (list (notmuch-select-tag-with-completion
+         "Tag to remove: "
+         (if (region-active-p)
+             (mapconcat 'identity
+                        (notmuch-search-find-thread-id-region (region-beginning) (region-end))
+                        " ")
+           (notmuch-search-find-thread-id)))))
+  (save-excursion
+    (if (region-active-p)
+       (let* ((beg (region-beginning))
+              (end (region-end)))
+         (notmuch-search-remove-tag-region tag beg end))
+      (notmuch-search-remove-tag-thread tag))))
+
+(defun notmuch-search-archive-thread ()
+  "Archive the currently selected thread (remove its \"inbox\" tag).
+
+This function advances the next thread when finished."
+  (interactive)
+  (notmuch-search-remove-tag-thread "inbox")
+  (forward-line))
+
+(defun notmuch-search-process-sentinel (proc msg)
+  "Add a message to let user know when \"notmuch search\" exits"
+  (let ((buffer (process-buffer proc))
+       (status (process-status proc))
+       (exit-status (process-exit-status proc))
+       (never-found-target-thread nil))
+    (if (memq status '(exit signal))
+       (if (buffer-live-p buffer)
+           (with-current-buffer buffer
+             (save-excursion
+               (let ((inhibit-read-only t)
+                     (atbob (bobp)))
+                 (goto-char (point-max))
+                 (if (eq status 'signal)
+                     (insert "Incomplete search results (search process was killed).\n"))
+                 (if (eq status 'exit)
+                     (progn
+                       (insert "End of search results.")
+                       (if (not (= exit-status 0))
+                           (insert (format " (process returned %d)" exit-status)))
+                       (insert "\n")
+                       (if (and atbob
+                                (not (string= notmuch-search-target-thread "found")))
+                           (set 'never-found-target-thread t))))))
+             (if (and never-found-target-thread
+                      notmuch-search-target-line)
+                 (goto-line notmuch-search-target-line)))))))
+
+(defcustom notmuch-search-line-faces nil
+  "Tag/face mapping for line highlighting in notmuch-search.
+
+Here is an example of how to color search results based on tags.
+(the following text would be placed in your ~/.emacs file):
+
+(setq notmuch-search-line-faces '((\"delete\" . '(:foreground \"red\"))
+                                 (\"unread\" . '(:foreground \"green\"))))
+
+Order matters: for lines with multiple tags, the the first
+matching will be applied."
+  :type '(alist :key-type (string) :value-type (list))
+  :group 'notmuch)
+
+(defun notmuch-search-color-line (start end line-tag-list)
+  "Colorize lines in notmuch-show based on tags"
+  (if notmuch-search-line-faces
+      (let ((overlay (make-overlay start end))
+           (tags-faces (copy-alist notmuch-search-line-faces)))
+       (while tags-faces
+         (let* ((tag-face (car tags-faces))
+                (tag (car tag-face))
+                (face (cdr tag-face)))
+           (cond ((member tag line-tag-list)
+                  (overlay-put overlay 'face face)
+                  (setq tags-faces nil))
+                 (t
+                  (setq tags-faces (cdr tags-faces)))))))))
+
+(defun notmuch-search-insert-field (field date count authors subject tags)
+  (cond
+   ((string-equal field "date")
+    (insert (format (cdr (assoc field notmuch-search-result-format)) date)))
+   ((string-equal field "count")
+    (insert (format (cdr (assoc field notmuch-search-result-format)) count)))
+   ((string-equal field "authors")
+    (insert (format (cdr (assoc field notmuch-search-result-format)) authors)))
+   ((string-equal field "subject")
+    (insert (format (cdr (assoc field notmuch-search-result-format)) subject)))
+   ((string-equal field "tags")
+    (insert (concat "(" (propertize tags 'font-lock-face 'notmuch-tag-face) ")")))))
+
+(defun notmuch-search-show-result (date count authors subject tags)
+  (let ((fields) (field))
+    (setq fields (mapcar 'car notmuch-search-result-format))
+    (loop for field in fields
+         do (notmuch-search-insert-field field date count authors subject tags)))
+  (insert "\n"))
+
+(defun notmuch-search-process-filter (proc string)
+  "Process and filter the output of \"notmuch search\""
+  (let ((buffer (process-buffer proc))
+       (found-target nil))
+    (if (buffer-live-p buffer)
+       (with-current-buffer buffer
+         (save-excursion
+           (let ((line 0)
+                 (more t)
+                 (inhibit-read-only t))
+             (while more
+               (if (string-match "^\\(thread:[0-9A-Fa-f]*\\) \\(.*\\) \\(\\[[0-9/]*\\]\\) \\([^;]*\\); \\(.*\\) (\\([^()]*\\))$" string line)
+                   (let* ((thread-id (match-string 1 string))
+                          (date (match-string 2 string))
+                          (count (match-string 3 string))
+                          (authors (match-string 4 string))
+                          (authors-length (length authors))
+                          (subject (match-string 5 string))
+                          (tags (match-string 6 string))
+                          (tag-list (if tags (save-match-data (split-string tags)))))
+                     (if (> authors-length notmuch-search-authors-width)
+                         (set 'authors (concat (substring authors 0 (- notmuch-search-authors-width 3)) "...")))
+                     (goto-char (point-max))
+                     (let ((beg (point-marker)))
+                       (notmuch-search-show-result date count authors subject tags)
+                       (notmuch-search-color-line beg (point-marker) tag-list)
+                       (put-text-property beg (point-marker) 'notmuch-search-thread-id thread-id)
+                       (put-text-property beg (point-marker) 'notmuch-search-authors authors)
+                       (put-text-property beg (point-marker) 'notmuch-search-subject subject)
+                       (if (string= thread-id notmuch-search-target-thread)
+                           (progn
+                             (set 'found-target beg)
+                             (set 'notmuch-search-target-thread "found"))))
+                     (set 'line (match-end 0)))
+                 (set 'more nil)))))
+         (if found-target
+             (goto-char found-target)))
+      (delete-process proc))))
+
+(defun notmuch-search-operate-all (action)
+  "Add/remove tags from all matching messages.
+
+Tis command adds or removes tags from all messages matching the
+current search terms. When called interactively, this command
+will prompt for tags to be added or removed. Tags prefixed with
+'+' will be added and tags prefixed with '-' will be removed.
+
+Each character of the tag name may consist of alphanumeric
+characters as well as `_.+-'.
+"
+  (interactive "sOperation (+add -drop): notmuch tag ")
+  (let ((action-split (split-string action " +")))
+    ;; Perform some validation
+    (let ((words action-split))
+      (when (null words) (error "No operation given"))
+      (while words
+       (unless (string-match-p "^[-+][-+_.[:word:]]+$" (car words))
+         (error "Action must be of the form `+thistag -that_tag'"))
+       (setq words (cdr words))))
+    (apply 'notmuch-call-notmuch-process "tag"
+          (append action-split (list notmuch-search-query-string) nil))))
+
+(defcustom notmuch-folders (quote (("inbox" . "tag:inbox") ("unread" . "tag:unread")))
+  "List of searches for the notmuch folder view"
+  :type '(alist :key-type (string) :value-type (string))
+  :group 'notmuch)
+
+(defun notmuch-search-buffer-title (query)
+  "Returns the title for a buffer with notmuch search results."
+  (let* ((folder (rassoc-if (lambda (key)
+                             (string-match (concat "^" (regexp-quote key))
+                                           query))
+                           notmuch-folders))
+        (folder-name (car folder))
+        (folder-query (cdr folder)))
+    (cond ((and folder (equal folder-query query))
+          ;; Query is the same as folder search (ignoring case)
+          (concat "*notmuch-folder-" folder-name "*"))
+         (folder
+          (concat "*notmuch-search-"
+                  (replace-regexp-in-string (concat "^" (regexp-quote folder-query))
+                                            (concat "[ " folder-name " ]")
+                                            query)
+                  "*"))
+         (t
+          (concat "*notmuch-search-" query "*"))
+         )))
+
+;;;###autoload
+(defun notmuch-search (query &optional oldest-first target-thread target-line)
+  "Run \"notmuch search\" with the given query string and display results.
+
+The optional parameters are used as follows:
+
+  oldest-first: A Boolean controlling the sort order of returned threads
+  target-thread: A thread ID (with the thread: prefix) that will be made
+                 current if it appears in the search results.
+  target-line: The line number to move to if the target thread does not
+               appear in the search results."
+  (interactive "sNotmuch search: ")
+  (let ((buffer (get-buffer-create (notmuch-search-buffer-title query))))
+    (switch-to-buffer buffer)
+    (notmuch-search-mode)
+    (set 'notmuch-search-query-string query)
+    (set 'notmuch-search-oldest-first oldest-first)
+    (set 'notmuch-search-target-thread target-thread)
+    (set 'notmuch-search-target-line target-line)
+    (let ((proc (get-buffer-process (current-buffer)))
+         (inhibit-read-only t))
+      (if proc
+         (error "notmuch search process already running for query `%s'" query)
+       )
+      (erase-buffer)
+      (goto-char (point-min))
+      (save-excursion
+       (let ((proc (start-process-shell-command
+                    "notmuch-search" buffer notmuch-command "search"
+                    (if oldest-first "--sort=oldest-first" "--sort=newest-first")
+                    (shell-quote-argument query))))
+         (set-process-sentinel proc 'notmuch-search-process-sentinel)
+         (set-process-filter proc 'notmuch-search-process-filter))))
+    (run-hooks 'notmuch-search-hook)))
+
+(defun notmuch-search-refresh-view ()
+  "Refresh the current view.
+
+Kills the current buffer and runs a new search with the same
+query string as the current search. If the current thread is in
+the new search results, then point will be placed on the same
+thread. Otherwise, point will be moved to attempt to be in the
+same relative position within the new buffer."
+  (interactive)
+  (let ((target-line (line-number-at-pos))
+       (oldest-first notmuch-search-oldest-first)
+       (target-thread (notmuch-search-find-thread-id))
+       (query notmuch-search-query-string))
+    (kill-this-buffer)
+    (notmuch-search query oldest-first target-thread target-line)
+    (goto-char (point-min))
+    ))
+
+(defun notmuch-search-toggle-order ()
+  "Toggle the current search order.
+
+By default, the \"inbox\" view created by `notmuch' is displayed
+in chronological order (oldest thread at the beginning of the
+buffer), while any global searches created by `notmuch-search'
+are displayed in reverse-chronological order (newest thread at
+the beginning of the buffer).
+
+This command toggles the sort order for the current search.
+
+Note that any filtered searches created by
+`notmuch-search-filter' retain the search order of the parent
+search."
+  (interactive)
+  (set 'notmuch-search-oldest-first (not notmuch-search-oldest-first))
+  (notmuch-search-refresh-view))
+
+(defun notmuch-search-filter (query)
+  "Filter the current search results based on an additional query string.
+
+Runs a new search matching only messages that match both the
+current search results AND the additional query string provided."
+  (interactive "sFilter search: ")
+  (let ((grouped-query (if (string-match-p notmuch-search-disjunctive-regexp query)
+                          (concat "( " query " )")
+                        query)))
+    (notmuch-search (if (string= notmuch-search-query-string "*")
+                       grouped-query
+                     (concat notmuch-search-query-string " and " grouped-query)) notmuch-search-oldest-first)))
+
+(defun notmuch-search-filter-by-tag (tag)
+  "Filter the current search results based on a single tag.
+
+Runs a new search matching only messages that match both the
+current search results AND that are tagged with the given tag."
+  (interactive
+   (list (notmuch-select-tag-with-completion "Filter by tag: ")))
+  (notmuch-search (concat notmuch-search-query-string " and tag:" tag) notmuch-search-oldest-first))
+
+;;;###autoload
+(defun notmuch ()
+  "Run notmuch to display all mail with tag of 'inbox'"
+  (interactive)
+  (notmuch-search "tag:inbox" notmuch-search-oldest-first))
+
+(setq mail-user-agent 'message-user-agent)
+
+(defvar notmuch-folder-mode-map
+  (let ((map (make-sparse-keymap)))
+    (define-key map "?" 'notmuch-help)
+    (define-key map "x" 'kill-this-buffer)
+    (define-key map "q" 'kill-this-buffer)
+    (define-key map "m" 'message-mail)
+    (define-key map "e" 'notmuch-folder-show-empty-toggle)
+    (define-key map ">" 'notmuch-folder-last)
+    (define-key map "<" 'notmuch-folder-first)
+    (define-key map "=" 'notmuch-folder)
+    (define-key map "s" 'notmuch-search)
+    (define-key map [mouse-1] 'notmuch-folder-show-search)
+    (define-key map (kbd "RET") 'notmuch-folder-show-search)
+    (define-key map " " 'notmuch-folder-show-search)
+    (define-key map "p" 'notmuch-folder-previous)
+    (define-key map "n" 'notmuch-folder-next)
+    map)
+  "Keymap for \"notmuch folder\" buffers.")
+
+(fset 'notmuch-folder-mode-map notmuch-folder-mode-map)
+
+(defun notmuch-folder-mode ()
+  "Major mode for showing notmuch 'folders'.
+
+This buffer contains a list of message counts returned by a
+customizable set of searches of your email archives. Each line in
+the buffer shows the name of a saved search and the resulting
+message count.
+
+Pressing RET on any line opens a search window containing the
+results for the saved search on that line.
+
+Here is an example of how the search list could be
+customized, (the following text would be placed in your ~/.emacs
+file):
+
+(setq notmuch-folders '((\"inbox\" . \"tag:inbox\")
+                        (\"unread\" . \"tag:inbox AND tag:unread\")
+                        (\"notmuch\" . \"tag:inbox AND to:notmuchmail.org\")))
+
+Of course, you can have any number of folders, each configured
+with any supported search terms (see \"notmuch help search-terms\").
+
+Currently available key bindings:
+
+\\{notmuch-folder-mode-map}"
+  (interactive)
+  (kill-all-local-variables)
+  (use-local-map 'notmuch-folder-mode-map)
+  (setq truncate-lines t)
+  (hl-line-mode 1)
+  (setq major-mode 'notmuch-folder-mode
+       mode-name "notmuch-folder")
+  (setq buffer-read-only t))
+
+(defun notmuch-folder-next ()
+  "Select the next folder in the list."
+  (interactive)
+  (forward-line 1)
+  (if (eobp)
+      (forward-line -1)))
+
+(defun notmuch-folder-previous ()
+  "Select the previous folder in the list."
+  (interactive)
+  (forward-line -1))
+
+(defun notmuch-folder-first ()
+  "Select the first folder in the list."
+  (interactive)
+  (goto-char (point-min)))
+
+(defun notmuch-folder-last ()
+  "Select the last folder in the list."
+  (interactive)
+  (goto-char (point-max))
+  (forward-line -1))
+
+(defun notmuch-folder-count (search)
+  (car (process-lines notmuch-command "count" search)))
+
+(defvar notmuch-folder-show-empty t
+  "Whether `notmuch-folder-mode' should display empty folders.")
+
+(defun notmuch-folder-show-empty-toggle ()
+  "Toggle the listing of empty folders"
+  (interactive)
+  (setq notmuch-folder-show-empty (not notmuch-folder-show-empty))
+  (notmuch-folder))
+
+(defun notmuch-folder-add (folders)
+  (if folders
+      (let* ((name (car (car folders)))
+           (inhibit-read-only t)
+           (search (cdr (car folders)))
+           (count (notmuch-folder-count search)))
+       (if (or notmuch-folder-show-empty
+               (not (equal count "0")))
+           (progn
+             (insert name)
+             (indent-to 16 1)
+             (insert count)
+             (insert "\n")
+             )
+         )
+       (notmuch-folder-add (cdr folders)))))
+
+(defun notmuch-folder-find-name ()
+  (save-excursion
+    (beginning-of-line)
+    (let ((beg (point)))
+      (re-search-forward "\\([ \t]*[^ \t]+\\)")
+      (filter-buffer-substring (match-beginning 1) (match-end 1)))))
+
+(defun notmuch-folder-show-search (&optional folder)
+  "Show a search window for the search related to the specified folder."
+  (interactive)
+  (if (null folder)
+      (setq folder (notmuch-folder-find-name)))
+  (let ((search (assoc folder notmuch-folders)))
+    (if search
+       (notmuch-search (cdr search) notmuch-search-oldest-first))))
+
+;;;###autoload
+(defun notmuch-folder ()
+  "Show the notmuch folder view and update the displayed counts."
+  (interactive)
+  (let ((buffer (get-buffer-create "*notmuch-folders*")))
+    (switch-to-buffer buffer)
+    (let ((inhibit-read-only t)
+         (n (line-number-at-pos)))
+      (erase-buffer)
+      (notmuch-folder-mode)
+      (notmuch-folder-add notmuch-folders)
+      (goto-char (point-min))
+      (goto-line n))))
+
+(provide 'notmuch)
diff --git a/gmime-filter-headers.c b/gmime-filter-headers.c
new file mode 100644 (file)
index 0000000..2f3df80
--- /dev/null
@@ -0,0 +1,263 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ * Copyright © 2010 Michal Sojka <sojkam1@fel.cvut.cz>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#include "gmime-filter-headers.h"
+#include <string.h>
+#include <gmime/gmime-utils.h>
+#include <glib/gprintf.h>
+#include <stdlib.h>
+#include <xutil.h>
+
+/**
+ * SECTION: gmime-filter-headers
+ * @title: GMimeFilterHeaders
+ * @short_description: Add/remove headers markers
+ *
+ * A #GMimeFilter for decoding rfc2047 encoded headers to UTF-8
+ **/
+
+
+static void g_mime_filter_headers_class_init (GMimeFilterHeadersClass *klass);
+static void g_mime_filter_headers_init (GMimeFilterHeaders *filter, GMimeFilterHeadersClass *klass);
+static void g_mime_filter_headers_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+static void filter_reset (GMimeFilter *filter);
+
+
+static GMimeFilterClass *parent_class = NULL;
+
+GType
+g_mime_filter_headers_get_type (void)
+{
+       static GType type = 0;
+
+       if (!type) {
+               static const GTypeInfo info = {
+                       sizeof (GMimeFilterHeadersClass),
+                       NULL, /* base_class_init */
+                       NULL, /* base_class_finalize */
+                       (GClassInitFunc) g_mime_filter_headers_class_init,
+                       NULL, /* class_finalize */
+                       NULL, /* class_data */
+                       sizeof (GMimeFilterHeaders),
+                       0,    /* n_preallocs */
+                       (GInstanceInitFunc) g_mime_filter_headers_init,
+                       NULL    /* value_table */
+               };
+
+               type = g_type_register_static (GMIME_TYPE_FILTER, "GMimeFilterHeaders", &info, (GTypeFlags) 0);
+       }
+
+       return type;
+}
+
+
+static void
+g_mime_filter_headers_class_init (GMimeFilterHeadersClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+       parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+
+       object_class->finalize = g_mime_filter_headers_finalize;
+
+       filter_class->copy = filter_copy;
+       filter_class->filter = filter_filter;
+       filter_class->complete = filter_complete;
+       filter_class->reset = filter_reset;
+}
+
+static void
+g_mime_filter_headers_init (GMimeFilterHeaders *filter, GMimeFilterHeadersClass *klass)
+{
+       (void) klass;
+       filter->saw_nl = TRUE;
+       filter->line = NULL;
+       filter->line_size = 0;
+       filter->lineptr = NULL;
+}
+
+static void
+g_mime_filter_headers_finalize (GObject *object)
+{
+       free (GMIME_FILTER_HEADERS (object)->line);
+       G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+
+static GMimeFilter *
+filter_copy (GMimeFilter *filter)
+{
+       (void) filter;
+       return g_mime_filter_headers_new ();
+}
+
+static void
+output_decoded_header (GMimeFilterHeaders *headers, char **outptr)
+{
+       char *colon, *name, *s, *decoded_value;
+       size_t offset;
+       gint ret;
+
+       colon = strchr (headers->line, ':');
+       if (colon == NULL)
+               return;
+
+       name = headers->line;
+       *colon = '\0';
+       s = colon + 1;
+       while (*s == ' ' || *s == '\t')
+               s++;
+       decoded_value = g_mime_utils_header_decode_text(s);
+       if (decoded_value == NULL)
+               return;
+       offset = *outptr - GMIME_FILTER (headers)->outbuf;
+       g_mime_filter_set_size (GMIME_FILTER (headers), strlen(name) + 2 +
+                              strlen(decoded_value) + 2, TRUE);
+       *outptr = GMIME_FILTER (headers)->outbuf + offset;
+       ret = g_sprintf (*outptr, "%s: %s\n", name, decoded_value);
+       if (ret > 0)
+               *outptr += ret;
+       free (decoded_value);
+}
+
+static void
+output_final_newline (GMimeFilterHeaders *headers, char **outptr)
+{
+       size_t offset;
+
+       offset = *outptr - GMIME_FILTER (headers)->outbuf;
+       g_mime_filter_set_size (GMIME_FILTER (headers), 1, TRUE);
+       *outptr = GMIME_FILTER (headers)->outbuf + offset;
+       *(*outptr)++ = '\n';
+}
+
+static void
+filter_filter (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+       GMimeFilterHeaders *headers = (GMimeFilterHeaders *) filter;
+       register const char *inptr = inbuf;
+       const char *inend = inbuf + inlen;
+       char *lineptr, *lineend, *outptr;
+
+       (void) prespace;
+       if (headers->line == NULL) {
+               headers->line_size = 200;
+               headers->lineptr = headers->line = malloc (headers->line_size);
+       }
+       lineptr = headers->lineptr;
+       lineend = headers->line + headers->line_size;
+       if (lineptr == NULL)
+               return;
+       outptr = filter->outbuf;
+       while (inptr < inend) {
+               if (*inptr == '\n') {
+                       if (headers->saw_nl)
+                               output_final_newline(headers, &outptr);
+                       headers->saw_nl = TRUE;
+                       inptr++;
+                       continue;
+               }
+
+               if (lineptr == lineend) {
+                       headers->line_size *= 2;
+                       headers->line = xrealloc (headers->line, headers->line_size);
+                       lineptr = headers->line + headers->line_size / 2;
+                       lineend = headers->line + headers->line_size;
+               }
+
+               if (headers->saw_nl && *inptr != ' ' && *inptr != '\t') {
+                       *lineptr = '\0';
+                       output_decoded_header (headers, &outptr);
+                       lineptr = headers->line;
+               }
+               if (headers->saw_nl && (*inptr == ' ' || *inptr == '\t')) {
+                       *lineptr = ' ';
+                       lineptr++;
+                       while (inptr < inend && (*inptr == ' ' || *inptr == '\t'))
+                               inptr++;
+                       headers->saw_nl = FALSE;
+                       continue;
+               }
+               headers->saw_nl = FALSE;
+
+               if (*inptr != '\r')
+                       *lineptr++ = *inptr;
+               inptr++;
+       }
+       if (headers->saw_nl) {
+               *lineptr = '\0';
+               output_decoded_header (headers, &outptr);
+               lineptr = headers->line;
+       }
+       headers->lineptr = lineptr;
+       *outlen = outptr - filter->outbuf;
+       *outprespace = filter->outpre;
+       *outbuf = filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+       if (inbuf && inlen)
+               filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *filter)
+{
+       GMimeFilterHeaders *headers = (GMimeFilterHeaders *) filter;
+
+       headers->saw_nl = TRUE;
+       free(headers->line);
+       headers->line = NULL;
+       headers->line_size = 0;
+}
+
+
+/**
+ * g_mime_filter_headers_new:
+ * @encode: %TRUE if the filter should encode or %FALSE otherwise
+ * @dots: encode/decode dots (as for SMTP)
+ *
+ * Creates a new #GMimeFilterHeaders filter.
+ *
+ * If @encode is %TRUE, then all lines will be prefixed by "> ",
+ * otherwise any lines starting with "> " will have that removed
+ *
+ * Returns: a new #GMimeFilterHeaders filter.
+ **/
+GMimeFilter *
+g_mime_filter_headers_new (void)
+{
+       GMimeFilterHeaders *new_headers;
+
+       new_headers = (GMimeFilterHeaders *) g_object_newv (GMIME_TYPE_FILTER_HEADERS, 0, NULL);
+
+       return (GMimeFilter *) new_headers;
+}
+
diff --git a/gmime-filter-headers.h b/gmime-filter-headers.h
new file mode 100644 (file)
index 0000000..47d1d45
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ * Copyright © 2010 Michal Sojka <sojkam1@fel.cvut.cz>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#ifndef _GMIME_FILTER_HEADERS_H_
+#define _GMIME_FILTER_HEADERS_H_
+
+#include <gmime/gmime-filter.h>
+
+G_BEGIN_DECLS
+
+#define GMIME_TYPE_FILTER_HEADERS            (g_mime_filter_headers_get_type ())
+#define GMIME_FILTER_HEADERS(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeaders))
+#define GMIME_FILTER_HEADERS_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeadersClass))
+#define GMIME_IS_FILTER_HEADERS(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GMIME_TYPE_FILTER_HEADERS))
+#define GMIME_IS_FILTER_HEADERS_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GMIME_TYPE_FILTER_HEADERS))
+#define GMIME_FILTER_HEADERS_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_HEADERS, GMimeFilterHeadersClass))
+
+typedef struct _GMimeFilterHeaders GMimeFilterHeaders;
+typedef struct _GMimeFilterHeadersClass GMimeFilterHeadersClass;
+
+/**
+ * GMimeFilterHeaders:
+ * @parent_object: parent #GMimeFilter
+ * @saw_nl: previous char was a \n
+ * @line: temporary buffer for line unfolding
+ * @line_size: size of currently allocated nemory for @line
+ * @lineptr: pointer to the first unused character in @line
+ *
+ * A filter to decode rfc2047 encoded headers
+ **/
+struct _GMimeFilterHeaders {
+       GMimeFilter parent_object;
+
+       gboolean saw_nl;
+       char *line;
+       size_t line_size;
+       char *lineptr;
+};
+
+struct _GMimeFilterHeadersClass {
+       GMimeFilterClass parent_class;
+
+};
+
+
+GType g_mime_filter_headers_get_type (void);
+
+GMimeFilter *g_mime_filter_headers_new (void);
+
+G_END_DECLS
+
+
+#endif /* _GMIME_FILTER_HEADERS_H_ */
diff --git a/gmime-filter-reply.c b/gmime-filter-reply.c
new file mode 100644 (file)
index 0000000..b269db4
--- /dev/null
@@ -0,0 +1,209 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#include "gmime-filter-reply.h"
+
+/**
+ * SECTION: gmime-filter-reply
+ * @title: GMimeFilterReply
+ * @short_description: Add/remove reply markers
+ *
+ * A #GMimeFilter for adding or removing reply markers
+ **/
+
+
+static void g_mime_filter_reply_class_init (GMimeFilterReplyClass *klass);
+static void g_mime_filter_reply_init (GMimeFilterReply *filter, GMimeFilterReplyClass *klass);
+static void g_mime_filter_reply_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+static void filter_reset (GMimeFilter *filter);
+
+
+static GMimeFilterClass *parent_class = NULL;
+
+GType
+g_mime_filter_reply_get_type (void)
+{
+       static GType type = 0;
+
+       if (!type) {
+               static const GTypeInfo info = {
+                       sizeof (GMimeFilterReplyClass),
+                       NULL, /* base_class_init */
+                       NULL, /* base_class_finalize */
+                       (GClassInitFunc) g_mime_filter_reply_class_init,
+                       NULL, /* class_finalize */
+                       NULL, /* class_data */
+                       sizeof (GMimeFilterReply),
+                       0,    /* n_preallocs */
+                       (GInstanceInitFunc) g_mime_filter_reply_init,
+                       NULL    /* value_table */
+               };
+
+               type = g_type_register_static (GMIME_TYPE_FILTER, "GMimeFilterReply", &info, (GTypeFlags) 0);
+       }
+
+       return type;
+}
+
+
+static void
+g_mime_filter_reply_class_init (GMimeFilterReplyClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+       parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+
+       object_class->finalize = g_mime_filter_reply_finalize;
+
+       filter_class->copy = filter_copy;
+       filter_class->filter = filter_filter;
+       filter_class->complete = filter_complete;
+       filter_class->reset = filter_reset;
+}
+
+static void
+g_mime_filter_reply_init (GMimeFilterReply *filter, GMimeFilterReplyClass *klass)
+{
+       (void) klass;
+       filter->saw_nl = TRUE;
+       filter->saw_angle = FALSE;
+}
+
+static void
+g_mime_filter_reply_finalize (GObject *object)
+{
+       G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+
+static GMimeFilter *
+filter_copy (GMimeFilter *filter)
+{
+       GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+
+       return g_mime_filter_reply_new (reply->encode);
+}
+
+static void
+filter_filter (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+       GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+       register const char *inptr = inbuf;
+       const char *inend = inbuf + inlen;
+       char *outptr;
+
+       (void) prespace;
+       if (reply->encode) {
+               g_mime_filter_set_size (filter, 3 * inlen, FALSE);
+
+               outptr = filter->outbuf;
+               while (inptr < inend) {
+                       if (reply->saw_nl) {
+                               *outptr++ = '>';
+                               *outptr++ = ' ';
+                               reply->saw_nl = FALSE;
+                       }
+                       if (*inptr == '\n')
+                               reply->saw_nl = TRUE;
+                       else
+                               reply->saw_nl = FALSE;
+                       if (*inptr != '\r')
+                               *outptr++ = *inptr;
+                       inptr++;
+               }
+       } else {
+               g_mime_filter_set_size (filter, inlen + 1, FALSE);
+
+               outptr = filter->outbuf;
+               while (inptr < inend) {
+                       if (reply->saw_nl) {
+                               if (*inptr == '>')
+                                       reply->saw_angle = TRUE;
+                               else
+                                       *outptr++ = *inptr;
+                               reply->saw_nl = FALSE;
+                       } else if (reply->saw_angle) {
+                               if (*inptr == ' ')
+                                       ;
+                               else
+                                       *outptr++ = *inptr;
+                               reply->saw_angle = FALSE;
+                       } else if (*inptr != '\r') {
+                               if (*inptr == '\n')
+                                       reply->saw_nl = TRUE;
+                               *outptr++ = *inptr;
+                       }
+
+                       inptr++;
+               }
+       }
+
+       *outlen = outptr - filter->outbuf;
+       *outprespace = filter->outpre;
+       *outbuf = filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+       if (inbuf && inlen)
+               filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *filter)
+{
+       GMimeFilterReply *reply = (GMimeFilterReply *) filter;
+
+       reply->saw_nl = TRUE;
+       reply->saw_angle = FALSE;
+}
+
+
+/**
+ * g_mime_filter_reply_new:
+ * @encode: %TRUE if the filter should encode or %FALSE otherwise
+ * @dots: encode/decode dots (as for SMTP)
+ *
+ * Creates a new #GMimeFilterReply filter.
+ *
+ * If @encode is %TRUE, then all lines will be prefixed by "> ",
+ * otherwise any lines starting with "> " will have that removed
+ *
+ * Returns: a new #GMimeFilterReply filter.
+ **/
+GMimeFilter *
+g_mime_filter_reply_new (gboolean encode)
+{
+       GMimeFilterReply *new_reply;
+
+       new_reply = (GMimeFilterReply *) g_object_newv (GMIME_TYPE_FILTER_REPLY, 0, NULL);
+       new_reply->encode = encode;
+
+       return (GMimeFilter *) new_reply;
+}
+
diff --git a/gmime-filter-reply.h b/gmime-filter-reply.h
new file mode 100644 (file)
index 0000000..b7cbc6b
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2009 Keith Packard <keithp@keithp.com>
+ *
+ * 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, write to the Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
+ */
+
+#ifndef _GMIME_FILTER_REPLY_H_
+#define _GMIME_FILTER_REPLY_H_
+
+#include <gmime/gmime-filter.h>
+
+G_BEGIN_DECLS
+
+#define GMIME_TYPE_FILTER_REPLY            (g_mime_filter_reply_get_type ())
+#define GMIME_FILTER_REPLY(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReply))
+#define GMIME_FILTER_REPLY_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass))
+#define GMIME_IS_FILTER_REPLY(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GMIME_TYPE_FILTER_REPLY))
+#define GMIME_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GMIME_TYPE_FILTER_REPLY))
+#define GMIME_FILTER_REPLY_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass))
+
+typedef struct _GMimeFilterReply GMimeFilterReply;
+typedef struct _GMimeFilterReplyClass GMimeFilterReplyClass;
+
+/**
+ * GMimeFilterReply:
+ * @parent_object: parent #GMimeFilter
+ * @encode: encoding vs decoding reply markers
+ * @saw_nl: previous char was a \n
+ * @saw_angle: previous char was a >
+ *
+ * A filter to insert/remove reply markers (lines beginning with >)
+ **/
+struct _GMimeFilterReply {
+       GMimeFilter parent_object;
+
+       gboolean encode;
+       gboolean saw_nl;
+       gboolean saw_angle;
+};
+
+struct _GMimeFilterReplyClass {
+       GMimeFilterClass parent_class;
+
+};
+
+
+GType g_mime_filter_reply_get_type (void);
+
+GMimeFilter *g_mime_filter_reply_new (gboolean encode);
+
+G_END_DECLS
+
+
+#endif /* _GMIME_FILTER_REPLY_H_ */
diff --git a/json.c b/json.c
new file mode 100644 (file)
index 0000000..817fc83
--- /dev/null
+++ b/json.c
@@ -0,0 +1,109 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Dave Gamble
+ * Copyright © 2009 Scott Robinson
+ *
+ * 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/ .
+ *
+ * Authors: Dave Gamble
+ *          Scott Robinson <scott@quadhome.com>
+ *
+ */
+
+#include "notmuch-client.h"
+
+/* This function was derived from the print_string_ptr function of
+ * cJSON (http://cjson.sourceforge.net/) and is used by permission of
+ * the following license:
+ *
+ * Copyright (c) 2009 Dave Gamble
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+char *
+json_quote_chararray(const void *ctx, const char *str, const size_t len)
+{
+    const char *ptr;
+    char *ptr2;
+    char *out;
+    size_t loop;
+    size_t required;
+
+    for (loop = 0, required = 0, ptr = str;
+        loop < len;
+        loop++, required++, ptr++) {
+       if ((unsigned char)(*ptr) < 32 || *ptr == '\"' || *ptr == '\\')
+           required++;
+    }
+
+    /*
+     * + 3 for:
+     * - leading quotation mark,
+     * - trailing quotation mark,
+     * - trailing NULL.
+     */
+    out = talloc_array (ctx, char, required + 3);
+
+    ptr = str;
+    ptr2 = out;
+
+    *ptr2++ = '\"';
+    for (loop = 0; loop < len; loop++) {
+       if ((unsigned char)(*ptr) > 31 && *ptr != '\"' && *ptr != '\\') {
+               *ptr2++ = *ptr++;
+           } else {
+               *ptr2++ = '\\';
+               switch (*ptr++) {
+                   case '\"':  *ptr2++ = '\"'; break;
+                   case '\\':  *ptr2++ = '\\'; break;
+                   case '\b':  *ptr2++ = 'b';  break;
+                   case '\f':  *ptr2++ = 'f';  break;
+                   case '\n':  *ptr2++ = 'n';  break;
+                   case '\r':  *ptr2++ = 'r';  break;
+                   case '\t':  *ptr2++ = 't';  break;
+                   default:     ptr2--;        break;
+               }
+           }
+    }
+    *ptr2++ = '\"';
+    *ptr2++ = '\0';
+
+    return out;
+}
+
+char *
+json_quote_str(const void *ctx, const char *str)
+{
+    if (str == NULL)
+       str = "";
+
+    return (json_quote_chararray (ctx, str, strlen (str)));
+}
diff --git a/lib/Makefile b/lib/Makefile
new file mode 100644 (file)
index 0000000..b6859ea
--- /dev/null
@@ -0,0 +1,7 @@
+# See Makfefile.local for the list of files to be compiled in this
+# directory.
+all:
+       $(MAKE) -C .. all
+
+.DEFAULT:
+       $(MAKE) -C .. $@
diff --git a/lib/Makefile.local b/lib/Makefile.local
new file mode 100644 (file)
index 0000000..a092090
--- /dev/null
@@ -0,0 +1,85 @@
+# -*- makefile -*-
+
+# The major version of the library interface. This will control the soname.
+# As such, this number must be incremented for any incompatible change to
+# the library interface, (such as the deletion of an API or a major
+# semantic change that breaks formerly functioning code).
+#
+# Note: We don't currently have plans to increment this at this time.
+# If we *do* want to make an incompatible change to the library
+# interface, we'll have to decide whether to increment this (creating
+# a new soname) or to introduce symbol versioning to be able to
+# provide support for both old and new interfaces without having to
+# increment this.
+LIBNOTMUCH_VERSION_MAJOR = 1
+
+# The minor version of the library interface. This should be incremented at
+# the time of release for any additions to the library interface.
+LIBNOTMUCH_VERSION_MINOR = 0
+
+# The release version the library interface. This should be incremented at
+# the time of release if there have been no changes to the interface, (but
+# simply compatible changes to the implementation).
+LIBNOTMUCH_VERSION_RELEASE = 0
+
+ifeq ($(MAC_OS_X),1)
+LIBRARY_SUFFIX = dylib
+# On OS X, library version numbers go before suffix.
+LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX)
+SONAME = libnotmuch.$(LIBNOTMUCH_VERSION_MAJOR).$(LIBRARY_SUFFIX)
+LIBNAME = libnotmuch.$(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE).$(LIBRARY_SUFFIX)
+LIBRARY_LINK_FLAG = -dynamiclib -install_name $(SONAME) -compatibility_version $(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR) -current_version $(LIBNOTMUCH_VERSION_MAJOR).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
+else
+LIBRARY_SUFFIX = so
+LINKER_NAME = libnotmuch.$(LIBRARY_SUFFIX)
+SONAME = $(LINKER_NAME).$(LIBNOTMUCH_VERSION_MAJOR)
+LIBNAME = $(SONAME).$(LIBNOTMUCH_VERSION_MINOR).$(LIBNOTMUCH_VERSION_RELEASE)
+LIBRARY_LINK_FLAG = -shared -Wl,-soname=$(SONAME)
+endif
+
+dir := lib
+extra_cflags += -I$(dir) -fPIC
+
+libnotmuch_c_srcs =            \
+       $(notmuch_compat_srcs)  \
+       $(dir)/libsha1.c        \
+       $(dir)/message-file.c   \
+       $(dir)/messages.c       \
+       $(dir)/sha1.c           \
+       $(dir)/tags.c           \
+       $(dir)/xutil.c
+
+libnotmuch_cxx_srcs =          \
+       $(dir)/database.cc      \
+       $(dir)/directory.cc     \
+       $(dir)/index.cc         \
+       $(dir)/message.cc       \
+       $(dir)/query.cc         \
+       $(dir)/thread.cc
+
+libnotmuch_modules = $(libnotmuch_c_srcs:.c=.o) $(libnotmuch_cxx_srcs:.cc=.o)
+
+$(dir)/libnotmuch.a: $(libnotmuch_modules)
+       $(call quiet,AR) rcs $@ $^
+
+$(dir)/$(LIBNAME): $(libnotmuch_modules)
+       $(call quiet,CXX $(CXXFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) $(LIBRARY_LINK_FLAG) -o $@
+
+$(dir)/$(SONAME): $(dir)/$(LIBNAME)
+       ln -sf $(LIBNAME) $@
+
+$(dir)/$(LINKER_NAME): $(dir)/$(SONAME)
+       ln -sf $(LIBNAME) $@
+
+install: install-$(dir)
+
+install-$(dir):
+       mkdir -p $(DESTDIR)$(libdir)/
+       install -m0644 $(dir)/$(LIBNAME) $(DESTDIR)$(libdir)/
+       ln -sf $(LIBNAME) $(DESTDIR)$(libdir)/$(SONAME)
+       ln -sf $(LIBNAME) $(DESTDIR)$(libdir)/$(LINKER_NAME)
+       mkdir -p $(DESTDIR)$(includedir)
+       install -m0644 $(dir)/notmuch.h $(DESTDIR)$(includedir)/
+
+SRCS  := $(SRCS) $(libnotmuch_c_srcs) $(libnotmuch_cxx_srcs)
+CLEAN := $(CLEAN) $(libnotmuch_modules) $(dir)/$(SONAME) $(dir)/$(LINKER_NAME) $(dir)$(LIBNAME) libnotmuch.a
diff --git a/lib/database-private.h b/lib/database-private.h
new file mode 100644 (file)
index 0000000..41918d7
--- /dev/null
@@ -0,0 +1,67 @@
+/* database-private.h - For peeking into the internals of notmuch_database_t
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#ifndef NOTMUCH_DATABASE_PRIVATE_H
+#define NOTMUCH_DATABASE_PRIVATE_H
+
+/* According to WG14/N1124, a C++ implementation won't provide us a
+ * macro like PRIx64 (which gives a printf format string for
+ * formatting a uint64_t as hexadecimal) unless we define
+ * __STDC_FORMAT_MACROS before including inttypes.h. That's annoying,
+ * but there it is.
+ */
+#define __STDC_FORMAT_MACROS
+#include <inttypes.h>
+
+#include "notmuch-private.h"
+
+#include <xapian.h>
+
+struct _notmuch_database {
+    notmuch_bool_t exception_reported;
+
+    char *path;
+
+    notmuch_bool_t needs_upgrade;
+    notmuch_database_mode_t mode;
+    Xapian::Database *xapian_db;
+
+    uint64_t last_thread_id;
+
+    Xapian::QueryParser *query_parser;
+    Xapian::TermGenerator *term_gen;
+    Xapian::ValueRangeProcessor *value_range_processor;
+
+};
+
+/* Convert tags from Xapian internal format to notmuch format.
+ *
+ * The function gets a TermIterator as argument and uses that iterator to find
+ * all tag terms in the object. The tags are then converted to a
+ * notmuch_tags_t list and returned. The function needs to allocate memory for
+ * the resulting list and it uses the argument ctx as talloc context.
+ *
+ * The function returns NULL on failure.
+ */
+notmuch_tags_t *
+_notmuch_convert_tags (void *ctx, Xapian::TermIterator &i,
+                      Xapian::TermIterator &end);
+
+#endif
diff --git a/lib/database.cc b/lib/database.cc
new file mode 100644 (file)
index 0000000..6842faf
--- /dev/null
@@ -0,0 +1,1709 @@
+/* database.cc - The database interfaces of the notmuch mail library
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#include "database-private.h"
+
+#include <iostream>
+
+#include <sys/time.h>
+#include <signal.h>
+#include <xapian.h>
+
+#include <glib.h> /* g_free, GPtrArray, GHashTable */
+
+using namespace std;
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+typedef struct {
+    const char *name;
+    const char *prefix;
+} prefix_t;
+
+#define NOTMUCH_DATABASE_VERSION 1
+
+#define STRINGIFY(s) _SUB_STRINGIFY(s)
+#define _SUB_STRINGIFY(s) #s
+
+/* Here's the current schema for our database (for NOTMUCH_DATABASE_VERSION):
+ *
+ * We currently have two different types of documents (mail and
+ * directory) and also some metadata.
+ *
+ * Mail document
+ * -------------
+ * A mail document is associated with a particular email message file
+ * on disk. It is indexed with the following prefixed terms which the
+ * database uses to construct threads, etc.:
+ *
+ *    Single terms of given prefix:
+ *
+ *     type:   mail
+ *
+ *     id:     Unique ID of mail, (from Message-ID header or generated
+ *             as "notmuch-sha1-<sha1_sum_of_entire_file>.
+ *
+ *     thread: The ID of the thread to which the mail belongs
+ *
+ *     replyto: The ID from the In-Reply-To header of the mail (if any).
+ *
+ *    Multiple terms of given prefix:
+ *
+ *     reference: All message IDs from In-Reply-To and Re ferences
+ *                headers in the message.
+ *
+ *     tag:       Any tags associated with this message by the user.
+ *
+ *     file-direntry:  A colon-separated pair of values
+ *                     (INTEGER:STRING), where INTEGER is the
+ *                     document ID of a directory document, and
+ *                     STRING is the name of a file within that
+ *                     directory for this mail message.
+ *
+ *    A mail document also has two values:
+ *
+ *     TIMESTAMP:      The time_t value corresponding to the message's
+ *                     Date header.
+ *
+ *     MESSAGE_ID:     The unique ID of the mail mess (see "id" above)
+ *
+ * In addition, terms from the content of the message are added with
+ * "from", "to", "attachment", and "subject" prefixes for use by the
+ * user in searching. But the database doesn't really care itself
+ * about any of these.
+ *
+ * The data portion of a mail document is empty.
+ *
+ * Directory document
+ * ------------------
+ * A directory document is used by a client of the notmuch library to
+ * maintain data necessary to allow for efficient polling of mail
+ * directories.
+ *
+ * All directory documents contain one term:
+ *
+ *     directory:      The directory path (relative to the database path)
+ *                     Or the SHA1 sum of the directory path (if the
+ *                     path itself is too long to fit in a Xapian
+ *                     term).
+ *
+ * And all directory documents for directories other than top-level
+ * directories also contain the following term:
+ *
+ *     directory-direntry: A colon-separated pair of values
+ *                         (INTEGER:STRING), where INTEGER is the
+ *                         document ID of the parent directory
+ *                         document, and STRING is the name of this
+ *                         directory within that parent.
+ *
+ * All directory documents have a single value:
+ *
+ *     TIMESTAMP:      The mtime of the directory (at last scan)
+ *
+ * The data portion of a directory document contains the path of the
+ * directory (relative to the database path).
+ *
+ * Database metadata
+ * -----------------
+ * Xapian allows us to store arbitrary name-value pairs as
+ * "metadata". We currently use the following metadata names with the
+ * given meanings:
+ *
+ *     version         The database schema version, (which is distinct
+ *                     from both the notmuch package version (see
+ *                     notmuch --version) and the libnotmuch library
+ *                     version. The version is stored as an base-10
+ *                     ASCII integer. The initial database version
+ *                     was 1, (though a schema existed before that
+ *                     were no "version" database value existed at
+ *                     all). Succesive versions are allocated as
+ *                     changes are made to the database (such as by
+ *                     indexing new fields).
+ *
+ *     last_thread_id  The last thread ID generated. This is stored
+ *                     as a 16-byte hexadecimal ASCII representation
+ *                     of a 64-bit unsigned integer. The first ID
+ *                     generated is 1 and the value will be
+ *                     incremented for each thread ID.
+ *
+ *     thread_id_*     A pre-allocated thread ID for a particular
+ *                     message. This is actually an arbitarily large
+ *                     family of metadata name. Any particular name
+ *                     is formed by concatenating "thread_id_" with a
+ *                     message ID. The value stored is a thread ID.
+ *
+ *                     These thread ID metadata values are stored
+ *                     whenever a message references a parent message
+ *                     that does not yet exist in the database. A
+ *                     thread ID will be allocated and stored, and if
+ *                     the message is later added, the stored thread
+ *                     ID will be used (and the metadata value will
+ *                     be cleared).
+ *
+ *                     Even before a message is added, it's
+ *                     pre-allocated thread ID is useful so that all
+ *                     descendant messages that reference this common
+ *                     parent can be recognized as belonging to the
+ *                     same thread.
+ */
+
+/* With these prefix values we follow the conventions published here:
+ *
+ * http://xapian.org/docs/omega/termprefixes.html
+ *
+ * as much as makes sense. Note that I took some liberty in matching
+ * the reserved prefix values to notmuch concepts, (for example, 'G'
+ * is documented as "newsGroup (or similar entity - e.g. a web forum
+ * name)", for which I think the thread is the closest analogue in
+ * notmuch. This in spite of the fact that we will eventually be
+ * storing mailing-list messages where 'G' for "mailing list name"
+ * might be even a closer analogue. I'm treating the single-character
+ * prefixes preferentially for core notmuch concepts (which will be
+ * nearly universal to all mail messages).
+ */
+
+prefix_t BOOLEAN_PREFIX_INTERNAL[] = {
+    { "type",                  "T" },
+    { "reference",             "XREFERENCE" },
+    { "replyto",               "XREPLYTO" },
+    { "directory",             "XDIRECTORY" },
+    { "file-direntry",         "XFDIRENTRY" },
+    { "directory-direntry",    "XDDIRENTRY" },
+};
+
+prefix_t BOOLEAN_PREFIX_EXTERNAL[] = {
+    { "thread",                        "G" },
+    { "tag",                   "K" },
+    { "is",                    "K" },
+    { "id",                    "Q" }
+};
+
+prefix_t PROBABILISTIC_PREFIX[]= {
+    { "from",                  "XFROM" },
+    { "to",                    "XTO" },
+    { "attachment",            "XATTACHMENT" },
+    { "subject",               "XSUBJECT"}
+};
+
+int
+_internal_error (const char *format, ...)
+{
+    va_list va_args;
+
+    va_start (va_args, format);
+
+    fprintf (stderr, "Internal error: ");
+    vfprintf (stderr, format, va_args);
+
+    exit (1);
+
+    return 1;
+}
+
+const char *
+_find_prefix (const char *name)
+{
+    unsigned int i;
+
+    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_INTERNAL); i++) {
+       if (strcmp (name, BOOLEAN_PREFIX_INTERNAL[i].name) == 0)
+           return BOOLEAN_PREFIX_INTERNAL[i].prefix;
+    }
+
+    for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
+       if (strcmp (name, BOOLEAN_PREFIX_EXTERNAL[i].name) == 0)
+           return BOOLEAN_PREFIX_EXTERNAL[i].prefix;
+    }
+
+    for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
+       if (strcmp (name, PROBABILISTIC_PREFIX[i].name) == 0)
+           return PROBABILISTIC_PREFIX[i].prefix;
+    }
+
+    INTERNAL_ERROR ("No prefix exists for '%s'\n", name);
+
+    return "";
+}
+
+const char *
+notmuch_status_to_string (notmuch_status_t status)
+{
+    switch (status) {
+    case NOTMUCH_STATUS_SUCCESS:
+       return "No error occurred";
+    case NOTMUCH_STATUS_OUT_OF_MEMORY:
+       return "Out of memory";
+    case NOTMUCH_STATUS_READ_ONLY_DATABASE:
+       return "Attempt to write to a read-only database";
+    case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+       return "A Xapian exception occurred";
+    case NOTMUCH_STATUS_FILE_ERROR:
+       return "Something went wrong trying to read or write a file";
+    case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+       return "File is not an email";
+    case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+       return "Message ID is identical to a message in database";
+    case NOTMUCH_STATUS_NULL_POINTER:
+       return "Erroneous NULL pointer";
+    case NOTMUCH_STATUS_TAG_TOO_LONG:
+       return "Tag value is too long (exceeds NOTMUCH_TAG_MAX)";
+    case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+       return "Unbalanced number of calls to notmuch_message_freeze/thaw";
+    default:
+    case NOTMUCH_STATUS_LAST_STATUS:
+       return "Unknown error status value";
+    }
+}
+
+static void
+find_doc_ids_for_term (notmuch_database_t *notmuch,
+                      const char *term,
+                      Xapian::PostingIterator *begin,
+                      Xapian::PostingIterator *end)
+{
+    *begin = notmuch->xapian_db->postlist_begin (term);
+
+    *end = notmuch->xapian_db->postlist_end (term);
+}
+
+static void
+find_doc_ids (notmuch_database_t *notmuch,
+             const char *prefix_name,
+             const char *value,
+             Xapian::PostingIterator *begin,
+             Xapian::PostingIterator *end)
+{
+    char *term;
+
+    term = talloc_asprintf (notmuch, "%s%s",
+                           _find_prefix (prefix_name), value);
+
+    find_doc_ids_for_term (notmuch, term, begin, end);
+
+    talloc_free (term);
+}
+
+notmuch_private_status_t
+_notmuch_database_find_unique_doc_id (notmuch_database_t *notmuch,
+                                     const char *prefix_name,
+                                     const char *value,
+                                     unsigned int *doc_id)
+{
+    Xapian::PostingIterator i, end;
+
+    find_doc_ids (notmuch, prefix_name, value, &i, &end);
+
+    if (i == end) {
+       *doc_id = 0;
+       return NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+    }
+
+    *doc_id = *i;
+
+#if DEBUG_DATABASE_SANITY
+    i++;
+
+    if (i != end)
+       INTERNAL_ERROR ("Term %s:%s is not unique as expected.\n",
+                       prefix_name, value);
+#endif
+
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+static Xapian::Document
+find_document_for_doc_id (notmuch_database_t *notmuch, unsigned doc_id)
+{
+    return notmuch->xapian_db->get_document (doc_id);
+}
+
+notmuch_message_t *
+notmuch_database_find_message (notmuch_database_t *notmuch,
+                              const char *message_id)
+{
+    notmuch_private_status_t status;
+    unsigned int doc_id;
+
+    status = _notmuch_database_find_unique_doc_id (notmuch, "id",
+                                                  message_id, &doc_id);
+
+    if (status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+       return NULL;
+
+    return _notmuch_message_create (notmuch, notmuch, doc_id, NULL);
+}
+
+/* Advance 'str' past any whitespace or RFC 822 comments. A comment is
+ * a (potentially nested) parenthesized sequence with '\' used to
+ * escape any character (including parentheses).
+ *
+ * If the sequence to be skipped continues to the end of the string,
+ * then 'str' will be left pointing at the final terminating '\0'
+ * character.
+ */
+static void
+skip_space_and_comments (const char **str)
+{
+    const char *s;
+
+    s = *str;
+    while (*s && (isspace (*s) || *s == '(')) {
+       while (*s && isspace (*s))
+           s++;
+       if (*s == '(') {
+           int nesting = 1;
+           s++;
+           while (*s && nesting) {
+               if (*s == '(') {
+                   nesting++;
+               } else if (*s == ')') {
+                   nesting--;
+               } else if (*s == '\\') {
+                   if (*(s+1))
+                       s++;
+               }
+               s++;
+           }
+       }
+    }
+
+    *str = s;
+}
+
+/* Parse an RFC 822 message-id, discarding whitespace, any RFC 822
+ * comments, and the '<' and '>' delimeters.
+ *
+ * If not NULL, then *next will be made to point to the first character
+ * not parsed, (possibly pointing to the final '\0' terminator.
+ *
+ * Returns a newly talloc'ed string belonging to 'ctx'.
+ *
+ * Returns NULL if there is any error parsing the message-id. */
+static char *
+_parse_message_id (void *ctx, const char *message_id, const char **next)
+{
+    const char *s, *end;
+    char *result;
+
+    if (message_id == NULL || *message_id == '\0')
+       return NULL;
+
+    s = message_id;
+
+    skip_space_and_comments (&s);
+
+    /* Skip any unstructured text as well. */
+    while (*s && *s != '<')
+       s++;
+
+    if (*s == '<') {
+       s++;
+    } else {
+       if (next)
+           *next = s;
+       return NULL;
+    }
+
+    skip_space_and_comments (&s);
+
+    end = s;
+    while (*end && *end != '>')
+       end++;
+    if (next) {
+       if (*end)
+           *next = end + 1;
+       else
+           *next = end;
+    }
+
+    if (end > s && *end == '>')
+       end--;
+    if (end <= s)
+       return NULL;
+
+    result = talloc_strndup (ctx, s, end - s + 1);
+
+    /* Finally, collapse any whitespace that is within the message-id
+     * itself. */
+    {
+       char *r;
+       int len;
+
+       for (r = result, len = strlen (r); *r; r++, len--)
+           if (*r == ' ' || *r == '\t')
+               memmove (r, r+1, len);
+    }
+
+    return result;
+}
+
+/* Parse a References header value, putting a (talloc'ed under 'ctx')
+ * copy of each referenced message-id into 'hash'.
+ *
+ * We explicitly avoid including any reference identical to
+ * 'message_id' in the result (to avoid mass confusion when a single
+ * message references itself cyclically---and yes, mail messages are
+ * not infrequent in the wild that do this---don't ask me why).
+*/
+static void
+parse_references (void *ctx,
+                 const char *message_id,
+                 GHashTable *hash,
+                 const char *refs)
+{
+    char *ref;
+
+    if (refs == NULL || *refs == '\0')
+       return;
+
+    while (*refs) {
+       ref = _parse_message_id (ctx, refs, &refs);
+
+       if (ref && strcmp (ref, message_id))
+           g_hash_table_insert (hash, ref, NULL);
+    }
+}
+
+notmuch_database_t *
+notmuch_database_create (const char *path)
+{
+    notmuch_database_t *notmuch = NULL;
+    char *notmuch_path = NULL;
+    struct stat st;
+    int err;
+
+    if (path == NULL) {
+       fprintf (stderr, "Error: Cannot create a database for a NULL path.\n");
+       goto DONE;
+    }
+
+    err = stat (path, &st);
+    if (err) {
+       fprintf (stderr, "Error: Cannot create database at %s: %s.\n",
+                path, strerror (errno));
+       goto DONE;
+    }
+
+    if (! S_ISDIR (st.st_mode)) {
+       fprintf (stderr, "Error: Cannot create database at %s: Not a directory.\n",
+                path);
+       goto DONE;
+    }
+
+    notmuch_path = talloc_asprintf (NULL, "%s/%s", path, ".notmuch");
+
+    err = mkdir (notmuch_path, 0755);
+
+    if (err) {
+       fprintf (stderr, "Error: Cannot create directory %s: %s.\n",
+                notmuch_path, strerror (errno));
+       goto DONE;
+    }
+
+    notmuch = notmuch_database_open (path,
+                                    NOTMUCH_DATABASE_MODE_READ_WRITE);
+    notmuch_database_upgrade (notmuch, NULL, NULL);
+
+  DONE:
+    if (notmuch_path)
+       talloc_free (notmuch_path);
+
+    return notmuch;
+}
+
+notmuch_status_t
+_notmuch_database_ensure_writable (notmuch_database_t *notmuch)
+{
+    if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY) {
+       fprintf (stderr, "Cannot write to a read-only database.\n");
+       return NOTMUCH_STATUS_READ_ONLY_DATABASE;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_database_t *
+notmuch_database_open (const char *path,
+                      notmuch_database_mode_t mode)
+{
+    notmuch_database_t *notmuch = NULL;
+    char *notmuch_path = NULL, *xapian_path = NULL;
+    struct stat st;
+    int err;
+    unsigned int i, version;
+
+    if (asprintf (&notmuch_path, "%s/%s", path, ".notmuch") == -1) {
+       notmuch_path = NULL;
+       fprintf (stderr, "Out of memory\n");
+       goto DONE;
+    }
+
+    err = stat (notmuch_path, &st);
+    if (err) {
+       fprintf (stderr, "Error opening database at %s: %s\n",
+                notmuch_path, strerror (errno));
+       goto DONE;
+    }
+
+    if (asprintf (&xapian_path, "%s/%s", notmuch_path, "xapian") == -1) {
+       xapian_path = NULL;
+       fprintf (stderr, "Out of memory\n");
+       goto DONE;
+    }
+
+    notmuch = talloc (NULL, notmuch_database_t);
+    notmuch->exception_reported = FALSE;
+    notmuch->path = talloc_strdup (notmuch, path);
+
+    if (notmuch->path[strlen (notmuch->path) - 1] == '/')
+       notmuch->path[strlen (notmuch->path) - 1] = '\0';
+
+    notmuch->needs_upgrade = FALSE;
+    notmuch->mode = mode;
+    try {
+       string last_thread_id;
+
+       if (mode == NOTMUCH_DATABASE_MODE_READ_WRITE) {
+           notmuch->xapian_db = new Xapian::WritableDatabase (xapian_path,
+                                                              Xapian::DB_CREATE_OR_OPEN);
+           version = notmuch_database_get_version (notmuch);
+
+           if (version > NOTMUCH_DATABASE_VERSION) {
+               fprintf (stderr,
+                        "Error: Notmuch database at %s\n"
+                        "       has a newer database format version (%u) than supported by this\n"
+                        "       version of notmuch (%u). Refusing to open this database in\n"
+                        "       read-write mode.\n",
+                        notmuch_path, version, NOTMUCH_DATABASE_VERSION);
+               notmuch->mode = NOTMUCH_DATABASE_MODE_READ_ONLY;
+               notmuch_database_close (notmuch);
+               notmuch = NULL;
+               goto DONE;
+           }
+
+           if (version < NOTMUCH_DATABASE_VERSION)
+               notmuch->needs_upgrade = TRUE;
+       } else {
+           notmuch->xapian_db = new Xapian::Database (xapian_path);
+           version = notmuch_database_get_version (notmuch);
+           if (version > NOTMUCH_DATABASE_VERSION)
+           {
+               fprintf (stderr,
+                        "Warning: Notmuch database at %s\n"
+                        "         has a newer database format version (%u) than supported by this\n"
+                        "         version of notmuch (%u). Some operations may behave incorrectly,\n"
+                        "         (but the database will not be harmed since it is being opened\n"
+                        "         in read-only mode).\n",
+                        notmuch_path, version, NOTMUCH_DATABASE_VERSION);
+           }
+       }
+
+       last_thread_id = notmuch->xapian_db->get_metadata ("last_thread_id");
+       if (last_thread_id.empty ()) {
+           notmuch->last_thread_id = 0;
+       } else {
+           const char *str;
+           char *end;
+
+           str = last_thread_id.c_str ();
+           notmuch->last_thread_id = strtoull (str, &end, 16);
+           if (*end != '\0')
+               INTERNAL_ERROR ("Malformed database last_thread_id: %s", str);
+       }
+
+       notmuch->query_parser = new Xapian::QueryParser;
+       notmuch->term_gen = new Xapian::TermGenerator;
+       notmuch->term_gen->set_stemmer (Xapian::Stem ("english"));
+       notmuch->value_range_processor = new Xapian::NumberValueRangeProcessor (NOTMUCH_VALUE_TIMESTAMP);
+
+       notmuch->query_parser->set_default_op (Xapian::Query::OP_AND);
+       notmuch->query_parser->set_database (*notmuch->xapian_db);
+       notmuch->query_parser->set_stemmer (Xapian::Stem ("english"));
+       notmuch->query_parser->set_stemming_strategy (Xapian::QueryParser::STEM_SOME);
+       notmuch->query_parser->add_valuerangeprocessor (notmuch->value_range_processor);
+
+       for (i = 0; i < ARRAY_SIZE (BOOLEAN_PREFIX_EXTERNAL); i++) {
+           prefix_t *prefix = &BOOLEAN_PREFIX_EXTERNAL[i];
+           notmuch->query_parser->add_boolean_prefix (prefix->name,
+                                                      prefix->prefix);
+       }
+
+       for (i = 0; i < ARRAY_SIZE (PROBABILISTIC_PREFIX); i++) {
+           prefix_t *prefix = &PROBABILISTIC_PREFIX[i];
+           notmuch->query_parser->add_prefix (prefix->name, prefix->prefix);
+       }
+    } catch (const Xapian::Error &error) {
+       fprintf (stderr, "A Xapian exception occurred opening database: %s\n",
+                error.get_msg().c_str());
+       notmuch = NULL;
+    }
+
+  DONE:
+    if (notmuch_path)
+       free (notmuch_path);
+    if (xapian_path)
+       free (xapian_path);
+
+    return notmuch;
+}
+
+void
+notmuch_database_close (notmuch_database_t *notmuch)
+{
+    try {
+       if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_WRITE)
+           (static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db))->flush ();
+    } catch (const Xapian::Error &error) {
+       if (! notmuch->exception_reported) {
+           fprintf (stderr, "Error: A Xapian exception occurred flushing database: %s\n",
+                    error.get_msg().c_str());
+       }
+    }
+
+    delete notmuch->term_gen;
+    delete notmuch->query_parser;
+    delete notmuch->xapian_db;
+    delete notmuch->value_range_processor;
+    talloc_free (notmuch);
+}
+
+const char *
+notmuch_database_get_path (notmuch_database_t *notmuch)
+{
+    return notmuch->path;
+}
+
+unsigned int
+notmuch_database_get_version (notmuch_database_t *notmuch)
+{
+    unsigned int version;
+    string version_string;
+    const char *str;
+    char *end;
+
+    version_string = notmuch->xapian_db->get_metadata ("version");
+    if (version_string.empty ())
+       return 0;
+
+    str = version_string.c_str ();
+    if (str == NULL || *str == '\0')
+       return 0;
+
+    version = strtoul (str, &end, 10);
+    if (*end != '\0')
+       INTERNAL_ERROR ("Malformed database version: %s", str);
+
+    return version;
+}
+
+notmuch_bool_t
+notmuch_database_needs_upgrade (notmuch_database_t *notmuch)
+{
+    return notmuch->needs_upgrade;
+}
+
+static volatile sig_atomic_t do_progress_notify = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+    do_progress_notify = 1;
+}
+
+/* Upgrade 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.
+ *
+ * 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 'count' as the number of messages upgraded
+ * so far and 'total' the overall number of messages that will be
+ * converted.
+ */
+notmuch_status_t
+notmuch_database_upgrade (notmuch_database_t *notmuch,
+                         void (*progress_notify) (void *closure,
+                                                  double progress),
+                         void *closure)
+{
+    Xapian::WritableDatabase *db;
+    struct sigaction action;
+    struct itimerval timerval;
+    notmuch_bool_t timer_is_active = FALSE;
+    unsigned int version;
+    notmuch_status_t status;
+    unsigned int count = 0, total = 0;
+
+    status = _notmuch_database_ensure_writable (notmuch);
+    if (status)
+       return status;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    version = notmuch_database_get_version (notmuch);
+
+    if (version >= NOTMUCH_DATABASE_VERSION)
+       return NOTMUCH_STATUS_SUCCESS;
+
+    if (progress_notify) {
+       /* Setup our handler for SIGALRM */
+       memset (&action, 0, sizeof (struct sigaction));
+       action.sa_handler = handle_sigalrm;
+       sigemptyset (&action.sa_mask);
+       action.sa_flags = SA_RESTART;
+       sigaction (SIGALRM, &action, NULL);
+
+       /* Then start a timer to send SIGALRM once per second. */
+       timerval.it_interval.tv_sec = 1;
+       timerval.it_interval.tv_usec = 0;
+       timerval.it_value.tv_sec = 1;
+       timerval.it_value.tv_usec = 0;
+       setitimer (ITIMER_REAL, &timerval, NULL);
+
+       timer_is_active = TRUE;
+    }
+
+    /* Before version 1, each message document had its filename in the
+     * data field. Copy that into the new format by calling
+     * notmuch_message_add_filename.
+     */
+    if (version < 1) {
+       notmuch_query_t *query = notmuch_query_create (notmuch, "");
+       notmuch_messages_t *messages;
+       notmuch_message_t *message;
+       char *filename;
+       Xapian::TermIterator t, t_end;
+
+       total = notmuch_query_count_messages (query);
+
+       for (messages = notmuch_query_search_messages (query);
+            notmuch_messages_valid (messages);
+            notmuch_messages_move_to_next (messages))
+       {
+           if (do_progress_notify) {
+               progress_notify (closure, (double) count / total);
+               do_progress_notify = 0;
+           }
+
+           message = notmuch_messages_get (messages);
+
+           filename = _notmuch_message_talloc_copy_data (message);
+           if (filename && *filename != '\0') {
+               _notmuch_message_add_filename (message, filename);
+               _notmuch_message_sync (message);
+           }
+           talloc_free (filename);
+
+           notmuch_message_destroy (message);
+
+           count++;
+       }
+
+       notmuch_query_destroy (query);
+
+       /* Also, before version 1 we stored directory timestamps in
+        * XTIMESTAMP documents instead of the current XDIRECTORY
+        * documents. So copy those as well. */
+
+       t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
+
+       for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
+            t != t_end;
+            t++)
+       {
+           Xapian::PostingIterator p, p_end;
+           std::string term = *t;
+
+           p_end = notmuch->xapian_db->postlist_end (term);
+
+           for (p = notmuch->xapian_db->postlist_begin (term);
+                p != p_end;
+                p++)
+           {
+               Xapian::Document document;
+               time_t mtime;
+               notmuch_directory_t *directory;
+
+               if (do_progress_notify) {
+                   progress_notify (closure, (double) count / total);
+                   do_progress_notify = 0;
+               }
+
+               document = find_document_for_doc_id (notmuch, *p);
+               mtime = Xapian::sortable_unserialise (
+                   document.get_value (NOTMUCH_VALUE_TIMESTAMP));
+
+               directory = notmuch_database_get_directory (notmuch,
+                                                           term.c_str() + 10);
+               notmuch_directory_set_mtime (directory, mtime);
+               notmuch_directory_destroy (directory);
+           }
+       }
+    }
+
+    db->set_metadata ("version", STRINGIFY (NOTMUCH_DATABASE_VERSION));
+    db->flush ();
+
+    /* Now that the upgrade is complete we can remove the old data
+     * and documents that are no longer needed. */
+    if (version < 1) {
+       notmuch_query_t *query = notmuch_query_create (notmuch, "");
+       notmuch_messages_t *messages;
+       notmuch_message_t *message;
+       char *filename;
+
+       for (messages = notmuch_query_search_messages (query);
+            notmuch_messages_valid (messages);
+            notmuch_messages_move_to_next (messages))
+       {
+           if (do_progress_notify) {
+               progress_notify (closure, (double) count / total);
+               do_progress_notify = 0;
+           }
+
+           message = notmuch_messages_get (messages);
+
+           filename = _notmuch_message_talloc_copy_data (message);
+           if (filename && *filename != '\0') {
+               _notmuch_message_clear_data (message);
+               _notmuch_message_sync (message);
+           }
+           talloc_free (filename);
+
+           notmuch_message_destroy (message);
+       }
+
+       notmuch_query_destroy (query);
+    }
+
+    if (version < 1) {
+       Xapian::TermIterator t, t_end;
+
+       t_end = notmuch->xapian_db->allterms_end ("XTIMESTAMP");
+
+       for (t = notmuch->xapian_db->allterms_begin ("XTIMESTAMP");
+            t != t_end;
+            t++)
+       {
+           Xapian::PostingIterator p, p_end;
+           std::string term = *t;
+
+           p_end = notmuch->xapian_db->postlist_end (term);
+
+           for (p = notmuch->xapian_db->postlist_begin (term);
+                p != p_end;
+                p++)
+           {
+               if (do_progress_notify) {
+                   progress_notify (closure, (double) count / total);
+                   do_progress_notify = 0;
+               }
+
+               db->delete_document (*p);
+           }
+       }
+    }
+
+    if (timer_is_active) {
+       /* Now stop the timer. */
+       timerval.it_interval.tv_sec = 0;
+       timerval.it_interval.tv_usec = 0;
+       timerval.it_value.tv_sec = 0;
+       timerval.it_value.tv_usec = 0;
+       setitimer (ITIMER_REAL, &timerval, NULL);
+
+       /* And disable the signal handler. */
+       action.sa_handler = SIG_IGN;
+       sigaction (SIGALRM, &action, NULL);
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* We allow the user to use arbitrarily long paths for directories. But
+ * we have a term-length limit. So if we exceed that, we'll use the
+ * SHA-1 of the path for the database term.
+ *
+ * Note: This function may return the original value of 'path'. If it
+ * does not, then the caller is responsible to free() the returned
+ * value.
+ */
+const char *
+_notmuch_database_get_directory_db_path (const char *path)
+{
+    int term_len = strlen (_find_prefix ("directory")) + strlen (path);
+
+    if (term_len > NOTMUCH_TERM_MAX)
+       return notmuch_sha1_of_string (path);
+    else
+       return path;
+}
+
+/* Given a path, split it into two parts: the directory part is all
+ * components except for the last, and the basename is that last
+ * component. Getting the return-value for either part is optional
+ * (the caller can pass NULL).
+ *
+ * The original 'path' can represent either a regular file or a
+ * directory---the splitting will be carried out in the same way in
+ * either case. Trailing slashes on 'path' will be ignored, and any
+ * cases of multiple '/' characters appearing in series will be
+ * treated as a single '/'.
+ *
+ * Allocation (if any) will have 'ctx' as the talloc owner. But
+ * pointers will be returned within the original path string whenever
+ * possible.
+ *
+ * Note: If 'path' is non-empty and contains no non-trailing slash,
+ * (that is, consists of a filename with no parent directory), then
+ * the directory returned will be an empty string. However, if 'path'
+ * is an empty string, then both directory and basename will be
+ * returned as NULL.
+ */
+notmuch_status_t
+_notmuch_database_split_path (void *ctx,
+                             const char *path,
+                             const char **directory,
+                             const char **basename)
+{
+    const char *slash;
+
+    if (path == NULL || *path == '\0') {
+       if (directory)
+           *directory = NULL;
+       if (basename)
+           *basename = NULL;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    /* Find the last slash (not counting a trailing slash), if any. */
+
+    slash = path + strlen (path) - 1;
+
+    /* First, skip trailing slashes. */
+    while (slash != path) {
+       if (*slash != '/')
+           break;
+
+       --slash;
+    }
+
+    /* Then, find a slash. */
+    while (slash != path) {
+       if (*slash == '/')
+           break;
+
+       if (basename)
+           *basename = slash;
+
+       --slash;
+    }
+
+    /* Finally, skip multiple slashes. */
+    while (slash != path) {
+       if (*slash != '/')
+           break;
+
+       --slash;
+    }
+
+    if (slash == path) {
+       if (directory)
+           *directory = talloc_strdup (ctx, "");
+       if (basename)
+           *basename = path;
+    } else {
+       if (directory)
+           *directory = talloc_strndup (ctx, path, slash - path + 1);
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+_notmuch_database_find_directory_id (notmuch_database_t *notmuch,
+                                    const char *path,
+                                    unsigned int *directory_id)
+{
+    notmuch_directory_t *directory;
+    notmuch_status_t status;
+
+    if (path == NULL) {
+       *directory_id = 0;
+       return NOTMUCH_STATUS_SUCCESS;
+    }
+
+    directory = _notmuch_directory_create (notmuch, path, &status);
+    if (status) {
+       *directory_id = -1;
+       return status;
+    }
+
+    *directory_id = _notmuch_directory_get_document_id (directory);
+
+    notmuch_directory_destroy (directory);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+const char *
+_notmuch_database_get_directory_path (void *ctx,
+                                     notmuch_database_t *notmuch,
+                                     unsigned int doc_id)
+{
+    Xapian::Document document;
+
+    document = find_document_for_doc_id (notmuch, doc_id);
+
+    return talloc_strdup (ctx, document.get_data ().c_str ());
+}
+
+/* Given a legal 'filename' for the database, (either relative to
+ * database path or absolute with initial components identical to
+ * database path), return a new string (with 'ctx' as the talloc
+ * owner) suitable for use as a direntry term value.
+ *
+ * The necessary directory documents will be created in the database
+ * as needed.
+ */
+notmuch_status_t
+_notmuch_database_filename_to_direntry (void *ctx,
+                                       notmuch_database_t *notmuch,
+                                       const char *filename,
+                                       char **direntry)
+{
+    const char *relative, *directory, *basename;
+    Xapian::docid directory_id;
+    notmuch_status_t status;
+
+    relative = _notmuch_database_relative_path (notmuch, filename);
+
+    status = _notmuch_database_split_path (ctx, relative,
+                                          &directory, &basename);
+    if (status)
+       return status;
+
+    status = _notmuch_database_find_directory_id (notmuch, directory,
+                                                 &directory_id);
+    if (status)
+       return status;
+
+    *direntry = talloc_asprintf (ctx, "%u:%s", directory_id, basename);
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+/* Given a legal 'path' for the database, return the relative path.
+ *
+ * The return value will be a pointer to the originl path contents,
+ * and will be either the original string (if 'path' was relative) or
+ * a portion of the string (if path was absolute and begins with the
+ * database path).
+ */
+const char *
+_notmuch_database_relative_path (notmuch_database_t *notmuch,
+                                const char *path)
+{
+    const char *db_path, *relative;
+    unsigned int db_path_len;
+
+    db_path = notmuch_database_get_path (notmuch);
+    db_path_len = strlen (db_path);
+
+    relative = path;
+
+    if (*relative == '/') {
+       while (*relative == '/' && *(relative+1) == '/')
+           relative++;
+
+       if (strncmp (relative, db_path, db_path_len) == 0)
+       {
+           relative += db_path_len;
+           while (*relative == '/')
+               relative++;
+       }
+    }
+
+    return relative;
+}
+
+notmuch_directory_t *
+notmuch_database_get_directory (notmuch_database_t *notmuch,
+                               const char *path)
+{
+    notmuch_status_t status;
+
+    return _notmuch_directory_create (notmuch, path, &status);
+}
+
+static const char *
+_notmuch_database_generate_thread_id (notmuch_database_t *notmuch)
+{
+    /* 16 bytes (+ terminator) for hexadecimal representation of
+     * a 64-bit integer. */
+    static char thread_id[17];
+    Xapian::WritableDatabase *db;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    notmuch->last_thread_id++;
+
+    sprintf (thread_id, "%016" PRIx64, notmuch->last_thread_id);
+
+    db->set_metadata ("last_thread_id", thread_id);
+
+    return thread_id;
+}
+
+static char *
+_get_metadata_thread_id_key (void *ctx, const char *message_id)
+{
+    return talloc_asprintf (ctx, "thread_id_%s", message_id);
+}
+
+/* Find the thread ID to which the message with 'message_id' belongs.
+ *
+ * Always returns a newly talloced string belonging to 'ctx'.
+ *
+ * Note: If there is no message in the database with the given
+ * 'message_id' then a new thread_id will be allocated for this
+ * message and stored in the database metadata, (where this same
+ * thread ID can be looked up if the message is added to the database
+ * later).
+ */
+static const char *
+_resolve_message_id_to_thread_id (notmuch_database_t *notmuch,
+                                 void *ctx,
+                                 const char *message_id)
+{
+    notmuch_message_t *message;
+    string thread_id_string;
+    const char *thread_id;
+    char *metadata_key;
+    Xapian::WritableDatabase *db;
+
+    message = notmuch_database_find_message (notmuch, message_id);
+
+    if (message) {
+       thread_id = talloc_steal (ctx, notmuch_message_get_thread_id (message));
+
+       notmuch_message_destroy (message);
+
+       return thread_id;
+    }
+
+    /* Message has not been seen yet.
+     *
+     * We may have seen a reference to it already, in which case, we
+     * can return the thread ID stored in the metadata. Otherwise, we
+     * generate a new thread ID and store it there.
+     */
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+    metadata_key = _get_metadata_thread_id_key (ctx, message_id);
+    thread_id_string = notmuch->xapian_db->get_metadata (metadata_key);
+
+    if (thread_id_string.empty()) {
+       thread_id = _notmuch_database_generate_thread_id (notmuch);
+       db->set_metadata (metadata_key, thread_id);
+    } else {
+       thread_id = thread_id_string.c_str();
+    }
+
+    talloc_free (metadata_key);
+
+    return thread_id;
+}
+
+static notmuch_status_t
+_merge_threads (notmuch_database_t *notmuch,
+               const char *winner_thread_id,
+               const char *loser_thread_id)
+{
+    Xapian::PostingIterator loser, loser_end;
+    notmuch_message_t *message = NULL;
+    notmuch_private_status_t private_status;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    find_doc_ids (notmuch, "thread", loser_thread_id, &loser, &loser_end);
+
+    for ( ; loser != loser_end; loser++) {
+       message = _notmuch_message_create (notmuch, notmuch,
+                                          *loser, &private_status);
+       if (message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       _notmuch_message_remove_term (message, "thread", loser_thread_id);
+       _notmuch_message_add_term (message, "thread", winner_thread_id);
+       _notmuch_message_sync (message);
+
+       notmuch_message_destroy (message);
+       message = NULL;
+    }
+
+  DONE:
+    if (message)
+       notmuch_message_destroy (message);
+
+    return ret;
+}
+
+static void
+_my_talloc_free_for_g_hash (void *ptr)
+{
+    talloc_free (ptr);
+}
+
+static notmuch_status_t
+_notmuch_database_link_message_to_parents (notmuch_database_t *notmuch,
+                                          notmuch_message_t *message,
+                                          notmuch_message_file_t *message_file,
+                                          const char **thread_id)
+{
+    GHashTable *parents = NULL;
+    const char *refs, *in_reply_to, *in_reply_to_message_id;
+    GList *l, *keys = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+
+    parents = g_hash_table_new_full (g_str_hash, g_str_equal,
+                                    _my_talloc_free_for_g_hash, NULL);
+
+    refs = notmuch_message_file_get_header (message_file, "references");
+    parse_references (message, notmuch_message_get_message_id (message),
+                     parents, refs);
+
+    in_reply_to = notmuch_message_file_get_header (message_file, "in-reply-to");
+    parse_references (message, notmuch_message_get_message_id (message),
+                     parents, in_reply_to);
+
+    /* Carefully avoid adding any self-referential in-reply-to term. */
+    in_reply_to_message_id = _parse_message_id (message, in_reply_to, NULL);
+    if (in_reply_to_message_id &&
+       strcmp (in_reply_to_message_id,
+               notmuch_message_get_message_id (message)))
+    {
+       _notmuch_message_add_term (message, "replyto",
+                            _parse_message_id (message, in_reply_to, NULL));
+    }
+
+    keys = g_hash_table_get_keys (parents);
+    for (l = keys; l; l = l->next) {
+       char *parent_message_id;
+       const char *parent_thread_id;
+
+       parent_message_id = (char *) l->data;
+
+       _notmuch_message_add_term (message, "reference",
+                                  parent_message_id);
+
+       parent_thread_id = _resolve_message_id_to_thread_id (notmuch,
+                                                            message,
+                                                            parent_message_id);
+
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, parent_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, parent_thread_id)) {
+           ret = _merge_threads (notmuch, *thread_id, parent_thread_id);
+           if (ret)
+               goto DONE;
+       }
+    }
+
+  DONE:
+    if (keys)
+       g_list_free (keys);
+    if (parents)
+       g_hash_table_unref (parents);
+
+    return ret;
+}
+
+static notmuch_status_t
+_notmuch_database_link_message_to_children (notmuch_database_t *notmuch,
+                                           notmuch_message_t *message,
+                                           const char **thread_id)
+{
+    const char *message_id = notmuch_message_get_message_id (message);
+    Xapian::PostingIterator child, children_end;
+    notmuch_message_t *child_message = NULL;
+    const char *child_thread_id;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+
+    find_doc_ids (notmuch, "reference", message_id, &child, &children_end);
+
+    for ( ; child != children_end; child++) {
+
+       child_message = _notmuch_message_create (message, notmuch,
+                                                *child, &private_status);
+       if (child_message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Cannot find document for doc_id from query");
+           goto DONE;
+       }
+
+       child_thread_id = notmuch_message_get_thread_id (child_message);
+       if (*thread_id == NULL) {
+           *thread_id = talloc_strdup (message, child_thread_id);
+           _notmuch_message_add_term (message, "thread", *thread_id);
+       } else if (strcmp (*thread_id, child_thread_id)) {
+           _notmuch_message_remove_term (child_message, "reference",
+                                         message_id);
+           _notmuch_message_sync (child_message);
+           ret = _merge_threads (notmuch, *thread_id, child_thread_id);
+           if (ret)
+               goto DONE;
+       }
+
+       notmuch_message_destroy (child_message);
+       child_message = NULL;
+    }
+
+  DONE:
+    if (child_message)
+       notmuch_message_destroy (child_message);
+
+    return ret;
+}
+
+/* Given a (mostly empty) 'message' and its corresponding
+ * 'message_file' link it to existing threads in the database.
+ *
+ * The first check is in the metadata of the database to see if we
+ * have pre-allocated a thread_id in advance for this message, (which
+ * would have happened if a message was previously added that
+ * referenced this one).
+ *
+ * Second, we look at 'message_file' and its link-relevant headers
+ * (References and In-Reply-To) for message IDs.
+ *
+ * Finally, we look in the database for existing message that
+ * reference 'message'.
+ *
+ * In all cases, we assign to the current message the first thread_id
+ * found (through either parent or child). We will also merge any
+ * existing, distinct threads where this message belongs to both,
+ * (which is not uncommon when mesages are processed out of order).
+ *
+ * Finally, if no thread ID has been found through parent or child, we
+ * call _notmuch_message_generate_thread_id to generate a new thread
+ * ID. This should only happen for new, top-level messages, (no
+ * References or In-Reply-To header in this message, and no previously
+ * added message refers to this message).
+ */
+static notmuch_status_t
+_notmuch_database_link_message (notmuch_database_t *notmuch,
+                               notmuch_message_t *message,
+                               notmuch_message_file_t *message_file)
+{
+    notmuch_status_t status;
+    const char *message_id, *thread_id = NULL;
+    char *metadata_key;
+    string stored_id;
+
+    message_id = notmuch_message_get_message_id (message);
+    metadata_key = _get_metadata_thread_id_key (message, message_id);
+
+    /* Check if we have already seen related messages to this one.
+     * If we have then use the thread_id that we stored at that time.
+     */
+    stored_id = notmuch->xapian_db->get_metadata (metadata_key);
+    if (! stored_id.empty()) {
+        Xapian::WritableDatabase *db;
+
+       db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+       /* Clear the metadata for this message ID. We don't need it
+        * anymore. */
+        db->set_metadata (metadata_key, "");
+        thread_id = stored_id.c_str();
+
+        _notmuch_message_add_term (message, "thread", thread_id);
+    }
+    talloc_free (metadata_key);
+
+    status = _notmuch_database_link_message_to_parents (notmuch, message,
+                                                       message_file,
+                                                       &thread_id);
+    if (status)
+       return status;
+
+    status = _notmuch_database_link_message_to_children (notmuch, message,
+                                                        &thread_id);
+    if (status)
+       return status;
+
+    /* If not part of any existing thread, generate a new thread ID. */
+    if (thread_id == NULL) {
+       thread_id = _notmuch_database_generate_thread_id (notmuch);
+
+       _notmuch_message_add_term (message, "thread", thread_id);
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_database_add_message (notmuch_database_t *notmuch,
+                             const char *filename,
+                             notmuch_message_t **message_ret)
+{
+    notmuch_message_file_t *message_file;
+    notmuch_message_t *message = NULL;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    notmuch_private_status_t private_status;
+
+    const char *date, *header;
+    const char *from, *to, *subject;
+    char *message_id = NULL;
+
+    if (message_ret)
+       *message_ret = NULL;
+
+    ret = _notmuch_database_ensure_writable (notmuch);
+    if (ret)
+       return ret;
+
+    message_file = notmuch_message_file_open (filename);
+    if (message_file == NULL)
+       return NOTMUCH_STATUS_FILE_ERROR;
+
+    notmuch_message_file_restrict_headers (message_file,
+                                          "date",
+                                          "from",
+                                          "in-reply-to",
+                                          "message-id",
+                                          "references",
+                                          "subject",
+                                          "to",
+                                          (char *) NULL);
+
+    try {
+       /* Before we do any real work, (especially before doing a
+        * potential SHA-1 computation on the entire file's contents),
+        * let's make sure that what we're looking at looks like an
+        * actual email message.
+        */
+       from = notmuch_message_file_get_header (message_file, "from");
+       subject = notmuch_message_file_get_header (message_file, "subject");
+       to = notmuch_message_file_get_header (message_file, "to");
+
+       if ((from == NULL || *from == '\0') &&
+           (subject == NULL || *subject == '\0') &&
+           (to == NULL || *to == '\0'))
+       {
+           ret = NOTMUCH_STATUS_FILE_NOT_EMAIL;
+           goto DONE;
+       }
+
+       /* Now that we're sure it's mail, the first order of business
+        * is to find a message ID (or else create one ourselves). */
+
+       header = notmuch_message_file_get_header (message_file, "message-id");
+       if (header && *header != '\0') {
+           message_id = _parse_message_id (message_file, header, NULL);
+
+           /* So the header value isn't RFC-compliant, but it's
+            * better than no message-id at all. */
+           if (message_id == NULL)
+               message_id = talloc_strdup (message_file, header);
+
+           /* Reject a Message ID that's too long. */
+           if (message_id && strlen (message_id) + 1 > NOTMUCH_TERM_MAX) {
+               talloc_free (message_id);
+               message_id = NULL;
+           }
+       }
+
+       if (message_id == NULL ) {
+           /* No message-id at all, let's generate one by taking a
+            * hash over the file's contents. */
+           char *sha1 = notmuch_sha1_of_file (filename);
+
+           /* If that failed too, something is really wrong. Give up. */
+           if (sha1 == NULL) {
+               ret = NOTMUCH_STATUS_FILE_ERROR;
+               goto DONE;
+           }
+
+           message_id = talloc_asprintf (message_file,
+                                         "notmuch-sha1-%s", sha1);
+           free (sha1);
+       }
+
+       /* Now that we have a message ID, we get a message object,
+        * (which may or may not reference an existing document in the
+        * database). */
+
+       message = _notmuch_message_create_for_message_id (notmuch,
+                                                         message_id,
+                                                         &private_status);
+
+       talloc_free (message_id);
+
+       if (message == NULL) {
+           ret = COERCE_STATUS (private_status,
+                                "Unexpected status value from _notmuch_message_create_for_message_id");
+           goto DONE;
+       }
+
+       _notmuch_message_add_filename (message, filename);
+
+       /* Is this a newly created message object? */
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           _notmuch_message_add_term (message, "type", "mail");
+
+           ret = _notmuch_database_link_message (notmuch, message,
+                                                 message_file);
+           if (ret)
+               goto DONE;
+
+           date = notmuch_message_file_get_header (message_file, "date");
+           _notmuch_message_set_date (message, date);
+
+           _notmuch_message_index_file (message, filename);
+       } else {
+           ret = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+       }
+
+       _notmuch_message_sync (message);
+    } catch (const Xapian::Error &error) {
+       fprintf (stderr, "A Xapian exception occurred adding message: %s.\n",
+                error.get_description().c_str());
+       notmuch->exception_reported = TRUE;
+       ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       goto DONE;
+    }
+
+  DONE:
+    if (message) {
+       if (ret == NOTMUCH_STATUS_SUCCESS && message_ret)
+           *message_ret = message;
+       else
+           notmuch_message_destroy (message);
+    }
+
+    if (message_file)
+       notmuch_message_file_close (message_file);
+
+    return ret;
+}
+
+notmuch_status_t
+notmuch_database_remove_message (notmuch_database_t *notmuch,
+                                const char *filename)
+{
+    Xapian::WritableDatabase *db;
+    void *local = talloc_new (notmuch);
+    const char *prefix = _find_prefix ("file-direntry");
+    char *direntry, *term;
+    Xapian::PostingIterator i, end;
+    Xapian::Document document;
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (notmuch);
+    if (status)
+       return status;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    status = _notmuch_database_filename_to_direntry (local, notmuch,
+                                                    filename, &direntry);
+    if (status)
+       return status;
+
+    term = talloc_asprintf (notmuch, "%s%s", prefix, direntry);
+
+    find_doc_ids_for_term (notmuch, term, &i, &end);
+
+    for ( ; i != end; i++) {
+       Xapian::TermIterator j;
+
+       document = find_document_for_doc_id (notmuch, *i);
+
+       document.remove_term (term);
+
+       j = document.termlist_begin ();
+       j.skip_to (prefix);
+
+       /* Was this the last file-direntry in the message? */
+       if (j == document.termlist_end () ||
+           strncmp ((*j).c_str (), prefix, strlen (prefix)))
+       {
+           db->delete_document (document.get_docid ());
+           status = NOTMUCH_STATUS_SUCCESS;
+       } else {
+           db->replace_document (document.get_docid (), document);
+           status = NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID;
+       }
+    }
+
+    talloc_free (local);
+
+    return status;
+}
+
+notmuch_tags_t *
+_notmuch_convert_tags (void *ctx, Xapian::TermIterator &i,
+                      Xapian::TermIterator &end)
+{
+    const char *prefix = _find_prefix ("tag");
+    notmuch_tags_t *tags;
+    std::string tag;
+
+    /* Currently this iteration is written with the assumption that
+     * "tag" has a single-character prefix. */
+    assert (strlen (prefix) == 1);
+
+    tags = _notmuch_tags_create (ctx);
+    if (unlikely (tags == NULL))
+       return NULL;
+
+    i.skip_to (prefix);
+
+    while (i != end) {
+       tag = *i;
+
+       if (tag.empty () || tag[0] != *prefix)
+           break;
+
+       _notmuch_tags_add_tag (tags, tag.c_str () + 1);
+
+       i++;
+    }
+
+    _notmuch_tags_prepare_iterator (tags);
+
+    return tags;
+}
+
+notmuch_tags_t *
+notmuch_database_get_all_tags (notmuch_database_t *db)
+{
+    Xapian::TermIterator i, end;
+    i = db->xapian_db->allterms_begin();
+    end = db->xapian_db->allterms_end();
+    return _notmuch_convert_tags(db, i, end);
+}
diff --git a/lib/directory.cc b/lib/directory.cc
new file mode 100644 (file)
index 0000000..5e75b73
--- /dev/null
@@ -0,0 +1,338 @@
+/* directory.cc - Results of directory-based searches from a notmuch database
+ *
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+#include "database-private.h"
+
+#include <xapian.h>
+
+struct _notmuch_filenames {
+    Xapian::TermIterator iterator;
+    Xapian::TermIterator end;
+    int prefix_len;
+    char *filename;
+};
+
+/* We end up having to call the destructors explicitly because we had
+ * to use "placement new" in order to initialize C++ objects within a
+ * block that we allocated with talloc. So C++ is making talloc
+ * slightly less simple to use, (we wouldn't need
+ * talloc_set_destructor at all otherwise).
+ */
+static int
+_notmuch_filenames_destructor (notmuch_filenames_t *filenames)
+{
+    filenames->iterator.~TermIterator ();
+    filenames->end.~TermIterator ();
+
+    return 0;
+}
+
+/* Create an iterator to iterate over the basenames of files (or
+ * directories) that all share a common parent directory.
+ *
+ * The code here is general enough to be reused for any case of
+ * iterating over the non-prefixed portion of terms sharing a common
+ * prefix.
+ */
+notmuch_filenames_t *
+_notmuch_filenames_create (void *ctx,
+                          notmuch_database_t *notmuch,
+                          const char *prefix)
+{
+    notmuch_filenames_t *filenames;
+
+    filenames = talloc (ctx, notmuch_filenames_t);
+    if (unlikely (filenames == NULL))
+       return NULL;
+
+    new (&filenames->iterator) Xapian::TermIterator ();
+    new (&filenames->end) Xapian::TermIterator ();
+
+    talloc_set_destructor (filenames, _notmuch_filenames_destructor);
+
+    filenames->iterator = notmuch->xapian_db->allterms_begin (prefix);
+    filenames->end = notmuch->xapian_db->allterms_end (prefix);
+
+    filenames->prefix_len = strlen (prefix);
+
+    filenames->filename = NULL;
+
+    return filenames;
+}
+
+notmuch_bool_t
+notmuch_filenames_valid (notmuch_filenames_t *filenames)
+{
+    if (filenames == NULL)
+       return NULL;
+
+    return (filenames->iterator != filenames->end);
+}
+
+const char *
+notmuch_filenames_get (notmuch_filenames_t *filenames)
+{
+    if (filenames == NULL || filenames->iterator == filenames->end)
+       return NULL;
+
+    if (filenames->filename == NULL) {
+       std::string term = *filenames->iterator;
+
+       filenames->filename = talloc_strdup (filenames,
+                                            term.c_str () +
+                                            filenames->prefix_len);
+    }
+
+    return filenames->filename;
+}
+
+void
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames)
+{
+    if (filenames == NULL)
+       return;
+
+    if (filenames->filename) {
+       talloc_free (filenames->filename);
+       filenames->filename = NULL;
+    }
+
+    if (filenames->iterator != filenames->end)
+       filenames->iterator++;
+}
+
+void
+notmuch_filenames_destroy (notmuch_filenames_t *filenames)
+{
+    if (filenames == NULL)
+       return;
+
+    talloc_free (filenames);
+}
+
+struct _notmuch_directory {
+    notmuch_database_t *notmuch;
+    Xapian::docid document_id;
+    Xapian::Document doc;
+    time_t mtime;
+};
+
+/* We end up having to call the destructor explicitly because we had
+ * to use "placement new" in order to initialize C++ objects within a
+ * block that we allocated with talloc. So C++ is making talloc
+ * slightly less simple to use, (we wouldn't need
+ * talloc_set_destructor at all otherwise).
+ */
+static int
+_notmuch_directory_destructor (notmuch_directory_t *directory)
+{
+    directory->doc.~Document ();
+
+    return 0;
+}
+
+static notmuch_private_status_t
+find_directory_document (notmuch_database_t *notmuch,
+                        const char *db_path,
+                        Xapian::Document *document)
+{
+    notmuch_private_status_t status;
+    Xapian::docid doc_id;
+
+    status = _notmuch_database_find_unique_doc_id (notmuch, "directory",
+                                                  db_path, &doc_id);
+    if (status) {
+       *document = Xapian::Document ();
+       return status;
+    }
+
+    *document = notmuch->xapian_db->get_document (doc_id);
+    return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+notmuch_directory_t *
+_notmuch_directory_create (notmuch_database_t *notmuch,
+                          const char *path,
+                          notmuch_status_t *status_ret)
+{
+    Xapian::WritableDatabase *db;
+    notmuch_directory_t *directory;
+    notmuch_private_status_t private_status;
+    const char *db_path;
+
+    *status_ret = NOTMUCH_STATUS_SUCCESS;
+
+    path = _notmuch_database_relative_path (notmuch, path);
+
+    if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
+       INTERNAL_ERROR ("Failure to ensure database is writable");
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    directory = talloc (notmuch, notmuch_directory_t);
+    if (unlikely (directory == NULL))
+       return NULL;
+
+    directory->notmuch = notmuch;
+
+    /* "placement new"---not actually allocating memory */
+    new (&directory->doc) Xapian::Document;
+
+    talloc_set_destructor (directory, _notmuch_directory_destructor);
+
+    db_path = _notmuch_database_get_directory_db_path (path);
+
+    try {
+       Xapian::TermIterator i, end;
+
+       private_status = find_directory_document (notmuch, db_path,
+                                                 &directory->doc);
+       directory->document_id = directory->doc.get_docid ();
+
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {
+           void *local = talloc_new (directory);
+           const char *parent, *basename;
+           Xapian::docid parent_id;
+           char *term = talloc_asprintf (local, "%s%s",
+                                         _find_prefix ("directory"), db_path);
+           directory->doc.add_term (term, 0);
+
+           directory->doc.set_data (path);
+
+           _notmuch_database_split_path (local, path, &parent, &basename);
+
+           _notmuch_database_find_directory_id (notmuch, parent, &parent_id);
+
+           if (basename) {
+               term = talloc_asprintf (local, "%s%u:%s",
+                                       _find_prefix ("directory-direntry"),
+                                       parent_id, basename);
+               directory->doc.add_term (term, 0);
+           }
+
+           directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                                     Xapian::sortable_serialise (0));
+
+           directory->document_id = db->add_document (directory->doc);
+           talloc_free (local);
+       }
+
+       directory->mtime = Xapian::sortable_unserialise (
+           directory->doc.get_value (NOTMUCH_VALUE_TIMESTAMP));
+    } catch (const Xapian::Error &error) {
+       fprintf (stderr,
+                "A Xapian exception occurred creating a directory: %s.\n",
+                error.get_msg().c_str());
+       notmuch->exception_reported = TRUE;
+       notmuch_directory_destroy (directory);
+       *status_ret = NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+       return NULL;
+    }
+
+    if (db_path != path)
+       free ((char *) db_path);
+
+    return directory;
+}
+
+unsigned int
+_notmuch_directory_get_document_id (notmuch_directory_t *directory)
+{
+    return directory->document_id;
+}
+
+notmuch_status_t
+notmuch_directory_set_mtime (notmuch_directory_t *directory,
+                            time_t mtime)
+{
+    notmuch_database_t *notmuch = directory->notmuch;
+    Xapian::WritableDatabase *db;
+    notmuch_status_t status;
+
+    status = _notmuch_database_ensure_writable (notmuch);
+    if (status)
+       return status;
+
+    db = static_cast <Xapian::WritableDatabase *> (notmuch->xapian_db);
+
+    try {
+       directory->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+                                  Xapian::sortable_serialise (mtime));
+
+       db->replace_document (directory->document_id, directory->doc);
+    } catch (const Xapian::Error &error) {
+       fprintf (stderr,
+                "A Xapian exception occurred setting directory mtime: %s.\n",
+                error.get_msg().c_str());
+       notmuch->exception_reported = TRUE;
+       return NOTMUCH_STATUS_XAPIAN_EXCEPTION;
+    }
+
+    return NOTMUCH_STATUS_SUCCESS;
+}
+
+time_t
+notmuch_directory_get_mtime (notmuch_directory_t *directory)
+{
+    return directory->mtime;
+}
+
+notmuch_filenames_t *
+notmuch_directory_get_child_files (notmuch_directory_t *directory)
+{
+    char *term;
+    notmuch_filenames_t *child_files;
+
+    term = talloc_asprintf (directory, "%s%u:",
+                           _find_prefix ("file-direntry"),
+                           directory->document_id);
+
+    child_files = _notmuch_filenames_create (directory,
+                                            directory->notmuch, term);
+
+    talloc_free (term);
+
+    return child_files;
+}
+
+notmuch_filenames_t *
+notmuch_directory_get_child_directories (notmuch_directory_t *directory)
+{
+    char *term;
+    notmuch_filenames_t *child_directories;
+
+    term = talloc_asprintf (directory, "%s%u:",
+                           _find_prefix ("directory-direntry"),
+                           directory->document_id);
+
+    child_directories = _notmuch_filenames_create (directory,
+                                                  directory->notmuch, term);
+
+    talloc_free (term);
+
+    return child_directories;
+}
+
+void
+notmuch_directory_destroy (notmuch_directory_t *directory)
+{
+    talloc_free (directory);
+}
diff --git a/lib/index.cc b/lib/index.cc
new file mode 100644 (file)
index 0000000..0d6640b
--- /dev/null
@@ -0,0 +1,478 @@
+/*
+ * Copyright © 2009 Carl Worth
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see http://www.gnu.org/licenses/ .
+ *
+ * Author: Carl Worth <cworth@cworth.org>
+ */
+
+#include "notmuch-private.h"
+
+#include <gmime/gmime.h>
+#include <gmime/gmime-filter.h>
+
+#include <xapian.h>
+
+/* Oh, how I wish that gobject didn't require so much noisy boilerplate!
+ * (Though I have at least eliminated some of the stock set...) */
+typedef struct _NotmuchFilterDiscardUuencode NotmuchFilterDiscardUuencode;
+typedef struct _NotmuchFilterDiscardUuencodeClass NotmuchFilterDiscardUuencodeClass;
+
+/**
+ * NotmuchFilterDiscardUuencode:
+ *
+ * @parent_object: parent #GMimeFilter
+ * @encode: encoding vs decoding
+ * @state: State of the parser
+ *
+ * A filter to discard uuencoded portions of an email.
+ *
+ * A uuencoded portion is identified as beginning with a line
+ * matching:
+ *
+ *     begin [0-7][0-7][0-7] .*
+ *
+ * After that detection, and beginning with the following line,
+ * characters will be discarded as long as the first character of each
+ * line begins with M and subsequent characters on the line are within
+ * the range of ASCII characters from ' ' to '`'.
+ *
+ * This is not a perfect UUencode filter. It's possible to have a
+ * message that will legitimately match that pattern, (so that some
+ * legitimate content is discarded). And for most UUencoded files, the
+ * final line of encoded data (the line not starting with M) will be
+ * indexed.
+ **/
+struct _NotmuchFilterDiscardUuencode {
+    GMimeFilter parent_object;
+    int state;
+};
+
+struct _NotmuchFilterDiscardUuencodeClass {
+    GMimeFilterClass parent_class;
+};
+
+GMimeFilter *notmuch_filter_discard_uuencode_new (void);
+
+static void notmuch_filter_discard_uuencode_finalize (GObject *object);
+
+static GMimeFilter *filter_copy (GMimeFilter *filter);
+static void filter_filter (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                          char **out, size_t *outlen, size_t *outprespace);
+static void filter_complete (GMimeFilter *filter, char *in, size_t len, size_t prespace,
+                            char **out, size_t *outlen, size_t *outprespace);
+static void filter_reset (GMimeFilter *filter);
+
+
+static GMimeFilterClass *parent_class = NULL;
+
+static void
+notmuch_filter_discard_uuencode_class_init (NotmuchFilterDiscardUuencodeClass *klass)
+{
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GMimeFilterClass *filter_class = GMIME_FILTER_CLASS (klass);
+
+    parent_class = (GMimeFilterClass *) g_type_class_ref (GMIME_TYPE_FILTER);
+
+    object_class->finalize = notmuch_filter_discard_uuencode_finalize;
+
+    filter_class->copy = filter_copy;
+    filter_class->filter = filter_filter;
+    filter_class->complete = filter_complete;
+    filter_class->reset = filter_reset;
+}
+
+static void
+notmuch_filter_discard_uuencode_finalize (GObject *object)
+{
+    G_OBJECT_CLASS (parent_class)->finalize (object);
+}
+
+static GMimeFilter *
+filter_copy (GMimeFilter *gmime_filter)
+{
+    (void) gmime_filter;
+    return notmuch_filter_discard_uuencode_new ();
+}
+
+static void
+filter_filter (GMimeFilter *gmime_filter, char *inbuf, size_t inlen, size_t prespace,
+              char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    NotmuchFilterDiscardUuencode *filter = (NotmuchFilterDiscardUuencode *) gmime_filter;
+    register const char *inptr = inbuf;
+    const char *inend = inbuf + inlen;
+    char *outptr;
+
+    (void) prespace;
+
+    /* Simple, linear state-transition diagram for our filter.
+     *
+     * If the character being processed is within the range of [a, b]
+     * for the current state then we transition next_if_match
+     * state. If not, we transition to the next_if_not_match state.
+     *
+     * The final two states are special in that they are the states in
+     * which we discard data. */
+    static const struct {
+       int state;
+       int a;
+       int b;
+       int next_if_match;
+       int next_if_not_match;
+    } states[] = {
+       {0,  'b',  'b',  1,  0},
+       {1,  'e',  'e',  2,  0},
+       {2,  'g',  'g',  3,  0},
+       {3,  'i',  'i',  4,  0},
+       {4,  'n',  'n',  5,  0},
+       {5,  ' ',  ' ',  6,  0},
+       {6,  '0',  '7',  7,  0},
+       {7,  '0',  '7',  8,  0},
+       {8,  '0',  '7',  9,  0},
+       {9,  ' ',  ' ',  10, 0},
+       {10, '\n', '\n', 11, 10},
+       {11, 'M',  'M',  12, 0},
+       {12, ' ',  '`',  12, 11}  
+    };
+    int next;
+
+    g_mime_filter_set_size (gmime_filter, inlen, FALSE);
+    outptr = gmime_filter->outbuf;
+
+    while (inptr < inend) {
+       if (*inptr >= states[filter->state].a &&
+           *inptr <= states[filter->state].b)
+       {
+           next = states[filter->state].next_if_match;
+       }
+       else
+       {
+           next = states[filter->state].next_if_not_match;
+       }
+
+       if (filter->state < 11)
+           *outptr++ = *inptr;
+
+       filter->state = next;
+       inptr++;
+    }
+
+    *outlen = outptr - gmime_filter->outbuf;
+    *outprespace = gmime_filter->outpre;
+    *outbuf = gmime_filter->outbuf;
+}
+
+static void
+filter_complete (GMimeFilter *filter, char *inbuf, size_t inlen, size_t prespace,
+                char **outbuf, size_t *outlen, size_t *outprespace)
+{
+    if (inbuf && inlen)
+       filter_filter (filter, inbuf, inlen, prespace, outbuf, outlen, outprespace);
+}
+
+static void
+filter_reset (GMimeFilter *gmime_filter)
+{
+    NotmuchFilterDiscardUuencode *filter = (NotmuchFilterDiscardUuencode *) gmime_filter;
+
+    filter->state = 0;
+}
+
+/**
+ * notmuch_filter_discard_uuencode_new:
+ *
+ * Returns: a new #NotmuchFilterDiscardUuencode filter.
+ **/
+GMimeFilter *
+notmuch_filter_discard_uuencode_new (void)
+{
+    static GType type = 0;
+    NotmuchFilterDiscardUuencode *filter;
+
+    if (!type) {
+       static const GTypeInfo info = {
+           sizeof (NotmuchFilterDiscardUuencodeClass),
+           NULL, /* base_class_init */
+           NULL, /* base_class_finalize */
+           (GClassInitFunc) notmuch_filter_discard_uuencode_class_init,
+           NULL, /* class_finalize */
+           NULL, /* class_data */
+           sizeof (NotmuchFilterDiscardUuencode),
+           0,    /* n_preallocs */
+           NULL, /* instance_init */
+           NULL  /* value_table */
+       };
+
+       type = g_type_register_static (GMIME_TYPE_FILTER, "NotmuchFilterDiscardUuencode", &info, (GTypeFlags) 0);
+    }
+
+    filter = (NotmuchFilterDiscardUuencode *) g_object_newv (type, 0, NULL);
+    filter->state = 0;
+
+    return (GMimeFilter *) filter;
+}
+
+/* We're finally down to a single (NAME + address) email "mailbox". */
+static void
+_index_address_mailbox (notmuch_message_t *message,
+                       const char *prefix_name,
+                       InternetAddress *address)
+{
+    InternetAddressMailbox *mailbox = INTERNET_ADDRESS_MAILBOX (address);
+    const char *name, *addr;
+    void *local = talloc_new (message);
+
+    name = internet_address_get_name (address);
+    addr = internet_address_mailbox_get_addr (mailbox);
+
+    /* In the absence of a name, we'll strip the part before the @
+     * from the address. */
+    if (! name) {
+       const char *at;
+
+       at = strchr (addr, '@');
+       if (at)
+           name = talloc_strndup (local, addr, at - addr);
+    }
+
+    if (name)
+       _notmuch_message_gen_terms (message, prefix_name, name);
+    if (addr)
+       _notmuch_message_gen_terms (message, prefix_name, addr);
+
+    talloc_free (local);
+}
+
+static void
+_index_address_list (notmuch_message_t *message,
+                    const char *prefix_name,
+                    InternetAddressList *addresses);
+
+/* The outer loop over the InternetAddressList wasn't quite enough.
+ * There can actually be a tree here where a single member of the list
+ * is a "group" containing another list. Recurse please.
+ */
+static void
+_index_address_group (notmuch_message_t *message,
+                     const char *prefix_name,
+                     InternetAddress *address)
+{
+    InternetAddressGroup *group;
+    InternetAddressList *list;
+
+    group = INTERNET_ADDRESS_GROUP (address);
+    list = internet_address_group_get_members (group);
+
+    if (! list)
+       return;
+
+    _index_address_list (message, prefix_name, list);
+}
+
+static void
+_index_address_list (notmuch_message_t *message,
+                    const char *prefix_name,
+                    InternetAddressList *addresses)
+{
+    int i;
+    InternetAddress *address;
+
+    if (addresses == NULL)
+       return;
+
+    for (i = 0; i < internet_address_list_length (addresses); i++) {
+       address = internet_address_list_get_address (addresses, i);
+       if (INTERNET_ADDRESS_IS_MAILBOX (address)) {
+           _index_address_mailbox (message, prefix_name, address);
+       } else if (INTERNET_ADDRESS_IS_GROUP (address)) {
+           _index_address_group (message, prefix_name, address);
+       } else {
+           INTERNAL_ERROR ("GMime InternetAddress is neither a mailbox nor a group.\n");
+       }
+    }
+}
+
+static const char *
+skip_re_in_subject (const char *subject)
+{
+    const char *s = subject;
+
+    if (subject == NULL)
+       return NULL;
+
+    while (*s) {
+       while (*s && isspace (*s))
+           s++;
+       if (strncasecmp (s, "re:", 3) == 0)
+           s += 3;
+       else
+           break;
+    }
+
+    return s;
+}
+
+/* Callback to generate terms for each mime part of a message. */
+static void
+_index_mime_part (notmuch_message_t *message,
+                 GMimeObject *part)
+{
+    GMimeStream *stream, *filter;
+    GMimeFilter *discard_uuencode_filter;
+    GMimeDataWrapper *wrapper;
+    GByteArray *byte_array;
+    GMimeContentDisposition *disposition;
+    char *body;
+
+    if (! part) {
+       fprintf (stderr, "Warning: Not indexing empty mime part.\n");
+       return;
+    }
+
+    if (GMIME_IS_MULTIPART (part)) {
+       GMimeMultipart *multipart = GMIME_MULTIPART (part);
+       int i;
+
+       for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
+           if (GMIME_IS_MULTIPART_SIGNED (multipart)) {
+               /* Don't index the signature. */
+               if (i == 1)
+                   continue;
+               if (i > 1)
+                   fprintf (stderr, "Warning: Unexpected extra parts of multipart/signed. Indexing anyway.\n");
+           }
+           _index_mime_part (message,
+                             g_mime_multipart_get_part (multipart, i));
+       }
+       return;
+    }
+
+    if (GMIME_IS_MESSAGE_PART (part)) {
+       GMimeMessage *mime_message;
+
+       mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+
+       _index_mime_part (message, g_mime_message_get_mime_part (mime_message));
+
+       return;
+    }
+
+    if (! (GMIME_IS_PART (part))) {
+       fprintf (stderr, "Warning: Not indexing unknown mime part: %s.\n",
+                g_type_name (G_OBJECT_TYPE (part)));
+       return;
+    }
+
+    disposition = g_mime_object_get_content_disposition (part);
+    if (disposition &&
+       strcmp (disposition->disposition, GMIME_DISPOSITION_ATTACHMENT) == 0)
+    {
+       const char *filename = g_mime_part_get_filename (GMIME_PART (part));
+
+       _notmuch_message_add_term (message, "tag", "attachment");
+       _notmuch_message_gen_terms (message, "attachment", filename);
+
+       /* XXX: Would be nice to call out to something here to parse
+        * the attachment into text and then index that. */
+       return;
+    }
+
+    byte_array = g_byte_array_new ();
+
+    stream = g_mime_stream_mem_new_with_byte_array (byte_array);
+    g_mime_stream_mem_set_owner (GMIME_STREAM_MEM (stream), FALSE);
+
+    filter = g_mime_stream_filter_new (stream);
+    discard_uuencode_filter = notmuch_filter_discard_uuencode_new ();
+
+    g_mime_stream_filter_add (GMIME_STREAM_FILTER (filter),
+                             discard_uuencode_filter);
+
+    wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+    if (wrapper)
+       g_mime_data_wrapper_write_to_stream (wrapper, filter);
+
+    g_object_unref (stream);
+    g_object_unref (filter);
+    g_object_unref (discard_uuencode_filter);
+
+    g_byte_array_append (byte_array, (guint8 *) "\0", 1);
+    body = (char *) g_byte_array_free (byte_array, FALSE);
+
+    if (body) {
+       _notmuch_message_gen_terms (message, NULL, body);
+
+       free (body);
+    }
+}
+
+notmuch_status_t
+_notmuch_message_index_file (notmuch_message_t *message,
+                            const char *filename)
+{
+    GMimeStream *stream = NULL;
+    GMimeParser *parser = NULL;
+    GMimeMessage *mime_message = NULL;
+    InternetAddressList *addresses;
+    FILE *file = NULL;
+    const char *from, *subject;
+    notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+    static int initialized = 0;
+
+    if (! initialized) {
+       g_mime_init (0);
+       initialized = 1;
+    }
+
+    file = fopen (filename, "r");
+    if (! file) {
+       fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+       ret = NOTMUCH_STATUS_FILE_ERROR;
+       goto DONE;
+    }
+
+    /* Evil GMime steals my FILE* here so I won't fclose it. */
+    stream = g_mime_stream_file_new (file);
+
+    parser = g_mime_parser_new_with_stream (stream);
+
+    mime_message = g_mime_parser_construct_message (parser);
+
+    from = g_mime_message_get_sender (mime_message);
+    addresses = internet_address_list_parse_string (from);
+
+    _index_address_list (message, "from", addresses);
+
+    addresses = g_mime_message_get_all_recipients (mime_message);
+    _index_address_list (message, "to", addresses);
+
+    subject = g_mime_message_get_subject (mime_message);
+    subject = skip_re_in_subject (subject);
+    _notmuch_message_gen_terms (message, "subject", subject);
+
+    _index_mime_part (message, g_mime_message_get_mime_part (mime_message));
+
+  DONE:
+    if (mime_message)
+       g_object_unref (mime_message);
+
+    if (parser)
+       g_object_unref (parser);
+
+    if (stream)
+       g_object_unref (stream);
+
+    return ret;
+}
diff --git a/lib/libsha1.c b/lib/libsha1.c
new file mode 100644 (file)
index 0000000..c39a5a1
--- /dev/null
@@ -0,0 +1,242 @@
+/*
+ ---------------------------------------------------------------------------
+ Copyright (c) 2002, Dr Brian Gladman, Worcester, UK.   All rights reserved.
+
+ LICENSE TERMS
+
+ The free distribution and use of this software in both source and binary
+ form is allowed (with or without changes) provided that:
+
+   1. distributions of this source code include the above copyright
+      notice, this list of conditions and the following disclaimer;
+
+   2. distributions in binary form include the above copyright
+      notice, this list of conditions and the following disclaimer
+      in the documentation and/or other associated materials;
+
+   3. the copyright holder's name is not used to endorse products
+      built using this software without specific written permission.
+
+ ALTERNATIVELY, provided that this notice is retained in full, this product
+ may be distributed under the terms of the GNU General Public License (GPL),
+ in which case the provisions of the GPL apply INSTEAD OF those given above.
+
+ DISCLAIMER
+
+ This software is provided 'as is' with no explicit or implied warranties
+ in respect of its properties, including, but not limited to, correctness
+ and/or fitness for purpose.
+ ---------------------------------------------------------------------------
+ Issue Date: 01/08/2005
+
+ This is a byte oriented version of SHA1 that operates on arrays of bytes
+ stored in memory.
+*/
+
+#include <string.h>     /* for memcpy() etc.        */
+
+#include "libsha1.h"
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif
+
+#define SHA1_BLOCK_SIZE  64
+
+#define rotl32(x,n)   (((x) << n) | ((x) >> (32 - n)))
+#define rotr32(x,n)   (((x) >> n) | ((x) << (32 - n)))
+
+#define bswap_32(x) ((rotr32((x), 24) & 0x00ff00ff) | (rotr32((x), 8) & 0xff00ff00))
+
+#if (PLATFORM_BYTE_ORDER == IS_LITTLE_ENDIAN)
+#define bsw_32(p,n) \
+    { int _i = (n); while(_i--) ((uint32_t*)p)[_i] = bswap_32(((uint32_t*)p)[_i]); }
+#else
+#define bsw_32(p,n)
+#endif
+
+#define SHA1_MASK   (SHA1_BLOCK_SIZE - 1)
+
+#if 0
+
+#define ch(x,y,z)       (((x) & (y)) ^ (~(x) & (z)))
+#define parity(x,y,z)   ((x) ^ (y) ^ (z))
+#define maj(x,y,z)      (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+
+#else   /* Discovered by Rich Schroeppel and Colin Plumb   */
+
+#define ch(x,y,z)       ((z) ^ ((x) & ((y) ^ (z))))
+#define parity(x,y,z)   ((x) ^ (y) ^ (z))
+#define maj(x,y,z)      (((x) & (y)) | ((z) & ((x) ^ (y))))
+
+#endif
+
+/* Compile 64 bytes of hash data into SHA1 context. Note    */
+/* that this routine assumes that the byte order in the     */
+/* ctx->wbuf[] at this point is in such an order that low   */
+/* address bytes in the ORIGINAL byte stream will go in     */
+/* this buffer to the high end of 32-bit words on BOTH big  */
+/* and little endian systems                                */
+
+#ifdef ARRAY
+#define q(v,n)  v[n]
+#else
+#define q(v,n)  v##n
+#endif
+
+#define one_cycle(v,a,b,c,d,e,f,k,h)            \
+    q(v,e) += rotr32(q(v,a),27) +               \
+              f(q(v,b),q(v,c),q(v,d)) + k + h;  \
+    q(v,b)  = rotr32(q(v,b), 2)
+
+#define five_cycle(v,f,k,i)                 \
+    one_cycle(v, 0,1,2,3,4, f,k,hf(i  ));   \
+    one_cycle(v, 4,0,1,2,3, f,k,hf(i+1));   \
+    one_cycle(v, 3,4,0,1,2, f,k,hf(i+2));   \
+    one_cycle(v, 2,3,4,0,1, f,k,hf(i+3));   \
+    one_cycle(v, 1,2,3,4,0, f,k,hf(i+4))
+
+static void sha1_compile(sha1_ctx ctx[1])
+{   uint32_t    *w = ctx->wbuf;
+
+#ifdef ARRAY
+    uint32_t    v[5];
+    memcpy(v, ctx->hash, 5 * sizeof(uint32_t));
+#else
+    uint32_t    v0, v1, v2, v3, v4;
+    v0 = ctx->hash[0]; v1 = ctx->hash[1];
+    v2 = ctx->hash[2]; v3 = ctx->hash[3];
+    v4 = ctx->hash[4];
+#endif
+
+#define hf(i)   w[i]
+
+    five_cycle(v, ch, 0x5a827999,  0);
+    five_cycle(v, ch, 0x5a827999,  5);
+    five_cycle(v, ch, 0x5a827999, 10);
+    one_cycle(v,0,1,2,3,4, ch, 0x5a827999, hf(15)); \
+
+#undef  hf
+#define hf(i) (w[(i) & 15] = rotl32(                    \
+                 w[((i) + 13) & 15] ^ w[((i) + 8) & 15] \
+               ^ w[((i) +  2) & 15] ^ w[(i) & 15], 1))
+
+    one_cycle(v,4,0,1,2,3, ch, 0x5a827999, hf(16));
+    one_cycle(v,3,4,0,1,2, ch, 0x5a827999, hf(17));
+    one_cycle(v,2,3,4,0,1, ch, 0x5a827999, hf(18));
+    one_cycle(v,1,2,3,4,0, ch, 0x5a827999, hf(19));
+
+    five_cycle(v, parity, 0x6ed9eba1,  20);
+    five_cycle(v, parity, 0x6ed9eba1,  25);
+    five_cycle(v, parity, 0x6ed9eba1,  30);
+    five_cycle(v, parity, 0x6ed9eba1,  35);
+
+    five_cycle(v, maj, 0x8f1bbcdc,  40);
+    five_cycle(v, maj, 0x8f1bbcdc,  45);
+    five_cycle(v, maj, 0x8f1bbcdc,  50);
+    five_cycle(v, maj, 0x8f1bbcdc,  55);
+
+    five_cycle(v, parity, 0xca62c1d6,  60);
+    five_cycle(v, parity, 0xca62c1d6,  65);
+    five_cycle(v, parity, 0xca62c1d6,  70);
+    five_cycle(v, parity, 0xca62c1d6,  75);
+
+#ifdef ARRAY
+    ctx->hash[0] += v[0]; ctx->hash[1] += v[1];
+    ctx->hash[2] += v[2]; ctx->hash[3] += v[3];
+    ctx->hash[4] += v[4];
+#else
+    ctx->hash[0] += v0; ctx->hash[1] += v1;
+    ctx->hash[2] += v2; ctx->hash[3] += v3;
+    ctx->hash[4] += v4;
+#endif
+}
+
+void sha1_begin(sha1_ctx ctx[1])
+{
+    ctx->count[0] = ctx->count[1] = 0;
+    ctx->hash[0] = 0x67452301;
+    ctx->hash[1] = 0xefcdab89;
+    ctx->hash[2] = 0x98badcfe;
+    ctx->hash[3] = 0x10325476;
+    ctx->hash[4] = 0xc3d2e1f0;
+}
+
+/* SHA1 hash data in an array of bytes into hash buffer and */
+/* call the hash_compile function as required.              */
+
+void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1])
+{   uint32_t pos = (uint32_t)(ctx->count[0] & SHA1_MASK),
+            space = SHA1_BLOCK_SIZE - pos;
+    const unsigned char *sp = data;
+
+    if((ctx->count[0] += len) < len)
+        ++(ctx->count[1]);
+
+    while(len >= space)     /* tranfer whole blocks if possible  */
+    {
+        memcpy(((unsigned char*)ctx->wbuf) + pos, sp, space);
+        sp += space; len -= space; space = SHA1_BLOCK_SIZE; pos = 0;
+        bsw_32(ctx->wbuf, SHA1_BLOCK_SIZ