summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore15
-rw-r--r--AUTHORS30
-rw-r--r--COPYING15
-rw-r--r--COPYING-GPL-3676
-rw-r--r--INSTALL86
-rw-r--r--Makefile34
-rw-r--r--Makefile.local289
-rw-r--r--NEWS150
-rw-r--r--README71
-rw-r--r--RELEASING105
-rw-r--r--TODO293
-rw-r--r--compat/Makefile5
-rw-r--r--compat/Makefile.local14
-rw-r--r--compat/README16
-rw-r--r--compat/compat.h45
-rw-r--r--compat/getdelim.c133
-rw-r--r--compat/getline.c29
-rw-r--r--compat/have_getline.c13
-rw-r--r--compat/have_strcasestr.c10
-rw-r--r--compat/strcasestr.c40
-rw-r--r--completion/Makefile7
-rw-r--r--completion/Makefile.local18
-rw-r--r--completion/README10
-rw-r--r--completion/notmuch-completion.bash71
-rw-r--r--completion/notmuch-completion.tcsh2
-rw-r--r--completion/notmuch-completion.zsh74
-rwxr-xr-xconfigure452
-rw-r--r--debian/.gitignore3
-rw-r--r--debian/README.Debian6
-rw-r--r--debian/changelog52
-rw-r--r--debian/compat1
-rw-r--r--debian/control51
-rw-r--r--debian/copyright55
-rw-r--r--debian/gbp.conf14
-rw-r--r--debian/libnotmuch-dev.install2
-rw-r--r--debian/libnotmuch1.install1
-rw-r--r--debian/notmuch.dirs6
-rwxr-xr-xdebian/notmuch.emacsen-install47
-rwxr-xr-xdebian/notmuch.emacsen-remove10
-rw-r--r--debian/notmuch.emacsen-startup23
-rw-r--r--debian/notmuch.install5
-rwxr-xr-xdebian/rules17
-rw-r--r--debian/source/format1
-rw-r--r--debugger.c47
-rw-r--r--emacs/Makefile7
-rw-r--r--emacs/Makefile.local30
-rw-r--r--emacs/notmuch-lib.el42
-rw-r--r--emacs/notmuch-query.el81
-rw-r--r--emacs/notmuch-show.el1002
-rw-r--r--emacs/notmuch-wash.el150
-rw-r--r--emacs/notmuch.el932
-rw-r--r--gmime-filter-headers.c263
-rw-r--r--gmime-filter-headers.h69
-rw-r--r--gmime-filter-reply.c209
-rw-r--r--gmime-filter-reply.h66
-rw-r--r--json.c109
-rw-r--r--lib/Makefile7
-rw-r--r--lib/Makefile.local85
-rw-r--r--lib/database-private.h67
-rw-r--r--lib/database.cc1709
-rw-r--r--lib/directory.cc338
-rw-r--r--lib/index.cc478
-rw-r--r--lib/libsha1.c242
-rw-r--r--lib/libsha1.h67
-rw-r--r--lib/message-file.c363
-rw-r--r--lib/message.cc797
-rw-r--r--lib/messages.c182
-rw-r--r--lib/notmuch-private.h421
-rw-r--r--lib/notmuch.h1112
-rw-r--r--lib/query.cc372
-rw-r--r--lib/sha1.c115
-rw-r--r--lib/tags.c120
-rw-r--r--lib/thread.cc427
-rw-r--r--lib/xutil.c134
-rw-r--r--lib/xutil.h51
-rw-r--r--notmuch-client.h189
-rw-r--r--notmuch-config.c457
-rw-r--r--notmuch-count.c110
-rw-r--r--notmuch-dump.c95
-rw-r--r--notmuch-new.c865
-rw-r--r--notmuch-reply.c569
-rw-r--r--notmuch-restore.c152
-rw-r--r--notmuch-search-tags.c98
-rw-r--r--notmuch-search.c276
-rw-r--r--notmuch-setup.c169
-rw-r--r--notmuch-show.c579
-rw-r--r--notmuch-tag.c137
-rw-r--r--notmuch-time.c137
-rw-r--r--notmuch.1511
-rw-r--r--notmuch.c494
-rw-r--r--notmuch.desktop7
-rw-r--r--packaging/debian2
-rw-r--r--packaging/fedora/notmuch.spec110
-rw-r--r--query-string.c56
-rw-r--r--show-message.c196
-rwxr-xr-xtest/notmuch-test707
-rw-r--r--version1
-rw-r--r--vim/Makefile24
-rw-r--r--vim/README90
-rw-r--r--vim/notmuch.yaml8
-rw-r--r--vim/plugin/notmuch.vim1438
-rw-r--r--vim/syntax/notmuch-compose.vim7
-rw-r--r--vim/syntax/notmuch-folders.vim12
-rw-r--r--vim/syntax/notmuch-search.vim24
-rw-r--r--vim/syntax/notmuch-show.vim25
105 files changed, 20636 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..217440d
--- /dev/null
+++ b/.gitignore
@@ -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
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
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
index 0000000..4432540
--- /dev/null
+++ b/COPYING-GPL-3
@@ -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
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
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
index 0000000..5bb570b
--- /dev/null
+++ b/Makefile.local
@@ -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
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
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
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
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
index 0000000..fa25832
--- /dev/null
+++ b/compat/Makefile
@@ -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
index 0000000..653fc71
--- /dev/null
+++ b/compat/Makefile.local
@@ -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
index 0000000..cd32c56
--- /dev/null
+++ b/compat/README
@@ -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
index 0000000..173ef68
--- /dev/null
+++ b/compat/compat.h
@@ -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
index 0000000..407f3d0
--- /dev/null
+++ b/compat/getdelim.c
@@ -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
index 0000000..222e0f6
--- /dev/null
+++ b/compat/getline.c
@@ -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
index 0000000..a8bcd17
--- /dev/null
+++ b/compat/have_getline.c
@@ -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
index 0000000..c0fb762
--- /dev/null
+++ b/compat/have_strcasestr.c
@@ -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
index 0000000..62a3a54
--- /dev/null
+++ b/compat/strcasestr.c
@@ -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
index 0000000..b6859ea
--- /dev/null
+++ b/completion/Makefile
@@ -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
index 0000000..6a6012d
--- /dev/null
+++ b/completion/Makefile.local
@@ -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
index 0000000..40a30e5
--- /dev/null
+++ b/completion/README
@@ -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
index 0000000..8665268
--- /dev/null
+++ b/completion/notmuch-completion.bash
@@ -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
index 0000000..c0d3a44
--- /dev/null
+++ b/completion/notmuch-completion.tcsh
@@ -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
index 0000000..67a9aba
--- /dev/null
+++ b/completion/notmuch-completion.zsh
@@ -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
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
index 0000000..801ca02
--- /dev/null
+++ b/debian/.gitignore
@@ -0,0 +1,3 @@
+*.debhelper
+*.debhelper.log
+*.substvars
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644
index 0000000..f07b499
--- /dev/null
+++ b/debian/README.Debian
@@ -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
index 0000000..42a9f6d
--- /dev/null
+++ b/debian/changelog
@@ -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
index 0000000..7f8f011
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+7
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..e4c61ab
--- /dev/null
+++ b/debian/control
@@ -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
index 0000000..1cc7a15
--- /dev/null
+++ b/debian/copyright
@@ -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
index 0000000..dba526f
--- /dev/null
+++ b/debian/gbp.conf
@@ -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
index 0000000..185dba4
--- /dev/null
+++ b/debian/libnotmuch-dev.install
@@ -0,0 +1,2 @@
+usr/include
+usr/lib/libnotmuch.so
diff --git a/debian/libnotmuch1.install b/debian/libnotmuch1.install
new file mode 100644
index 0000000..da4fc25
--- /dev/null
+++ b/debian/libnotmuch1.install
@@ -0,0 +1 @@
+usr/lib/libnotmuch.so.*
diff --git a/debian/notmuch.dirs b/debian/notmuch.dirs
new file mode 100644
index 0000000..ce7f239
--- /dev/null
+++ b/debian/notmuch.dirs
@@ -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
index 0000000..98ec128
--- /dev/null
+++ b/debian/notmuch.emacsen-install
@@ -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
index 0000000..c35e8a4
--- /dev/null
+++ b/debian/notmuch.emacsen-remove
@@ -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
index 0000000..25a527f
--- /dev/null
+++ b/debian/notmuch.emacsen-startup
@@ -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
index 0000000..dd310fa
--- /dev/null
+++ b/debian/notmuch.install
@@ -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
index 0000000..a4b526c
--- /dev/null
+++ b/debian/rules
@@ -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
index 0000000..89ae9db
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/debugger.c b/debugger.c
new file mode 100644
index 0000000..e8b9378
--- /dev/null
+++ b/debugger.c
@@ -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
index 0000000..b6859ea
--- /dev/null
+++ b/emacs/Makefile
@@ -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
index 0000000..f759c0d
--- /dev/null
+++ b/emacs/Makefile.local
@@ -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
index 0000000..cb9be30
--- /dev/null
+++ b/emacs/notmuch-lib.el
@@ -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
index 0000000..0d6e775
--- /dev/null
+++ b/emacs/notmuch-query.el
@@ -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
index 0000000..916b39e
--- /dev/null
+++ b/emacs/notmuch-show.el
@@ -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)))
+
+;;
+
+(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
index 0000000..54a380a
--- /dev/null
+++ b/emacs/notmuch-wash.el
@@ -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
index 0000000..57b7fcf
--- /dev/null
+++ b/emacs/notmuch.el
@@ -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
index 0000000..2f3df80
--- /dev/null
+++ b/gmime-filter-headers.c
@@ -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
index 0000000..47d1d45
--- /dev/null
+++ b/gmime-filter-headers.h
@@ -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
index 0000000..b269db4
--- /dev/null
+++ b/gmime-filter-reply.c
@@ -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
index 0000000..b7cbc6b
--- /dev/null
+++ b/gmime-filter-reply.h
@@ -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
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
index 0000000..b6859ea
--- /dev/null
+++ b/lib/Makefile
@@ -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
index 0000000..a092090
--- /dev/null
+++ b/lib/Makefile.local
@@ -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
index 0000000..41918d7
--- /dev/null
+++ b/lib/database-private.h
@@ -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
index 0000000..6842faf
--- /dev/null
+++ b/lib/database.cc
@@ -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
index 0000000..5e75b73
--- /dev/null
+++ b/lib/directory.cc
@@ -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
index 0000000..0d6640b
--- /dev/null
+++ b/lib/index.cc
@@ -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
index 0000000..c39a5a1
--- /dev/null
+++ b/lib/libsha1.c
@@ -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_SIZE >> 2);
+ sha1_compile(ctx);
+ }
+
+ memcpy(((unsigned char*)ctx->wbuf) + pos, sp, len);
+}
+
+/* SHA1 final padding and digest calculation */
+
+void sha1_end(unsigned char hval[], sha1_ctx ctx[1])
+{ uint32_t i = (uint32_t)(ctx->count[0] & SHA1_MASK);
+
+ /* put bytes in the buffer in an order in which references to */
+ /* 32-bit words will put bytes with lower addresses into the */
+ /* top of 32 bit words on BOTH big and little endian machines */
+ bsw_32(ctx->wbuf, (i + 3) >> 2);
+
+ /* we now need to mask valid bytes and add the padding which is */
+ /* a single 1 bit and as many zero bits as necessary. Note that */
+ /* we can always add the first padding byte here because the */
+ /* buffer always has at least one empty slot */
+ ctx->wbuf[i >> 2] &= 0xffffff80 << 8 * (~i & 3);
+ ctx->wbuf[i >> 2] |= 0x00000080 << 8 * (~i & 3);
+
+ /* we need 9 or more empty positions, one for the padding byte */
+ /* (above) and eight for the length count. If there is not */
+ /* enough space, pad and empty the buffer */
+ if(i > SHA1_BLOCK_SIZE - 9)
+ {
+ if(i < 60) ctx->wbuf[15] = 0;
+ sha1_compile(ctx);
+ i = 0;
+ }
+ else /* compute a word index for the empty buffer positions */
+ i = (i >> 2) + 1;
+
+ while(i < 14) /* and zero pad all but last two positions */
+ ctx->wbuf[i++] = 0;
+
+ /* the following 32-bit length fields are assembled in the */
+ /* wrong byte order on little endian machines but this is */
+ /* corrected later since they are only ever used as 32-bit */
+ /* word values. */
+ ctx->wbuf[14] = (ctx->count[1] << 3) | (ctx->count[0] >> 29);
+ ctx->wbuf[15] = ctx->count[0] << 3;
+ sha1_compile(ctx);
+
+ /* extract the hash value as bytes in case the hash buffer is */
+ /* misaligned for 32-bit words */
+ for(i = 0; i < SHA1_DIGEST_SIZE; ++i)
+ hval[i] = (unsigned char)(ctx->hash[i >> 2] >> (8 * (~i & 3)));
+}
+
+void sha1(unsigned char hval[], const unsigned char data[], unsigned long len)
+{ sha1_ctx cx[1];
+
+ sha1_begin(cx); sha1_hash(data, len, cx); sha1_end(hval, cx);
+}
+
+#if defined(__cplusplus)
+}
+#endif
diff --git a/lib/libsha1.h b/lib/libsha1.h
new file mode 100644
index 0000000..b4dca93
--- /dev/null
+++ b/lib/libsha1.h
@@ -0,0 +1,67 @@
+/*
+ ---------------------------------------------------------------------------
+ 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
+*/
+
+#ifndef _SHA1_H
+#define _SHA1_H
+
+#if defined(__cplusplus)
+extern "C"
+{
+#endif
+#if 0
+} /* Appleasing Emacs */
+#endif
+
+#include <stdint.h>
+
+/* Size of SHA1 digest */
+
+#define SHA1_DIGEST_SIZE 20
+
+/* type to hold the SHA1 context */
+
+typedef struct
+{ uint32_t count[2];
+ uint32_t hash[5];
+ uint32_t wbuf[16];
+} sha1_ctx;
+
+void sha1_begin(sha1_ctx ctx[1]);
+void sha1_hash(const unsigned char data[], unsigned long len, sha1_ctx ctx[1]);
+void sha1_end(unsigned char hval[], sha1_ctx ctx[1]);
+void sha1(unsigned char hval[], const unsigned char data[], unsigned long len);
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif
diff --git a/lib/message-file.c b/lib/message-file.c
new file mode 100644
index 0000000..0c152a3
--- /dev/null
+++ b/lib/message-file.c
@@ -0,0 +1,363 @@
+/* message.c - Utility functions for parsing an email message 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>
+ */
+
+#include <stdarg.h>
+
+#include "notmuch-private.h"
+
+#include <gmime/gmime.h>
+
+#include <glib.h> /* GHashTable */
+
+typedef struct {
+ char *str;
+ size_t size;
+ size_t len;
+} header_value_closure_t;
+
+struct _notmuch_message_file {
+ /* File object */
+ FILE *file;
+
+ /* Header storage */
+ int restrict_headers;
+ GHashTable *headers;
+ int broken_headers;
+ int good_headers;
+ size_t header_size; /* Length of full message header in bytes. */
+
+ /* Parsing state */
+ char *line;
+ size_t line_size;
+ header_value_closure_t value;
+
+ int parsing_started;
+ int parsing_finished;
+};
+
+static int
+strcase_equal (const void *a, const void *b)
+{
+ return strcasecmp (a, b) == 0;
+}
+
+static unsigned int
+strcase_hash (const void *ptr)
+{
+ const char *s = ptr;
+
+ /* This is the djb2 hash. */
+ unsigned int hash = 5381;
+ while (s && *s) {
+ hash = ((hash << 5) + hash) + tolower (*s);
+ s++;
+ }
+
+ return hash;
+}
+
+static int
+_notmuch_message_file_destructor (notmuch_message_file_t *message)
+{
+ if (message->line)
+ free (message->line);
+
+ if (message->value.size)
+ free (message->value.str);
+
+ if (message->headers)
+ g_hash_table_destroy (message->headers);
+
+ if (message->file)
+ fclose (message->file);
+
+ return 0;
+}
+
+/* Create a new notmuch_message_file_t for 'filename' with 'ctx' as
+ * the talloc owner. */
+notmuch_message_file_t *
+_notmuch_message_file_open_ctx (void *ctx, const char *filename)
+{
+ notmuch_message_file_t *message;
+
+ message = talloc_zero (ctx, notmuch_message_file_t);
+ if (unlikely (message == NULL))
+ return NULL;
+
+ talloc_set_destructor (message, _notmuch_message_file_destructor);
+
+ message->file = fopen (filename, "r");
+ if (message->file == NULL)
+ goto FAIL;
+
+ message->headers = g_hash_table_new_full (strcase_hash,
+ strcase_equal,
+ free,
+ free);
+
+ message->parsing_started = 0;
+ message->parsing_finished = 0;
+
+ return message;
+
+ FAIL:
+ fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+ notmuch_message_file_close (message);
+
+ return NULL;
+}
+
+notmuch_message_file_t *
+notmuch_message_file_open (const char *filename)
+{
+ return _notmuch_message_file_open_ctx (NULL, filename);
+}
+
+void
+notmuch_message_file_close (notmuch_message_file_t *message)
+{
+ talloc_free (message);
+}
+
+void
+notmuch_message_file_restrict_headersv (notmuch_message_file_t *message,
+ va_list va_headers)
+{
+ char *header;
+
+ if (message->parsing_started)
+ INTERNAL_ERROR ("notmuch_message_file_restrict_headers called after parsing has started");
+
+ while (1) {
+ header = va_arg (va_headers, char*);
+ if (header == NULL)
+ break;
+ g_hash_table_insert (message->headers,
+ xstrdup (header), NULL);
+ }
+
+ message->restrict_headers = 1;
+}
+
+void
+notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...)
+{
+ va_list va_headers;
+
+ va_start (va_headers, message);
+
+ notmuch_message_file_restrict_headersv (message, va_headers);
+}
+
+static void
+copy_header_unfolding (header_value_closure_t *value,
+ const char *chunk)
+{
+ char *last;
+
+ if (chunk == NULL)
+ return;
+
+ while (*chunk == ' ' || *chunk == '\t')
+ chunk++;
+
+ if (value->len + 1 + strlen (chunk) + 1 > value->size) {
+ unsigned int new_size = value->size;
+ if (value->size == 0)
+ new_size = strlen (chunk) + 1;
+ else
+ while (value->len + 1 + strlen (chunk) + 1 > new_size)
+ new_size *= 2;
+ value->str = xrealloc (value->str, new_size);
+ value->size = new_size;
+ }
+
+ last = value->str + value->len;
+ if (value->len) {
+ *last = ' ';
+ last++;
+ value->len++;
+ }
+
+ strcpy (last, chunk);
+ value->len += strlen (chunk);
+
+ last = value->str + value->len - 1;
+ if (*last == '\n') {
+ *last = '\0';
+ value->len--;
+ }
+}
+
+/* As a special-case, a value of NULL for header_desired will force
+ * the entire header to be parsed if it is not parsed already. This is
+ * used by the _notmuch_message_file_get_headers_end function. */
+const char *
+notmuch_message_file_get_header (notmuch_message_file_t *message,
+ const char *header_desired)
+{
+ int contains;
+ char *header, *decoded_value;
+ const char *s, *colon;
+ int match;
+ static int initialized = 0;
+
+ if (! initialized) {
+ g_mime_init (0);
+ initialized = 1;
+ }
+
+ message->parsing_started = 1;
+
+ if (header_desired == NULL)
+ contains = 0;
+ else
+ contains = g_hash_table_lookup_extended (message->headers,
+ header_desired, NULL,
+ (gpointer *) &decoded_value);
+
+ if (contains && decoded_value)
+ return decoded_value;
+
+ if (message->parsing_finished)
+ return "";
+
+#define NEXT_HEADER_LINE(closure) \
+ while (1) { \
+ ssize_t bytes_read = getline (&message->line, \
+ &message->line_size, \
+ message->file); \
+ if (bytes_read == -1) { \
+ message->parsing_finished = 1; \
+ break; \
+ } \
+ if (*message->line == '\n') { \
+ message->parsing_finished = 1; \
+ break; \
+ } \
+ if (closure && \
+ (*message->line == ' ' || *message->line == '\t')) \
+ { \
+ copy_header_unfolding ((closure), message->line); \
+ } \
+ if (*message->line == ' ' || *message->line == '\t') \
+ message->header_size += strlen (message->line); \
+ else \
+ break; \
+ }
+
+ if (message->line == NULL)
+ NEXT_HEADER_LINE (NULL);
+
+ while (1) {
+
+ if (message->parsing_finished)
+ break;
+
+ colon = strchr (message->line, ':');
+
+ if (colon == NULL) {
+ message->broken_headers++;
+ /* A simple heuristic for giving up on things that just
+ * don't look like mail messages. */
+ if (message->broken_headers >= 10 &&
+ message->good_headers < 5)
+ {
+ message->parsing_finished = 1;
+ break;
+ }
+ NEXT_HEADER_LINE (NULL);
+ continue;
+ }
+
+ message->header_size += strlen (message->line);
+
+ message->good_headers++;
+
+ header = xstrndup (message->line, colon - message->line);
+
+ if (message->restrict_headers &&
+ ! g_hash_table_lookup_extended (message->headers,
+ header, NULL, NULL))
+ {
+ free (header);
+ NEXT_HEADER_LINE (NULL);
+ continue;
+ }
+
+ s = colon + 1;
+ while (*s == ' ' || *s == '\t')
+ s++;
+
+ message->value.len = 0;
+ copy_header_unfolding (&message->value, s);
+
+ NEXT_HEADER_LINE (&message->value);
+
+ if (header_desired == 0)
+ match = 0;
+ else
+ match = (strcasecmp (header, header_desired) == 0);
+
+ decoded_value = g_mime_utils_header_decode_text (message->value.str);
+ if (g_hash_table_lookup (message->headers, header) == NULL) {
+ /* Only insert if we don't have a value for this header, yet.
+ * This way we always return the FIRST instance of any header
+ * we search for
+ * FIXME: we should be returning ALL instances of a header
+ * or at least provide a way to iterate over them
+ */
+ g_hash_table_insert (message->headers, header, decoded_value);
+ }
+ if (match)
+ return decoded_value;
+ }
+
+ if (message->parsing_finished) {
+ fclose (message->file);
+ message->file = NULL;
+ }
+
+ if (message->line)
+ free (message->line);
+ message->line = NULL;
+
+ if (message->value.size) {
+ free (message->value.str);
+ message->value.str = NULL;
+ message->value.size = 0;
+ message->value.len = 0;
+ }
+
+ /* We've parsed all headers and never found the one we're looking
+ * for. It's probably just not there, but let's check that we
+ * didn't make a mistake preventing us from seeing it. */
+ if (message->restrict_headers && header_desired &&
+ ! g_hash_table_lookup_extended (message->headers,
+ header_desired, NULL, NULL))
+ {
+ INTERNAL_ERROR ("Attempt to get header \"%s\" which was not\n"
+ "included in call to notmuch_message_file_restrict_headers\n",
+ header_desired);
+ }
+
+ return "";
+}
diff --git a/lib/message.cc b/lib/message.cc
new file mode 100644
index 0000000..721c9a6
--- /dev/null
+++ b/lib/message.cc
@@ -0,0 +1,797 @@
+/* message.cc - Results of message-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 <stdint.h>
+
+#include <gmime/gmime.h>
+
+#include <xapian.h>
+
+struct _notmuch_message {
+ notmuch_database_t *notmuch;
+ Xapian::docid doc_id;
+ int frozen;
+ char *message_id;
+ char *thread_id;
+ char *in_reply_to;
+ char *filename;
+ notmuch_message_file_t *message_file;
+ notmuch_message_list_t *replies;
+ unsigned long flags;
+
+ Xapian::Document doc;
+};
+
+/* 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_message_destructor (notmuch_message_t *message)
+{
+ message->doc.~Document ();
+
+ return 0;
+}
+
+/* Create a new notmuch_message_t object for an existing document in
+ * the database.
+ *
+ * Here, 'talloc owner' is an optional talloc context to which the new
+ * message will belong. This allows for the caller to not bother
+ * calling notmuch_message_destroy on the message, and no that all
+ * memory will be reclaimed with 'talloc_owner' is free. The caller
+ * still can call notmuch_message_destroy when finished with the
+ * message if desired.
+ *
+ * The 'talloc_owner' argument can also be NULL, in which case the
+ * caller *is* responsible for calling notmuch_message_destroy.
+ *
+ * If no document exists in the database with document ID of 'doc_id'
+ * then this function returns NULL and optionally sets *status to
+ * NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND.
+ *
+ * This function can also fail to due lack of available memory,
+ * returning NULL and optionally setting *status to
+ * NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY.
+ *
+ * The caller can pass NULL for status if uninterested in
+ * distinguishing these two cases.
+ */
+notmuch_message_t *
+_notmuch_message_create (const void *talloc_owner,
+ notmuch_database_t *notmuch,
+ unsigned int doc_id,
+ notmuch_private_status_t *status)
+{
+ notmuch_message_t *message;
+
+ if (status)
+ *status = NOTMUCH_PRIVATE_STATUS_SUCCESS;
+
+ message = talloc (talloc_owner, notmuch_message_t);
+ if (unlikely (message == NULL)) {
+ if (status)
+ *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+ return NULL;
+ }
+
+ message->notmuch = notmuch;
+ message->doc_id = doc_id;
+
+ message->frozen = 0;
+ message->flags = 0;
+
+ /* Each of these will be lazily created as needed. */
+ message->message_id = NULL;
+ message->thread_id = NULL;
+ message->in_reply_to = NULL;
+ message->filename = NULL;
+ message->message_file = NULL;
+
+ message->replies = _notmuch_message_list_create (message);
+ if (unlikely (message->replies == NULL)) {
+ if (status)
+ *status = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+ return NULL;
+ }
+
+ /* This is C++'s creepy "placement new", which is really just an
+ * ugly way to call a constructor for a pre-allocated object. So
+ * it's really not an error to not be checking for OUT_OF_MEMORY
+ * here, since this "new" isn't actually allocating memory. This
+ * is language-design comedy of the wrong kind. */
+
+ new (&message->doc) Xapian::Document;
+
+ talloc_set_destructor (message, _notmuch_message_destructor);
+
+ try {
+ message->doc = notmuch->xapian_db->get_document (doc_id);
+ } catch (const Xapian::DocNotFoundError &error) {
+ talloc_free (message);
+ if (status)
+ *status = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+ return NULL;
+ }
+
+ return message;
+}
+
+/* Create a new notmuch_message_t object for a specific message ID,
+ * (which may or may not already exist in the database).
+ *
+ * The 'notmuch' database will be the talloc owner of the returned
+ * message.
+ *
+ * If there is already a document with message ID 'message_id' in the
+ * database, then the returned message can be used to query/modify the
+ * document. Otherwise, a new document will be inserted into the
+ * database before this function returns, (and *status will be set
+ * to NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND).
+ *
+ * If an error occurs, this function will return NULL and *status
+ * will be set as appropriate. (The status pointer argument must
+ * not be NULL.)
+ */
+notmuch_message_t *
+_notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
+ const char *message_id,
+ notmuch_private_status_t *status_ret)
+{
+ notmuch_message_t *message;
+ Xapian::Document doc;
+ Xapian::WritableDatabase *db;
+ unsigned int doc_id;
+ char *term;
+
+ *status_ret = NOTMUCH_PRIVATE_STATUS_SUCCESS;
+
+ message = notmuch_database_find_message (notmuch, message_id);
+ if (message)
+ return talloc_steal (notmuch, message);
+
+ term = talloc_asprintf (NULL, "%s%s",
+ _find_prefix ("id"), message_id);
+ if (term == NULL) {
+ *status_ret = NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;
+ return NULL;
+ }
+
+ if (notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
+ INTERNAL_ERROR ("Failure to ensure database is writable.");
+
+ db = static_cast<Xapian::WritableDatabase *> (notmuch->xapian_db);
+ try {
+ doc.add_term (term, 0);
+ talloc_free (term);
+
+ doc.add_value (NOTMUCH_VALUE_MESSAGE_ID, message_id);
+
+ doc_id = db->add_document (doc);
+ } catch (const Xapian::Error &error) {
+ fprintf (stderr, "A Xapian exception occurred creating message: %s\n",
+ error.get_msg().c_str());
+ notmuch->exception_reported = TRUE;
+ *status_ret = NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION;
+ return NULL;
+ }
+
+ message = _notmuch_message_create (notmuch, notmuch,
+ doc_id, status_ret);
+
+ /* We want to inform the caller that we had to create a new
+ * document. */
+ if (*status_ret == NOTMUCH_PRIVATE_STATUS_SUCCESS)
+ *status_ret = NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND;
+
+ return message;
+}
+
+const char *
+notmuch_message_get_message_id (notmuch_message_t *message)
+{
+ Xapian::TermIterator i;
+
+ if (message->message_id)
+ return message->message_id;
+
+ i = message->doc.termlist_begin ();
+ i.skip_to (_find_prefix ("id"));
+
+ if (i == message->doc.termlist_end ())
+ INTERNAL_ERROR ("Message with document ID of %d has no message ID.\n",
+ message->doc_id);
+
+ message->message_id = talloc_strdup (message, (*i).c_str () + 1);
+
+#if DEBUG_DATABASE_SANITY
+ i++;
+
+ if (i != message->doc.termlist_end () &&
+ strncmp ((*i).c_str (), _find_prefix ("id"),
+ strlen (_find_prefix ("id"))) == 0)
+ {
+ INTERNAL_ERROR ("Mail (doc_id: %d) has duplicate message IDs",
+ message->doc_id);
+ }
+#endif
+
+ return message->message_id;
+}
+
+static void
+_notmuch_message_ensure_message_file (notmuch_message_t *message)
+{
+ const char *filename;
+
+ if (message->message_file)
+ return;
+
+ filename = notmuch_message_get_filename (message);
+ if (unlikely (filename == NULL))
+ return;
+
+ message->message_file = _notmuch_message_file_open_ctx (message, filename);
+}
+
+const char *
+notmuch_message_get_header (notmuch_message_t *message, const char *header)
+{
+ _notmuch_message_ensure_message_file (message);
+ if (message->message_file == NULL)
+ return NULL;
+
+ return notmuch_message_file_get_header (message->message_file, header);
+}
+
+/* Return the message ID from the In-Reply-To header of 'message'.
+ *
+ * Returns an empty string ("") if 'message' has no In-Reply-To
+ * header.
+ *
+ * Returns NULL if any error occurs.
+ */
+const char *
+_notmuch_message_get_in_reply_to (notmuch_message_t *message)
+{
+ const char *prefix = _find_prefix ("replyto");
+ int prefix_len = strlen (prefix);
+ Xapian::TermIterator i;
+ std::string in_reply_to;
+
+ if (message->in_reply_to)
+ return message->in_reply_to;
+
+ i = message->doc.termlist_begin ();
+ i.skip_to (prefix);
+
+ if (i != message->doc.termlist_end ())
+ in_reply_to = *i;
+
+ /* It's perfectly valid for a message to have no In-Reply-To
+ * header. For these cases, we return an empty string. */
+ if (i == message->doc.termlist_end () ||
+ strncmp (in_reply_to.c_str (), prefix, prefix_len))
+ {
+ message->in_reply_to = talloc_strdup (message, "");
+ return message->in_reply_to;
+ }
+
+ message->in_reply_to = talloc_strdup (message,
+ in_reply_to.c_str () + prefix_len);
+
+#if DEBUG_DATABASE_SANITY
+ i++;
+
+ in_reply_to = *i;
+
+ if (i != message->doc.termlist_end () &&
+ strncmp ((*i).c_str (), prefix, prefix_len) == 0)
+ {
+ INTERNAL_ERROR ("Message %s has duplicate In-Reply-To IDs: %s and %s\n",
+ notmuch_message_get_message_id (message),
+ message->in_reply_to,
+ (*i).c_str () + prefix_len);
+ }
+#endif
+
+ return message->in_reply_to;
+}
+
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message)
+{
+ const char *prefix = _find_prefix ("thread");
+ Xapian::TermIterator i;
+ std::string id;
+
+ /* This code is written with the assumption that "thread" has a
+ * single-character prefix. */
+ assert (strlen (prefix) == 1);
+
+ if (message->thread_id)
+ return message->thread_id;
+
+ i = message->doc.termlist_begin ();
+ i.skip_to (prefix);
+
+ if (i != message->doc.termlist_end ())
+ id = *i;
+
+ if (i == message->doc.termlist_end () || id[0] != *prefix)
+ INTERNAL_ERROR ("Message with document ID of %d has no thread ID.\n",
+ message->doc_id);
+
+ message->thread_id = talloc_strdup (message, id.c_str () + 1);
+
+#if DEBUG_DATABASE_SANITY
+ i++;
+ id = *i;
+
+ if (i != message->doc.termlist_end () && id[0] == *prefix)
+ {
+ INTERNAL_ERROR ("Message %s has duplicate thread IDs: %s and %s\n",
+ notmuch_message_get_message_id (message),
+ message->thread_id,
+ id.c_str () + 1);
+ }
+#endif
+
+ return message->thread_id;
+}
+
+void
+_notmuch_message_add_reply (notmuch_message_t *message,
+ notmuch_message_node_t *reply)
+{
+ _notmuch_message_list_append (message->replies, reply);
+}
+
+notmuch_messages_t *
+notmuch_message_get_replies (notmuch_message_t *message)
+{
+ return _notmuch_messages_create (message->replies);
+}
+
+/* Add an additional 'filename' for 'message'.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_set_sync. */
+notmuch_status_t
+_notmuch_message_add_filename (notmuch_message_t *message,
+ const char *filename)
+{
+ notmuch_status_t status;
+ void *local = talloc_new (message);
+ char *direntry;
+
+ if (message->filename) {
+ talloc_free (message->filename);
+ message->filename = NULL;
+ }
+
+ if (filename == NULL)
+ INTERNAL_ERROR ("Message filename cannot be NULL.");
+
+ status = _notmuch_database_filename_to_direntry (local,
+ message->notmuch,
+ filename, &direntry);
+ if (status)
+ return status;
+
+ _notmuch_message_add_term (message, "file-direntry", direntry);
+
+ talloc_free (local);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+char *
+_notmuch_message_talloc_copy_data (notmuch_message_t *message)
+{
+ return talloc_strdup (message, message->doc.get_data ().c_str ());
+}
+
+void
+_notmuch_message_clear_data (notmuch_message_t *message)
+{
+ message->doc.set_data ("");
+}
+
+const char *
+notmuch_message_get_filename (notmuch_message_t *message)
+{
+ const char *prefix = _find_prefix ("file-direntry");
+ int prefix_len = strlen (prefix);
+ Xapian::TermIterator i;
+ char *colon, *direntry = NULL;
+ const char *db_path, *directory, *basename;
+ unsigned int directory_id;
+ void *local = talloc_new (message);
+
+ if (message->filename)
+ return message->filename;
+
+ i = message->doc.termlist_begin ();
+ i.skip_to (prefix);
+
+ if (i != message->doc.termlist_end ())
+ direntry = talloc_strdup (local, (*i).c_str ());
+
+ if (i == message->doc.termlist_end () ||
+ strncmp (direntry, prefix, prefix_len))
+ {
+ /* A message document created by an old version of notmuch
+ * (prior to rename support) will have the filename in the
+ * data of the document rather than as a file-direntry term.
+ *
+ * It would be nice to do the upgrade of the document directly
+ * here, but the database is likely open in read-only mode. */
+ const char *data;
+
+ data = message->doc.get_data ().c_str ();
+
+ if (data == NULL)
+ INTERNAL_ERROR ("message with no filename");
+
+ message->filename = talloc_strdup (message, data);
+
+ return message->filename;
+ }
+
+ direntry += prefix_len;
+
+ directory_id = strtol (direntry, &colon, 10);
+
+ if (colon == NULL || *colon != ':')
+ INTERNAL_ERROR ("malformed direntry");
+
+ basename = colon + 1;
+
+ *colon = '\0';
+
+ db_path = notmuch_database_get_path (message->notmuch);
+
+ directory = _notmuch_database_get_directory_path (local,
+ message->notmuch,
+ directory_id);
+
+ if (strlen (directory))
+ message->filename = talloc_asprintf (message, "%s/%s/%s",
+ db_path, directory, basename);
+ else
+ message->filename = talloc_asprintf (message, "%s/%s",
+ db_path, basename);
+ talloc_free ((void *) directory);
+
+ talloc_free (local);
+
+ return message->filename;
+}
+
+notmuch_bool_t
+notmuch_message_get_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag)
+{
+ return message->flags & (1 << flag);
+}
+
+void
+notmuch_message_set_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag, notmuch_bool_t enable)
+{
+ if (enable)
+ message->flags |= (1 << flag);
+ else
+ message->flags &= ~(1 << flag);
+}
+
+time_t
+notmuch_message_get_date (notmuch_message_t *message)
+{
+ std::string value;
+
+ try {
+ value = message->doc.get_value (NOTMUCH_VALUE_TIMESTAMP);
+ } catch (Xapian::Error &error) {
+ INTERNAL_ERROR ("Failed to read timestamp value from document.");
+ return 0;
+ }
+
+ return Xapian::sortable_unserialise (value);
+}
+
+notmuch_tags_t *
+notmuch_message_get_tags (notmuch_message_t *message)
+{
+ Xapian::TermIterator i, end;
+ i = message->doc.termlist_begin();
+ end = message->doc.termlist_end();
+ return _notmuch_convert_tags(message, i, end);
+}
+
+void
+_notmuch_message_set_date (notmuch_message_t *message,
+ const char *date)
+{
+ time_t time_value;
+
+ /* GMime really doesn't want to see a NULL date, so protect its
+ * sensibilities. */
+ if (date == NULL || *date == '\0')
+ time_value = 0;
+ else
+ time_value = g_mime_utils_header_decode_date (date, NULL);
+
+ message->doc.add_value (NOTMUCH_VALUE_TIMESTAMP,
+ Xapian::sortable_serialise (time_value));
+}
+
+/* Synchronize changes made to message->doc out into the database. */
+void
+_notmuch_message_sync (notmuch_message_t *message)
+{
+ Xapian::WritableDatabase *db;
+
+ if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)
+ return;
+
+ db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);
+ db->replace_document (message->doc_id, message->doc);
+}
+
+/* Ensure that 'message' is not holding any file object open. Future
+ * calls to various functions will still automatically open the
+ * message file as needed.
+ */
+void
+_notmuch_message_close (notmuch_message_t *message)
+{
+ if (message->message_file) {
+ notmuch_message_file_close (message->message_file);
+ message->message_file = NULL;
+ }
+}
+
+/* Add a name:value term to 'message', (the actual term will be
+ * encoded by prefixing the value with a short prefix). See
+ * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
+ * names to prefix values.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_set_sync. */
+notmuch_private_status_t
+_notmuch_message_add_term (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *value)
+{
+
+ char *term;
+
+ if (value == NULL)
+ return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
+
+ term = talloc_asprintf (message, "%s%s",
+ _find_prefix (prefix_name), value);
+
+ if (strlen (term) > NOTMUCH_TERM_MAX)
+ return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+
+ message->doc.add_term (term, 0);
+
+ talloc_free (term);
+
+ return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+/* Parse 'text' and add a term to 'message' for each parsed word. Each
+ * term will be added both prefixed (if prefix_name is not NULL) and
+ * also unprefixed). */
+notmuch_private_status_t
+_notmuch_message_gen_terms (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *text)
+{
+ Xapian::TermGenerator *term_gen = message->notmuch->term_gen;
+
+ if (text == NULL)
+ return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
+
+ term_gen->set_document (message->doc);
+
+ if (prefix_name) {
+ const char *prefix = _find_prefix (prefix_name);
+
+ term_gen->index_text (text, 1, prefix);
+ }
+
+ term_gen->index_text (text);
+
+ return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+/* Remove a name:value term from 'message', (the actual term will be
+ * encoded by prefixing the value with a short prefix). See
+ * NORMAL_PREFIX and BOOLEAN_PREFIX arrays for the mapping of term
+ * names to prefix values.
+ *
+ * This change will not be reflected in the database until the next
+ * call to _notmuch_message_set_sync. */
+notmuch_private_status_t
+_notmuch_message_remove_term (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *value)
+{
+ char *term;
+
+ if (value == NULL)
+ return NOTMUCH_PRIVATE_STATUS_NULL_POINTER;
+
+ term = talloc_asprintf (message, "%s%s",
+ _find_prefix (prefix_name), value);
+
+ if (strlen (term) > NOTMUCH_TERM_MAX)
+ return NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG;
+
+ try {
+ message->doc.remove_term (term);
+ } catch (const Xapian::InvalidArgumentError) {
+ /* We'll let the philosopher's try to wrestle with the
+ * question of whether failing to remove that which was not
+ * there in the first place is failure. For us, we'll silently
+ * consider it all good. */
+ }
+
+ talloc_free (term);
+
+ return NOTMUCH_PRIVATE_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_add_tag (notmuch_message_t *message, const char *tag)
+{
+ notmuch_private_status_t private_status;
+ notmuch_status_t status;
+
+ status = _notmuch_database_ensure_writable (message->notmuch);
+ if (status)
+ return status;
+
+ if (tag == NULL)
+ return NOTMUCH_STATUS_NULL_POINTER;
+
+ if (strlen (tag) > NOTMUCH_TAG_MAX)
+ return NOTMUCH_STATUS_TAG_TOO_LONG;
+
+ private_status = _notmuch_message_add_term (message, "tag", tag);
+ if (private_status) {
+ INTERNAL_ERROR ("_notmuch_message_add_term return unexpected value: %d\n",
+ private_status);
+ }
+
+ if (! message->frozen)
+ _notmuch_message_sync (message);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)
+{
+ notmuch_private_status_t private_status;
+ notmuch_status_t status;
+
+ status = _notmuch_database_ensure_writable (message->notmuch);
+ if (status)
+ return status;
+
+ if (tag == NULL)
+ return NOTMUCH_STATUS_NULL_POINTER;
+
+ if (strlen (tag) > NOTMUCH_TAG_MAX)
+ return NOTMUCH_STATUS_TAG_TOO_LONG;
+
+ private_status = _notmuch_message_remove_term (message, "tag", tag);
+ if (private_status) {
+ INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
+ private_status);
+ }
+
+ if (! message->frozen)
+ _notmuch_message_sync (message);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_remove_all_tags (notmuch_message_t *message)
+{
+ notmuch_private_status_t private_status;
+ notmuch_status_t status;
+ notmuch_tags_t *tags;
+ const char *tag;
+
+ status = _notmuch_database_ensure_writable (message->notmuch);
+ if (status)
+ return status;
+
+ for (tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ tag = notmuch_tags_get (tags);
+
+ private_status = _notmuch_message_remove_term (message, "tag", tag);
+ if (private_status) {
+ INTERNAL_ERROR ("_notmuch_message_remove_term return unexpected value: %d\n",
+ private_status);
+ }
+ }
+
+ if (! message->frozen)
+ _notmuch_message_sync (message);
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_freeze (notmuch_message_t *message)
+{
+ notmuch_status_t status;
+
+ status = _notmuch_database_ensure_writable (message->notmuch);
+ if (status)
+ return status;
+
+ message->frozen++;
+
+ return NOTMUCH_STATUS_SUCCESS;
+}
+
+notmuch_status_t
+notmuch_message_thaw (notmuch_message_t *message)
+{
+ notmuch_status_t status;
+
+ status = _notmuch_database_ensure_writable (message->notmuch);
+ if (status)
+ return status;
+
+ if (message->frozen > 0) {
+ message->frozen--;
+ if (message->frozen == 0)
+ _notmuch_message_sync (message);
+ return NOTMUCH_STATUS_SUCCESS;
+ } else {
+ return NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW;
+ }
+}
+
+void
+notmuch_message_destroy (notmuch_message_t *message)
+{
+ talloc_free (message);
+}
diff --git a/lib/messages.c b/lib/messages.c
new file mode 100644
index 0000000..db2b7a1
--- /dev/null
+++ b/lib/messages.c
@@ -0,0 +1,182 @@
+/* messages.c - Iterator for a set of messages
+ *
+ * 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 <glib.h>
+
+/* Create a new notmuch_message_list_t object, with 'ctx' as its
+ * talloc owner.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+notmuch_message_list_t *
+_notmuch_message_list_create (const void *ctx)
+{
+ notmuch_message_list_t *list;
+
+ list = talloc (ctx, notmuch_message_list_t);
+ if (unlikely (list == NULL))
+ return NULL;
+
+ list->head = NULL;
+ list->tail = &list->head;
+
+ return list;
+}
+
+/* Append 'node' (which can of course point to an arbitrarily long
+ * list of nodes) to the end of 'list'.
+ */
+void
+_notmuch_message_list_append (notmuch_message_list_t *list,
+ notmuch_message_node_t *node)
+{
+ *(list->tail) = node;
+ list->tail = &node->next;
+}
+
+/* Allocate a new node for 'message' and append it to the end of
+ * 'list'.
+ */
+void
+_notmuch_message_list_add_message (notmuch_message_list_t *list,
+ notmuch_message_t *message)
+{
+ notmuch_message_node_t *node = talloc (list, notmuch_message_node_t);
+
+ node->message = message;
+ node->next = NULL;
+
+ _notmuch_message_list_append (list, node);
+}
+
+notmuch_messages_t *
+_notmuch_messages_create (notmuch_message_list_t *list)
+{
+ notmuch_messages_t *messages;
+
+ if (list->head == NULL)
+ return NULL;
+
+ messages = talloc (list, notmuch_messages_t);
+ if (unlikely (messages == NULL))
+ return NULL;
+
+ messages->is_of_list_type = TRUE;
+ messages->iterator = list->head;
+
+ return messages;
+}
+
+/* We're using the "is_of_type_list" to conditionally defer to the
+ * notmuch_mset_messages_t implementation of notmuch_messages_t in
+ * query.cc. It's ugly that that's over in query.cc, and it's ugly
+ * that we're not using a union here. Both of those uglies are due to
+ * C++:
+ *
+ * 1. I didn't want to force a C++ header file onto
+ * notmuch-private.h and suddenly subject all our code to a
+ * C++ compiler and its rules.
+ *
+ * 2. C++ won't allow me to put C++ objects, (with non-trivial
+ * constructors) into a union anyway. Even though I'd
+ * carefully control object construction with placement new
+ * anyway. *sigh*
+ */
+notmuch_bool_t
+notmuch_messages_valid (notmuch_messages_t *messages)
+{
+ if (messages == NULL)
+ return FALSE;
+
+ if (! messages->is_of_list_type)
+ return _notmuch_mset_messages_valid (messages);
+
+ return (messages->iterator != NULL);
+}
+
+notmuch_message_t *
+notmuch_messages_get (notmuch_messages_t *messages)
+{
+ if (! messages->is_of_list_type)
+ return _notmuch_mset_messages_get (messages);
+
+ if (messages->iterator == NULL)
+ return NULL;
+
+ return messages->iterator->message;
+}
+
+void
+notmuch_messages_move_to_next (notmuch_messages_t *messages)
+{
+ if (! messages->is_of_list_type)
+ return _notmuch_mset_messages_move_to_next (messages);
+
+ if (messages->iterator == NULL)
+ return;
+
+ messages->iterator = messages->iterator->next;
+}
+
+void
+notmuch_messages_destroy (notmuch_messages_t *messages)
+{
+ talloc_free (messages);
+}
+
+
+notmuch_tags_t *
+notmuch_messages_collect_tags (notmuch_messages_t *messages)
+{
+ notmuch_tags_t *tags, *msg_tags;
+ notmuch_message_t *msg;
+ GHashTable *htable;
+ GList *keys, *l;
+ const char *tag;
+
+ tags = _notmuch_tags_create (messages);
+ if (tags == NULL) return NULL;
+
+ htable = g_hash_table_new_full (g_str_hash, g_str_equal, free, NULL);
+
+ while ((msg = notmuch_messages_get (messages))) {
+ msg_tags = notmuch_message_get_tags (msg);
+ while ((tag = notmuch_tags_get (msg_tags))) {
+ g_hash_table_insert (htable, xstrdup (tag), NULL);
+ notmuch_tags_move_to_next (msg_tags);
+ }
+ notmuch_tags_destroy (msg_tags);
+ notmuch_message_destroy (msg);
+ notmuch_messages_move_to_next (messages);
+ }
+
+ keys = g_hash_table_get_keys (htable);
+ for (l = keys; l; l = l->next) {
+ _notmuch_tags_add_tag (tags, (char *)l->data);
+ }
+
+ g_list_free (keys);
+ g_hash_table_destroy (htable);
+
+ _notmuch_tags_prepare_iterator (tags);
+ return tags;
+}
diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h
new file mode 100644
index 0000000..94cce1b
--- /dev/null
+++ b/lib/notmuch-private.h
@@ -0,0 +1,421 @@
+/* notmuch-private.h - Internal interfaces 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>
+ */
+
+#ifndef NOTMUCH_PRIVATE_H
+#define NOTMUCH_PRIVATE_H
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* For getline and asprintf */
+#endif
+#include <stdio.h>
+
+#include "compat.h"
+
+#include "notmuch.h"
+
+NOTMUCH_BEGIN_DECLS
+
+#include <stdlib.h>
+#include <stdarg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <assert.h>
+
+#include <talloc.h>
+
+#include "xutil.h"
+
+#ifdef DEBUG
+# define DEBUG_DATABASE_SANITY 1
+# define DEBUG_QUERY 1
+#endif
+
+#define COMPILE_TIME_ASSERT(pred) ((void)sizeof(char[1 - 2*!(pred)]))
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that PRINTF_ATTRIBUTE comes from talloc.h
+ */
+int
+_internal_error (const char *format, ...) PRINTF_ATTRIBUTE (1, 2);
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that __location__ comes from talloc.h.
+ */
+#define INTERNAL_ERROR(format, ...) \
+ _internal_error (format " (%s).\n", \
+ ##__VA_ARGS__, __location__)
+
+#define unused(x) x __attribute__ ((unused))
+
+/* Thanks to Andrew Tridgell's (SAMBA's) talloc for this definition of
+ * unlikely. The talloc source code comes to us via the GNU LGPL v. 3.
+ */
+/* these macros gain us a few percent of speed on gcc */
+#if (__GNUC__ >= 3)
+/* the strange !! is to ensure that __builtin_expect() takes either 0 or 1
+ as its first argument */
+#ifndef likely
+#define likely(x) __builtin_expect(!!(x), 1)
+#endif
+#ifndef unlikely
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#endif
+#else
+#ifndef likely
+#define likely(x) (x)
+#endif
+#ifndef unlikely
+#define unlikely(x) (x)
+#endif
+#endif
+
+typedef enum {
+ NOTMUCH_VALUE_TIMESTAMP = 0,
+ NOTMUCH_VALUE_MESSAGE_ID
+} notmuch_value_t;
+
+/* Xapian (with flint backend) complains if we provide a term longer
+ * than this, but I haven't yet found a way to query the limit
+ * programmatically. */
+#define NOTMUCH_TERM_MAX 245
+
+typedef enum _notmuch_private_status {
+ /* First, copy all the public status values. */
+ NOTMUCH_PRIVATE_STATUS_SUCCESS = NOTMUCH_STATUS_SUCCESS,
+ NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY = NOTMUCH_STATUS_OUT_OF_MEMORY,
+ NOTMUCH_PRIVATE_STATUS_READ_ONLY_DATABASE = NOTMUCH_STATUS_READ_ONLY_DATABASE,
+ NOTMUCH_PRIVATE_STATUS_XAPIAN_EXCEPTION = NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+ NOTMUCH_PRIVATE_STATUS_FILE_NOT_EMAIL = NOTMUCH_STATUS_FILE_NOT_EMAIL,
+ NOTMUCH_PRIVATE_STATUS_NULL_POINTER = NOTMUCH_STATUS_NULL_POINTER,
+ NOTMUCH_PRIVATE_STATUS_TAG_TOO_LONG = NOTMUCH_STATUS_TAG_TOO_LONG,
+ NOTMUCH_PRIVATE_STATUS_UNBALANCED_FREEZE_THAW = NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+
+ /* Then add our own private values. */
+ NOTMUCH_PRIVATE_STATUS_TERM_TOO_LONG = NOTMUCH_STATUS_LAST_STATUS,
+ NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND,
+
+ NOTMUCH_PRIVATE_STATUS_LAST_STATUS
+} notmuch_private_status_t;
+
+/* Coerce a notmuch_private_status_t value to a notmuch_status_t
+ * value, generating an internal error if the private value is equal
+ * to or greater than NOTMUCH_STATUS_LAST_STATUS. (The idea here is
+ * that the caller has previously handled any expected
+ * notmuch_private_status_t values.)
+ */
+#define COERCE_STATUS(private_status, format, ...) \
+ ((private_status >= (notmuch_private_status_t) NOTMUCH_STATUS_LAST_STATUS)\
+ ? \
+ (notmuch_status_t) _internal_error (format " (%s).\n", \
+ ##__VA_ARGS__, \
+ __location__) \
+ : \
+ (notmuch_status_t) private_status)
+
+/* database.cc */
+
+/* Lookup a prefix value by name.
+ *
+ * XXX: This should really be static inside of message.cc, and we can
+ * do that once we convert database.cc to use the
+ * _notmuch_message_add/remove_term functions. */
+const char *
+_find_prefix (const char *name);
+
+notmuch_status_t
+_notmuch_database_ensure_writable (notmuch_database_t *notmuch);
+
+const char *
+_notmuch_database_relative_path (notmuch_database_t *notmuch,
+ const char *path);
+
+notmuch_status_t
+_notmuch_database_split_path (void *ctx,
+ const char *path,
+ const char **directory,
+ const char **basename);
+
+const char *
+_notmuch_database_get_directory_db_path (const char *path);
+
+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);
+
+notmuch_status_t
+_notmuch_database_find_directory_id (notmuch_database_t *database,
+ const char *path,
+ unsigned int *directory_id);
+
+const char *
+_notmuch_database_get_directory_path (void *ctx,
+ notmuch_database_t *notmuch,
+ unsigned int doc_id);
+
+notmuch_status_t
+_notmuch_database_filename_to_direntry (void *ctx,
+ notmuch_database_t *notmuch,
+ const char *filename,
+ char **direntry);
+
+/* directory.cc */
+
+notmuch_directory_t *
+_notmuch_directory_create (notmuch_database_t *notmuch,
+ const char *path,
+ notmuch_status_t *status_ret);
+
+unsigned int
+_notmuch_directory_get_document_id (notmuch_directory_t *directory);
+
+/* thread.cc */
+
+notmuch_thread_t *
+_notmuch_thread_create (void *ctx,
+ notmuch_database_t *notmuch,
+ const char *thread_id,
+ const char *query_string,
+ notmuch_sort_t sort);
+
+/* message.cc */
+
+notmuch_message_t *
+_notmuch_message_create (const void *talloc_owner,
+ notmuch_database_t *notmuch,
+ unsigned int doc_id,
+ notmuch_private_status_t *status);
+
+notmuch_message_t *
+_notmuch_message_create_for_message_id (notmuch_database_t *notmuch,
+ const char *message_id,
+ notmuch_private_status_t *status);
+
+const char *
+_notmuch_message_get_in_reply_to (notmuch_message_t *message);
+
+notmuch_private_status_t
+_notmuch_message_add_term (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *value);
+
+notmuch_private_status_t
+_notmuch_message_remove_term (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *value);
+
+notmuch_private_status_t
+_notmuch_message_gen_terms (notmuch_message_t *message,
+ const char *prefix_name,
+ const char *text);
+
+void
+_notmuch_message_upgrade_filename_storage (notmuch_message_t *message);
+
+notmuch_status_t
+_notmuch_message_add_filename (notmuch_message_t *message,
+ const char *filename);
+
+void
+_notmuch_message_ensure_thread_id (notmuch_message_t *message);
+
+void
+_notmuch_message_set_date (notmuch_message_t *message,
+ const char *date);
+
+void
+_notmuch_message_sync (notmuch_message_t *message);
+
+void
+_notmuch_message_close (notmuch_message_t *message);
+
+/* Get a copy of the data in this message document.
+ *
+ * Caller should talloc_free the result when done.
+ *
+ * This function is intended to support database upgrade and really
+ * shouldn't be used otherwise. */
+char *
+_notmuch_message_talloc_copy_data (notmuch_message_t *message);
+
+/* Clear the data in this message document.
+ *
+ * This function is intended to support database upgrade and really
+ * shouldn't be used otherwise. */
+void
+_notmuch_message_clear_data (notmuch_message_t *message);
+
+/* index.cc */
+
+notmuch_status_t
+_notmuch_message_index_file (notmuch_message_t *message,
+ const char *filename);
+
+/* message-file.c */
+
+/* XXX: I haven't decided yet whether these will actually get exported
+ * into the public interface in notmuch.h
+ */
+
+typedef struct _notmuch_message_file notmuch_message_file_t;
+
+/* Open a file containing a single email message.
+ *
+ * The caller should call notmuch_message_close when done with this.
+ *
+ * Returns NULL if any error occurs.
+ */
+notmuch_message_file_t *
+notmuch_message_file_open (const char *filename);
+
+/* Like notmuch_message_file_open but with 'ctx' as the talloc owner. */
+notmuch_message_file_t *
+_notmuch_message_file_open_ctx (void *ctx, const char *filename);
+
+/* Close a notmuch message previously opened with notmuch_message_open. */
+void
+notmuch_message_file_close (notmuch_message_file_t *message);
+
+/* Restrict 'message' to only save the named headers.
+ *
+ * When the caller is only interested in a short list of headers,
+ * known in advance, calling this function can avoid wasted time and
+ * memory parsing/saving header values that will never be needed.
+ *
+ * The variable arguments should be a list of const char * with a
+ * final '(const char *) NULL' to terminate the list.
+ *
+ * If this function is called, it must be called before any calls to
+ * notmuch_message_get_header for this message.
+ *
+ * After calling this function, if notmuch_message_get_header is
+ * called with a header name not in this list, then NULL will be
+ * returned even if that header exists in the actual message.
+ */
+void
+notmuch_message_file_restrict_headers (notmuch_message_file_t *message, ...);
+
+/* Identical to notmuch_message_restrict_headers but accepting a va_list. */
+void
+notmuch_message_file_restrict_headersv (notmuch_message_file_t *message,
+ va_list va_headers);
+
+/* Get the value of the specified header from the message.
+ *
+ * The header name is case insensitive.
+ *
+ * The returned value is owned by the notmuch message and is valid
+ * only until the message is closed. The caller should copy it if
+ * needing to modify the value or to hold onto it for longer.
+ *
+ * Returns NULL if the message does not contain a header line matching
+ * 'header'.
+ */
+const char *
+notmuch_message_file_get_header (notmuch_message_file_t *message,
+ const char *header);
+
+/* messages.c */
+
+typedef struct _notmuch_message_node {
+ notmuch_message_t *message;
+ struct _notmuch_message_node *next;
+} notmuch_message_node_t;
+
+typedef struct _notmuch_message_list {
+ notmuch_message_node_t *head;
+ notmuch_message_node_t **tail;
+} notmuch_message_list_t;
+
+/* There's a rumor that there's an alternate struct _notmuch_messages
+ * somewhere with some nasty C++ objects in it. We'll try to maintain
+ * ignorance of that here. (See notmuch_mset_messages_t in query.cc)
+ */
+struct _notmuch_messages {
+ notmuch_bool_t is_of_list_type;
+ notmuch_message_node_t *iterator;
+};
+
+notmuch_message_list_t *
+_notmuch_message_list_create (const void *ctx);
+
+void
+_notmuch_message_list_append (notmuch_message_list_t *list,
+ notmuch_message_node_t *node);
+
+void
+_notmuch_message_list_add_message (notmuch_message_list_t *list,
+ notmuch_message_t *message);
+
+notmuch_messages_t *
+_notmuch_messages_create (notmuch_message_list_t *list);
+
+/* query.cc */
+
+notmuch_bool_t
+_notmuch_mset_messages_valid (notmuch_messages_t *messages);
+
+notmuch_message_t *
+_notmuch_mset_messages_get (notmuch_messages_t *messages);
+
+void
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages);
+
+/* message.cc */
+
+void
+_notmuch_message_add_reply (notmuch_message_t *message,
+ notmuch_message_node_t *reply);
+
+/* sha1.c */
+
+char *
+notmuch_sha1_of_string (const char *str);
+
+char *
+notmuch_sha1_of_file (const char *filename);
+
+/* tags.c */
+
+notmuch_tags_t *
+_notmuch_tags_create (void *ctx);
+
+void
+_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag);
+
+void
+_notmuch_tags_prepare_iterator (notmuch_tags_t *tags);
+
+NOTMUCH_END_DECLS
+
+#endif
diff --git a/lib/notmuch.h b/lib/notmuch.h
new file mode 100644
index 0000000..bae48a6
--- /dev/null
+++ b/lib/notmuch.h
@@ -0,0 +1,1112 @@
+/* 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>
+ */
+
+#ifndef NOTMUCH_H
+#define NOTMUCH_H
+
+#ifdef __cplusplus
+# define NOTMUCH_BEGIN_DECLS extern "C" {
+# define NOTMUCH_END_DECLS }
+#else
+# define NOTMUCH_BEGIN_DECLS
+# define NOTMUCH_END_DECLS
+#endif
+
+NOTMUCH_BEGIN_DECLS
+
+#include <time.h>
+
+#ifndef FALSE
+#define FALSE 0
+#endif
+
+#ifndef TRUE
+#define TRUE 1
+#endif
+
+typedef int notmuch_bool_t;
+
+/* Status codes used for the return values of most functions.
+ *
+ * A zero value (NOTMUCH_STATUS_SUCCESS) indicates that the function
+ * completed without error. Any other value indicates an error as
+ * follows:
+ *
+ * NOTMUCH_STATUS_SUCCESS: No error occurred.
+ *
+ * NOTMUCH_STATUS_OUT_OF_MEMORY: Out of memory
+ *
+ * XXX: We don't really want to expose this lame XAPIAN_EXCEPTION
+ * value. Instead we should map to things like DATABASE_LOCKED or
+ * whatever.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: An attempt was made to write to
+ * a database opened in read-only mode.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception occurred
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: An error occurred trying to read or
+ * write to a file (this could be file not found, permission
+ * denied, etc.)
+ *
+ * NOTMUCH_STATUS_FILE_NOT_EMAIL: A file was presented that doesn't
+ * appear to be an email message.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: A file contains a message ID
+ * that is identical to a message already in the database.
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The user erroneously passed a NULL
+ * pointer to a notmuch function.
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: A tag value is too long (exceeds
+ * NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: The notmuch_message_thaw
+ * function has been called more times than notmuch_message_freeze.
+ *
+ * And finally:
+ *
+ * NOTMUCH_STATUS_LAST_STATUS: Not an actual status value. Just a way
+ * to find out how many valid status values there are.
+ */
+typedef enum _notmuch_status {
+ NOTMUCH_STATUS_SUCCESS = 0,
+ NOTMUCH_STATUS_OUT_OF_MEMORY,
+ NOTMUCH_STATUS_READ_ONLY_DATABASE,
+ NOTMUCH_STATUS_XAPIAN_EXCEPTION,
+ NOTMUCH_STATUS_FILE_ERROR,
+ NOTMUCH_STATUS_FILE_NOT_EMAIL,
+ NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID,
+ NOTMUCH_STATUS_NULL_POINTER,
+ NOTMUCH_STATUS_TAG_TOO_LONG,
+ NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW,
+
+ NOTMUCH_STATUS_LAST_STATUS
+} notmuch_status_t;
+
+/* Get a string representation of a notmuch_status_t value.
+ *
+ * The result is readonly.
+ */
+const char *
+notmuch_status_to_string (notmuch_status_t status);
+
+/* Various opaque data types. For each notmuch_<foo>_t see the various
+ * notmuch_<foo> functions below. */
+typedef struct _notmuch_database notmuch_database_t;
+typedef struct _notmuch_query notmuch_query_t;
+typedef struct _notmuch_threads notmuch_threads_t;
+typedef struct _notmuch_thread notmuch_thread_t;
+typedef struct _notmuch_messages notmuch_messages_t;
+typedef struct _notmuch_message notmuch_message_t;
+typedef struct _notmuch_tags notmuch_tags_t;
+typedef struct _notmuch_directory notmuch_directory_t;
+typedef struct _notmuch_filenames notmuch_filenames_t;
+
+/* Create a new, empty notmuch database located at 'path'.
+ *
+ * The path should be a top-level directory to a collection of
+ * plain-text email messages (one message per file). This call will
+ * create a new ".notmuch" directory within 'path' where notmuch will
+ * store its data.
+ *
+ * After a successful call to notmuch_database_create, the returned
+ * database will be open so the caller should call
+ * notmuch_database_close when finished with it.
+ *
+ * The database will not yet have any data in it
+ * (notmuch_database_create itself is a very cheap function). Messages
+ * contained within 'path' can be added to the database by calling
+ * notmuch_database_add_message.
+ *
+ * In case of any failure, this function returns NULL, (after printing
+ * an error message on stderr).
+ */
+notmuch_database_t *
+notmuch_database_create (const char *path);
+
+typedef enum {
+ NOTMUCH_DATABASE_MODE_READ_ONLY = 0,
+ NOTMUCH_DATABASE_MODE_READ_WRITE
+} notmuch_database_mode_t;
+
+/* XXX: I think I'd like this to take an extra argument of
+ * notmuch_status_t* for returning a status value on failure. */
+
+/* Open an existing notmuch database located at 'path'.
+ *
+ * The database should have been created at some time in the past,
+ * (not necessarily by this process), by calling
+ * notmuch_database_create with 'path'. By default the database should be
+ * opened for reading only. In order to write to the database you need to
+ * pass the NOTMUCH_DATABASE_MODE_READ_WRITE mode.
+ *
+ * An existing notmuch database can be identified by the presence of a
+ * directory named ".notmuch" below 'path'.
+ *
+ * The caller should call notmuch_database_close when finished with
+ * this database.
+ *
+ * In case of any failure, this function returns NULL, (after printing
+ * an error message on stderr).
+ */
+notmuch_database_t *
+notmuch_database_open (const char *path,
+ notmuch_database_mode_t mode);
+
+/* Close the given notmuch database, freeing all associated
+ * resources. See notmuch_database_open. */
+void
+notmuch_database_close (notmuch_database_t *database);
+
+/* Return the database path of the given database.
+ *
+ * The return value is a string owned by notmuch so should not be
+ * modified nor freed by the caller. */
+const char *
+notmuch_database_get_path (notmuch_database_t *database);
+
+/* Return the database format version of the given database. */
+unsigned int
+notmuch_database_get_version (notmuch_database_t *database);
+
+/* Does this database need to be upgraded before writing to it?
+ *
+ * If this function returns TRUE then no functions that modify the
+ * database (notmuch_database_add_message, notmuch_message_add_tag,
+ * notmuch_directory_set_mtime, etc.) will work unless the function
+ * notmuch_database_upgrade is called successfully first. */
+notmuch_bool_t
+notmuch_database_needs_upgrade (notmuch_database_t *database);
+
+/* 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 'progress' as a floating-point value in
+ * the range of [0.0 .. 1.0] indicating the progress made so far in
+ * the upgrade process.
+ */
+notmuch_status_t
+notmuch_database_upgrade (notmuch_database_t *database,
+ void (*progress_notify) (void *closure,
+ double progress),
+ void *closure);
+
+/* Retrieve a directory object from the database for 'path'.
+ *
+ * Here, 'path' should be a path relative to the path of 'database'
+ * (see notmuch_database_get_path), or else should be an absolute path
+ * with initial components that match the path of 'database'.
+ */
+notmuch_directory_t *
+notmuch_database_get_directory (notmuch_database_t *database,
+ const char *path);
+
+/* Add a new message to the given notmuch database.
+ *
+ * Here,'filename' should be a path relative to the path of
+ * 'database' (see notmuch_database_get_path), or else should be an
+ * absolute filename with initial components that match the path of
+ * 'database'.
+ *
+ * The file should be a single mail message (not a multi-message mbox)
+ * that is expected to remain at its current location, (since the
+ * notmuch database will reference the filename, and will not copy the
+ * entire contents of the file.
+ *
+ * If 'message' is not NULL, then, on successful return '*message'
+ * will be initialized to a message object that can be used for things
+ * such as adding tags to the just-added message. The user should call
+ * notmuch_message_destroy when done with the message. On any failure
+ * '*message' will be set to NULL.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully added to database.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: Message has the same message
+ * ID as another message already in the database. The new
+ * filename was successfully added to the message in the database
+ * (if not already present).
+ *
+ * NOTMUCH_STATUS_FILE_ERROR: an error occurred trying to open the
+ * file, (such as permission denied, or file not found,
+ * etc.). Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_FILE_NOT_EMAIL: the contents of filename don't look
+ * like an email message. Nothing added to the database.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so no message can be added.
+ */
+notmuch_status_t
+notmuch_database_add_message (notmuch_database_t *database,
+ const char *filename,
+ notmuch_message_t **message);
+
+/* Remove a message from the given notmuch database.
+ *
+ * Note that only this particular filename association is removed from
+ * the database. If the same message (as determined by the message ID)
+ * is still available via other filenames, then the message will
+ * persist in the database for those filenames. When the last filename
+ * is removed for a particular message, the database content for that
+ * message will be entirely removed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: The last filename was removed and the
+ * message was removed from the database.
+ *
+ * NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID: This filename was removed but
+ * the message persists in the database with at least one other
+ * filename.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so no message can be removed.
+ */
+notmuch_status_t
+notmuch_database_remove_message (notmuch_database_t *database,
+ const char *filename);
+
+/* Find a message with the given message_id.
+ *
+ * If the database contains a message with the given message_id, then
+ * a new notmuch_message_t object is returned. The caller should call
+ * notmuch_message_destroy when done with the message.
+ *
+ * If no message is found with the given message_id or if an
+ * out-of-memory situation occurs, this function returns NULL.
+ */
+notmuch_message_t *
+notmuch_database_find_message (notmuch_database_t *database,
+ const char *message_id);
+
+/* Return a list of all tags found in the database.
+ *
+ * This function creates a list of all tags found in the database. The
+ * resulting list contains all tags from all messages found in the database.
+ *
+ * On error this function returns NULL.
+ */
+notmuch_tags_t *
+notmuch_database_get_all_tags (notmuch_database_t *db);
+
+/* Create a new query for 'database'.
+ *
+ * Here, 'database' should be an open database, (see
+ * notmuch_database_open and notmuch_database_create).
+ *
+ * For the query string, we'll document the syntax here more
+ * completely in the future, but it's likely to be a specialized
+ * version of the general Xapian query syntax:
+ *
+ * http://xapian.org/docs/queryparser.html
+ *
+ * As a special case, passing either a length-zero string, (that is ""),
+ * or a string consisting of a single asterisk (that is "*"), will
+ * result in a query that returns all messages in the database.
+ *
+ * See notmuch_query_set_sort for controlling the order of results.
+ * See notmuch_query_search_messages and notmuch_query_search_threads
+ * to actually execute the query.
+ *
+ * User should call notmuch_query_destroy when finished with this
+ * query.
+ *
+ * Will return NULL if insufficient memory is available.
+ */
+notmuch_query_t *
+notmuch_query_create (notmuch_database_t *database,
+ const char *query_string);
+
+/* Sort values for notmuch_query_set_sort */
+typedef enum {
+ NOTMUCH_SORT_OLDEST_FIRST,
+ NOTMUCH_SORT_NEWEST_FIRST,
+ NOTMUCH_SORT_MESSAGE_ID,
+ NOTMUCH_SORT_UNSORTED
+} notmuch_sort_t;
+
+/* Specify the sorting desired for this query. */
+void
+notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort);
+
+/* Execute a query for threads, returning a notmuch_threads_t object
+ * which can be used to iterate over the results. The returned threads
+ * object is owned by the query and as such, will only be valid until
+ * notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ * notmuch_query_t *query;
+ * notmuch_threads_t *threads;
+ * notmuch_thread_t *thread;
+ *
+ * query = notmuch_query_create (database, query_string);
+ *
+ * for (threads = notmuch_query_search_threads (query);
+ * notmuch_threads_valid (threads);
+ * notmuch_threads_move_to_next (threads))
+ * {
+ * thread = notmuch_threads_get (threads);
+ * ....
+ * notmuch_thread_destroy (thread);
+ * }
+ *
+ * notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a thread before its containing
+ * query, you can call notmuch_thread_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your thread objects
+ * are long-lived, then you don't need to call notmuch_thread_destroy
+ * and all the memory will still be reclaimed when the query is
+ * destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_threads_t object. (For consistency, we do provide a
+ * notmuch_threads_destroy function, but there's no good reason
+ * to call it if the query is about to be destroyed).
+ */
+notmuch_threads_t *
+notmuch_query_search_threads (notmuch_query_t *query);
+
+/* Execute a query for messages, returning a notmuch_messages_t object
+ * which can be used to iterate over the results. The returned
+ * messages object is owned by the query and as such, will only be
+ * valid until notmuch_query_destroy.
+ *
+ * Typical usage might be:
+ *
+ * notmuch_query_t *query;
+ * notmuch_messages_t *messages;
+ * notmuch_message_t *message;
+ *
+ * query = notmuch_query_create (database, query_string);
+ *
+ * for (messages = notmuch_query_search_messages (query);
+ * notmuch_messages_valid (messages);
+ * notmuch_messages_move_to_next (messages))
+ * {
+ * message = notmuch_messages_get (messages);
+ * ....
+ * notmuch_message_destroy (message);
+ * }
+ *
+ * notmuch_query_destroy (query);
+ *
+ * Note: If you are finished with a message before its containing
+ * query, you can call notmuch_message_destroy to clean up some memory
+ * sooner (as in the above example). Otherwise, if your message
+ * objects are long-lived, then you don't need to call
+ * notmuch_message_destroy and all the memory will still be reclaimed
+ * when the query is destroyed.
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_messages_t object. (For consistency, we do provide a
+ * notmuch_messages_destroy function, but there's no good
+ * reason to call it if the query is about to be destroyed).
+ */
+notmuch_messages_t *
+notmuch_query_search_messages (notmuch_query_t *query);
+
+/* Destroy a notmuch_query_t along with any associated resources.
+ *
+ * This will in turn destroy any notmuch_threads_t and
+ * notmuch_messages_t objects generated by this query, (and in
+ * turn any notmuch_thread_t and notmuch_message_t objects generated
+ * from those results, etc.), if such objects haven't already been
+ * destroyed.
+ */
+void
+notmuch_query_destroy (notmuch_query_t *query);
+
+/* Is the given 'threads' iterator pointing at a valid thread.
+ *
+ * When this function returns TRUE, notmuch_threads_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_threads_get will return NULL.
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+notmuch_bool_t
+notmuch_threads_valid (notmuch_threads_t *threads);
+
+/* Get the current thread from 'threads' as a notmuch_thread_t.
+ *
+ * Note: The returned thread belongs to 'threads' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+notmuch_thread_t *
+notmuch_threads_get (notmuch_threads_t *threads);
+
+/* Move the 'threads' iterator to the next thread.
+ *
+ * If 'threads' is already pointing at the last thread then the
+ * iterator will be moved to a point just beyond that last thread,
+ * (where notmuch_threads_valid will return FALSE and
+ * notmuch_threads_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_threads for example
+ * code showing how to iterate over a notmuch_threads_t object.
+ */
+void
+notmuch_threads_move_to_next (notmuch_threads_t *threads);
+
+/* Destroy a notmuch_threads_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_threads_t object will be reclaimed when the
+ * containg query object is destroyed.
+ */
+void
+notmuch_threads_destroy (notmuch_threads_t *threads);
+
+/* Return an estimate of the number of messages matching a search
+ *
+ * This function performs a search and returns Xapian's best
+ * guess as to number of matching messages.
+ */
+unsigned
+notmuch_query_count_messages (notmuch_query_t *query);
+
+/* Get the thread ID of 'thread'.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_thread_id (notmuch_thread_t *thread);
+
+/* Get the total number of messages in 'thread'.
+ *
+ * This count consists of all messages in the database belonging to
+ * this thread. Contrast with notmuch_thread_get_matched_messages() .
+ */
+int
+notmuch_thread_get_total_messages (notmuch_thread_t *thread);
+
+/* Get a notmuch_messages_t iterator for the top-level messages in
+ * 'thread'.
+ *
+ * This iterator will not necessarily iterate over all of the messages
+ * in the thread. It will only iterate over the messages in the thread
+ * which are not replies to other messages in the thread.
+ *
+ * To iterate over all messages in the thread, the caller will need to
+ * iterate over the result of notmuch_message_get_replies for each
+ * top-level message (and do that recursively for the resulting
+ * messages, etc.).
+ */
+notmuch_messages_t *
+notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread);
+
+/* Get the number of messages in 'thread' that matched the search.
+ *
+ * This count includes only the messages in this thread that were
+ * matched by the search from which the thread was created. Contrast
+ * with notmuch_thread_get_total_messages() .
+ */
+int
+notmuch_thread_get_matched_messages (notmuch_thread_t *thread);
+
+/* Get the authors of 'thread'
+ *
+ * The returned string is a comma-separated list of the names of the
+ * authors of mail messages in the query results that belong to this
+ * thread.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_authors (notmuch_thread_t *thread);
+
+/* Get the subject of 'thread'
+ *
+ * The subject is taken from the first message (according to the query
+ * order---see notmuch_query_set_sort) in the query results that
+ * belongs to this thread.
+ *
+ * The returned string belongs to 'thread' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * thread is valid, (which is until notmuch_thread_destroy or until
+ * the query from which it derived is destroyed).
+ */
+const char *
+notmuch_thread_get_subject (notmuch_thread_t *thread);
+
+/* Get the date of the oldest message in 'thread' as a time_t value.
+ */
+time_t
+notmuch_thread_get_oldest_date (notmuch_thread_t *thread);
+
+/* Get the date of the newest message in 'thread' as a time_t value.
+ */
+time_t
+notmuch_thread_get_newest_date (notmuch_thread_t *thread);
+
+/* Get the tags for 'thread', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * Note: In the Notmuch database, tags are stored on individual
+ * messages, not on threads. So the tags returned here will be all
+ * tags of the messages which matched the search and which belong to
+ * this thread.
+ *
+ * The tags object is owned by the thread and as such, will only be
+ * valid for as long as the thread is valid, (for example, until
+ * notmuch_thread_destroy or until the query from which it derived is
+ * destroyed).
+ *
+ * Typical usage might be:
+ *
+ * notmuch_thread_t *thread;
+ * notmuch_tags_t *tags;
+ * const char *tag;
+ *
+ * thread = notmuch_threads_get (threads);
+ *
+ * for (tags = notmuch_thread_get_tags (thread);
+ * notmuch_tags_valid (tags);
+ * notmuch_result_move_to_next (tags))
+ * {
+ * tag = notmuch_tags_get (tags);
+ * ....
+ * }
+ *
+ * notmuch_thread_destroy (thread);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+notmuch_tags_t *
+notmuch_thread_get_tags (notmuch_thread_t *thread);
+
+/* Destroy a notmuch_thread_t object. */
+void
+notmuch_thread_destroy (notmuch_thread_t *thread);
+
+/* Is the given 'messages' iterator pointing at a valid message.
+ *
+ * When this function returns TRUE, notmuch_messages_get will return a
+ * valid object. Whereas when this function returns FALSE,
+ * notmuch_messages_get will return NULL.
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+notmuch_bool_t
+notmuch_messages_valid (notmuch_messages_t *messages);
+
+/* Get the current message from 'messages' as a notmuch_message_t.
+ *
+ * Note: The returned message belongs to 'messages' and has a lifetime
+ * identical to it (and the query to which it belongs).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ *
+ * If an out-of-memory situation occurs, this function will return
+ * NULL.
+ */
+notmuch_message_t *
+notmuch_messages_get (notmuch_messages_t *messages);
+
+/* Move the 'messages' iterator to the next message.
+ *
+ * If 'messages' is already pointing at the last message then the
+ * iterator will be moved to a point just beyond that last message,
+ * (where notmuch_messages_valid will return FALSE and
+ * notmuch_messages_get will return NULL).
+ *
+ * See the documentation of notmuch_query_search_messages for example
+ * code showing how to iterate over a notmuch_messages_t object.
+ */
+void
+notmuch_messages_move_to_next (notmuch_messages_t *messages);
+
+/* Destroy a notmuch_messages_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_messages_t object will be reclaimed when the containing
+ * query object is destroyed.
+ */
+void
+notmuch_messages_destroy (notmuch_messages_t *messages);
+
+/* Return a list of tags from all messages.
+ *
+ * The resulting list is guaranteed not to contain duplicated tags.
+ *
+ * WARNING: You can no longer iterate over messages after calling this
+ * function, because the iterator will point at the end of the list.
+ * We do not have a function to reset the iterator yet and the only
+ * way how you can iterate over the list again is to recreate the
+ * message list.
+ *
+ * The function returns NULL on error.
+ */
+notmuch_tags_t *
+notmuch_messages_collect_tags (notmuch_messages_t *messages);
+
+/* Get the message ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (which is until the query from which it derived
+ * is destroyed).
+ *
+ * This function will not return NULL since Notmuch ensures that every
+ * message has a unique message ID, (Notmuch will generate an ID for a
+ * message if the original file does not contain one).
+ */
+const char *
+notmuch_message_get_message_id (notmuch_message_t *message);
+
+/* Get the thread ID of 'message'.
+ *
+ * The returned string belongs to 'message' and as such, should not be
+ * modified by the caller and will only be valid for as long as the
+ * message is valid, (for example, until the user calls
+ * notmuch_message_destroy on 'message' or until a query from which it
+ * derived is destroyed).
+ *
+ * This function will not return NULL since Notmuch ensures that every
+ * message belongs to a single thread.
+ */
+const char *
+notmuch_message_get_thread_id (notmuch_message_t *message);
+
+/* Get a notmuch_messages_t iterator for all of the replies to
+ * 'message'.
+ *
+ * Note: This call only makes sense if 'message' was ultimately
+ * obtained from a notmuch_thread_t object, (such as by coming
+ * directly from the result of calling notmuch_thread_get_
+ * toplevel_messages or by any number of subsequent
+ * calls to notmuch_message_get_replies).
+ *
+ * If 'message' was obtained through some non-thread means, (such as
+ * by a call to notmuch_query_search_messages), then this function
+ * will return NULL.
+ *
+ * If there are no replies to 'message', this function will return
+ * NULL. (Note that notmuch_messages_valid will accept that NULL
+ * value as legitimate, and simply return FALSE for it.)
+ */
+notmuch_messages_t *
+notmuch_message_get_replies (notmuch_message_t *message);
+
+/* Get a filename for the email corresponding to 'message'.
+ *
+ * The returned filename is an absolute filename, (the initial
+ * component will match notmuch_database_get_path() ).
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Note: If this message corresponds to multiple files in the mail
+ * store, (that is, multiple files contain identical message IDs),
+ * this function will arbitrarily return a single one of those
+ * filenames.
+ */
+const char *
+notmuch_message_get_filename (notmuch_message_t *message);
+
+/* Message flags */
+typedef enum _notmuch_message_flag {
+ NOTMUCH_MESSAGE_FLAG_MATCH,
+} notmuch_message_flag_t;
+
+/* Get a value of a flag for the email corresponding to 'message'. */
+notmuch_bool_t
+notmuch_message_get_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag);
+
+/* Set a value of a flag for the email corresponding to 'message'. */
+void
+notmuch_message_set_flag (notmuch_message_t *message,
+ notmuch_message_flag_t flag, notmuch_bool_t value);
+
+/* Get the date of 'message' as a time_t value.
+ *
+ * For the original textual representation of the Date header from the
+ * message call notmuch_message_get_header() with a header value of
+ * "date". */
+time_t
+notmuch_message_get_date (notmuch_message_t *message);
+
+/* Get the value of the specified header from 'message'.
+ *
+ * The value will be read from the actual message file, not from the
+ * notmuch database. The header name is case insensitive.
+ *
+ * The returned string belongs to the message so should not be
+ * modified or freed by the caller (nor should it be referenced after
+ * the message is destroyed).
+ *
+ * Returns an empty string ("") if the message does not contain a
+ * header line matching 'header'. Returns NULL if any error occurs.
+ */
+const char *
+notmuch_message_get_header (notmuch_message_t *message, const char *header);
+
+/* Get the tags for 'message', returning a notmuch_tags_t object which
+ * can be used to iterate over all tags.
+ *
+ * The tags object is owned by the message and as such, will only be
+ * valid for as long as the message is valid, (which is until the
+ * query from which it derived is destroyed).
+ *
+ * Typical usage might be:
+ *
+ * notmuch_message_t *message;
+ * notmuch_tags_t *tags;
+ * const char *tag;
+ *
+ * message = notmuch_database_find_message (database, message_id);
+ *
+ * for (tags = notmuch_message_get_tags (message);
+ * notmuch_tags_valid (tags);
+ * notmuch_result_move_to_next (tags))
+ * {
+ * tag = notmuch_tags_get (tags);
+ * ....
+ * }
+ *
+ * notmuch_message_destroy (message);
+ *
+ * Note that there's no explicit destructor needed for the
+ * notmuch_tags_t object. (For consistency, we do provide a
+ * notmuch_tags_destroy function, but there's no good reason to call
+ * it if the message is about to be destroyed).
+ */
+notmuch_tags_t *
+notmuch_message_get_tags (notmuch_message_t *message);
+
+/* The longest possible tag value. */
+#define NOTMUCH_TAG_MAX 200
+
+/* Add a tag to the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully added to message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ * (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_add_tag (notmuch_message_t *message, const char *tag);
+
+/* Remove a tag from the given message.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Tag successfully removed from message
+ *
+ * NOTMUCH_STATUS_NULL_POINTER: The 'tag' argument is NULL
+ *
+ * NOTMUCH_STATUS_TAG_TOO_LONG: The length of 'tag' is too long
+ * (exceeds NOTMUCH_TAG_MAX)
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);
+
+/* Remove all tags from the given message.
+ *
+ * See notmuch_message_freeze for an example showing how to safely
+ * replace tag values.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_remove_all_tags (notmuch_message_t *message);
+
+/* Freeze the current state of 'message' within the database.
+ *
+ * This means that changes to the message state, (via
+ * notmuch_message_add_tag, notmuch_message_remove_tag, and
+ * notmuch_message_remove_all_tags), will not be committed to the
+ * database until the message is thawed with notmuch_message_thaw.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls will
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * The ability to do freeze/thaw allows for safe transactions to
+ * change tag values. For example, explicitly setting a message to
+ * have a given set of tags might look like this:
+ *
+ * notmuch_message_freeze (message);
+ *
+ * notmuch_message_remove_all_tags (message);
+ *
+ * for (i = 0; i < NUM_TAGS; i++)
+ * notmuch_message_add_tag (message, tags[i]);
+ *
+ * notmuch_message_thaw (message);
+ *
+ * With freeze/thaw used like this, the message in the database is
+ * guaranteed to have either the full set of original tag values, or
+ * the full set of new tag values, but nothing in between.
+ *
+ * Imagine the example above without freeze/thaw and the operation
+ * somehow getting interrupted. This could result in the message being
+ * left with no tags if the interruption happened after
+ * notmuch_message_remove_all_tags but before notmuch_message_add_tag.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully frozen.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so message cannot be modified.
+ */
+notmuch_status_t
+notmuch_message_freeze (notmuch_message_t *message);
+
+/* Thaw the current 'message', synchronizing any changes that may have
+ * occurred while 'message' was frozen into the notmuch database.
+ *
+ * See notmuch_message_freeze for an example of how to use this
+ * function to safely provide tag changes.
+ *
+ * Multiple calls to freeze/thaw are valid and these calls with
+ * "stack". That is there must be as many calls to thaw as to freeze
+ * before a message is actually thawed.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: Message successfully thawed, (or at least
+ * its frozen count has successfully been reduced by 1).
+ *
+ * NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW: An attempt was made to thaw
+ * an unfrozen message. That is, there have been an unbalanced
+ * number of calls to notmuch_message_freeze and
+ * notmuch_message_thaw.
+ */
+notmuch_status_t
+notmuch_message_thaw (notmuch_message_t *message);
+
+/* Destroy a notmuch_message_t object.
+ *
+ * It can be useful to call this function in the case of a single
+ * query object with many messages in the result, (such as iterating
+ * over the entire database). Otherwise, it's fine to never call this
+ * function and there will still be no memory leaks. (The memory from
+ * the messages get reclaimed when the containing query is destroyed.)
+ */
+void
+notmuch_message_destroy (notmuch_message_t *message);
+
+/* Is the given 'tags' iterator pointing at a valid tag.
+ *
+ * When this function returns TRUE, notmuch_tags_get will return a
+ * valid string. Whereas when this function returns FALSE,
+ * notmuch_tags_get will return NULL.
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+notmuch_bool_t
+notmuch_tags_valid (notmuch_tags_t *tags);
+
+/* Get the current tag from 'tags' as a string.
+ *
+ * Note: The returned string belongs to 'tags' and has a lifetime
+ * identical to it (and the query to which it ultimately belongs).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+const char *
+notmuch_tags_get (notmuch_tags_t *tags);
+
+/* Move the 'tags' iterator to the next tag.
+ *
+ * If 'tags' is already pointing at the last tag then the iterator
+ * will be moved to a point just beyond that last tag, (where
+ * notmuch_tags_valid will return FALSE and notmuch_tags_get will
+ * return NULL).
+ *
+ * See the documentation of notmuch_message_get_tags for example code
+ * showing how to iterate over a notmuch_tags_t object.
+ */
+void
+notmuch_tags_move_to_next (notmuch_tags_t *tags);
+
+/* Destroy a notmuch_tags_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_tags_t object will be reclaimed when the containing
+ * message or query objects are destroyed.
+ */
+void
+notmuch_tags_destroy (notmuch_tags_t *tags);
+
+/* Store an mtime within the database for 'directory'.
+ *
+ * The 'directory' should be an object retrieved from the database
+ * with notmuch_database_get_directory for a particular path.
+ *
+ * The intention is for the caller to use the mtime to allow efficient
+ * identification of new messages to be added to the database. The
+ * recommended usage is as follows:
+ *
+ * o Read the mtime of a directory from the filesystem
+ *
+ * o Call add_message for all mail files in the directory
+ *
+ * o Call notmuch_directory_set_mtime with the mtime read from the
+ * filesystem.
+ *
+ * Then, when wanting to check for updates to the directory in the
+ * future, the client can call notmuch_directory_get_mtime and know
+ * that it only needs to add files if the mtime of the directory and
+ * files are newer than the stored timestamp.
+ *
+ * Note: The notmuch_directory_get_mtime function does not allow the
+ * caller to distinguish a timestamp of 0 from a non-existent
+ * timestamp. So don't store a timestamp of 0 unless you are
+ * comfortable with that.
+ *
+ * Return value:
+ *
+ * NOTMUCH_STATUS_SUCCESS: mtime successfully stored in database.
+ *
+ * NOTMUCH_STATUS_XAPIAN_EXCEPTION: A Xapian exception
+ * occurred, mtime not stored.
+ *
+ * NOTMUCH_STATUS_READ_ONLY_DATABASE: Database was opened in read-only
+ * mode so directory mtime cannot be modified.
+ */
+notmuch_status_t
+notmuch_directory_set_mtime (notmuch_directory_t *directory,
+ time_t mtime);
+
+/* Get the mtime of a directory, (as previously stored with
+ * notmuch_directory_set_mtime).
+ *
+ * Returns 0 if no mtime has previously been stored for this
+ * directory.*/
+time_t
+notmuch_directory_get_mtime (notmuch_directory_t *directory);
+
+/* Get a notmuch_filenames_t iterator listing all the filenames of
+ * messages in the database within the given directory.
+ *
+ * The returned filenames will be the basename-entries only (not
+ * complete paths). */
+notmuch_filenames_t *
+notmuch_directory_get_child_files (notmuch_directory_t *directory);
+
+/* Get a notmuch_filenams_t iterator listing all the filenames of
+ * sub-directories in the database within the given directory.
+ *
+ * The returned filenames will be the basename-entries only (not
+ * complete paths). */
+notmuch_filenames_t *
+notmuch_directory_get_child_directories (notmuch_directory_t *directory);
+
+/* Destroy a notmuch_directory_t object. */
+void
+notmuch_directory_destroy (notmuch_directory_t *directory);
+
+/* Is the given 'filenames' iterator pointing at a valid filename.
+ *
+ * When this function returns TRUE, notmuch_filenames_get will return
+ * a valid string. Whereas when this function returns FALSE,
+ * notmuch_filenames_get will return NULL.
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will always return FALSE.
+ */
+notmuch_bool_t
+notmuch_filenames_valid (notmuch_filenames_t *filenames);
+
+/* Get the current filename from 'filenames' as a string.
+ *
+ * Note: The returned string belongs to 'filenames' and has a lifetime
+ * identical to it (and the directory to which it ultimately belongs).
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will always return NULL.
+ */
+const char *
+notmuch_filenames_get (notmuch_filenames_t *filenames);
+
+/* Move the 'filenames' iterator to the next filename.
+ *
+ * If 'filenames' is already pointing at the last filename then the
+ * iterator will be moved to a point just beyond that last filename,
+ * (where notmuch_filenames_valid will return FALSE and
+ * notmuch_filenames_get will return NULL).
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will do nothing.
+ */
+void
+notmuch_filenames_move_to_next (notmuch_filenames_t *filenames);
+
+/* Destroy a notmuch_filenames_t object.
+ *
+ * It's not strictly necessary to call this function. All memory from
+ * the notmuch_filenames_t object will be reclaimed when the
+ * containing directory object is destroyed.
+ *
+ * It is acceptable to pass NULL for 'filenames', in which case this
+ * function will do nothing.
+ */
+void
+notmuch_filenames_destroy (notmuch_filenames_t *filenames);
+
+NOTMUCH_END_DECLS
+
+#endif
diff --git a/lib/query.cc b/lib/query.cc
new file mode 100644
index 0000000..7e1b6b5
--- /dev/null
+++ b/lib/query.cc
@@ -0,0 +1,372 @@
+/* query.cc - Support for searching 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 <glib.h> /* GHashTable, GPtrArray */
+
+#include <xapian.h>
+
+struct _notmuch_query {
+ notmuch_database_t *notmuch;
+ const char *query_string;
+ notmuch_sort_t sort;
+};
+
+typedef struct _notmuch_mset_messages {
+ notmuch_messages_t base;
+ notmuch_database_t *notmuch;
+ Xapian::MSetIterator iterator;
+ Xapian::MSetIterator iterator_end;
+} notmuch_mset_messages_t;
+
+struct _notmuch_threads {
+ notmuch_query_t *query;
+ GHashTable *threads;
+ notmuch_messages_t *messages;
+
+ /* This thread ID is our iterator state. */
+ const char *thread_id;
+};
+
+notmuch_query_t *
+notmuch_query_create (notmuch_database_t *notmuch,
+ const char *query_string)
+{
+ notmuch_query_t *query;
+
+#ifdef DEBUG_QUERY
+ fprintf (stderr, "Query string is:\n%s\n", query_string);
+#endif
+
+ query = talloc (NULL, notmuch_query_t);
+ if (unlikely (query == NULL))
+ return NULL;
+
+ query->notmuch = notmuch;
+
+ query->query_string = talloc_strdup (query, query_string);
+
+ query->sort = NOTMUCH_SORT_NEWEST_FIRST;
+
+ return query;
+}
+
+void
+notmuch_query_set_sort (notmuch_query_t *query, notmuch_sort_t sort)
+{
+ query->sort = sort;
+}
+
+/* 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_messages_destructor (notmuch_mset_messages_t *messages)
+{
+ messages->iterator.~MSetIterator ();
+ messages->iterator_end.~MSetIterator ();
+
+ return 0;
+}
+
+notmuch_messages_t *
+notmuch_query_search_messages (notmuch_query_t *query)
+{
+ notmuch_database_t *notmuch = query->notmuch;
+ const char *query_string = query->query_string;
+ notmuch_mset_messages_t *messages;
+
+ messages = talloc (query, notmuch_mset_messages_t);
+ if (unlikely (messages == NULL))
+ return NULL;
+
+ try {
+
+ messages->base.is_of_list_type = FALSE;
+ messages->base.iterator = NULL;
+ messages->notmuch = notmuch;
+ new (&messages->iterator) Xapian::MSetIterator ();
+ new (&messages->iterator_end) Xapian::MSetIterator ();
+
+ talloc_set_destructor (messages, _notmuch_messages_destructor);
+
+ Xapian::Enquire enquire (*notmuch->xapian_db);
+ Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
+ _find_prefix ("type"),
+ "mail"));
+ Xapian::Query string_query, final_query;
+ Xapian::MSet mset;
+ unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
+ Xapian::QueryParser::FLAG_PHRASE |
+ Xapian::QueryParser::FLAG_LOVEHATE |
+ Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
+ Xapian::QueryParser::FLAG_WILDCARD |
+ Xapian::QueryParser::FLAG_PURE_NOT);
+
+ if (strcmp (query_string, "") == 0 ||
+ strcmp (query_string, "*") == 0)
+ {
+ final_query = mail_query;
+ } else {
+ string_query = notmuch->query_parser->
+ parse_query (query_string, flags);
+ final_query = Xapian::Query (Xapian::Query::OP_AND,
+ mail_query, string_query);
+ }
+
+ enquire.set_weighting_scheme (Xapian::BoolWeight());
+
+ switch (query->sort) {
+ case NOTMUCH_SORT_OLDEST_FIRST:
+ enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, FALSE);
+ break;
+ case NOTMUCH_SORT_NEWEST_FIRST:
+ enquire.set_sort_by_value (NOTMUCH_VALUE_TIMESTAMP, TRUE);
+ break;
+ case NOTMUCH_SORT_MESSAGE_ID:
+ enquire.set_sort_by_value (NOTMUCH_VALUE_MESSAGE_ID, FALSE);
+ break;
+ case NOTMUCH_SORT_UNSORTED:
+ break;
+ }
+
+#if DEBUG_QUERY
+ fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
+#endif
+
+ enquire.set_query (final_query);
+
+ mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+ messages->iterator = mset.begin ();
+ messages->iterator_end = mset.end ();
+
+ } catch (const Xapian::Error &error) {
+ fprintf (stderr, "A Xapian exception occurred performing query: %s\n",
+ error.get_msg().c_str());
+ fprintf (stderr, "Query string was: %s\n", query->query_string);
+ notmuch->exception_reported = TRUE;
+ }
+
+ return &messages->base;
+}
+
+notmuch_bool_t
+_notmuch_mset_messages_valid (notmuch_messages_t *messages)
+{
+ notmuch_mset_messages_t *mset_messages;
+
+ mset_messages = (notmuch_mset_messages_t *) messages;
+
+ return (mset_messages->iterator != mset_messages->iterator_end);
+}
+
+notmuch_message_t *
+_notmuch_mset_messages_get (notmuch_messages_t *messages)
+{
+ notmuch_message_t *message;
+ Xapian::docid doc_id;
+ notmuch_private_status_t status;
+ notmuch_mset_messages_t *mset_messages;
+
+ mset_messages = (notmuch_mset_messages_t *) messages;
+
+ if (! _notmuch_mset_messages_valid (&mset_messages->base))
+ return NULL;
+
+ doc_id = *mset_messages->iterator;
+
+ message = _notmuch_message_create (mset_messages,
+ mset_messages->notmuch, doc_id,
+ &status);
+
+ if (message == NULL &&
+ status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND)
+ {
+ INTERNAL_ERROR ("a messages iterator contains a non-existent document ID.\n");
+ }
+
+ return message;
+}
+
+void
+_notmuch_mset_messages_move_to_next (notmuch_messages_t *messages)
+{
+ notmuch_mset_messages_t *mset_messages;
+
+ mset_messages = (notmuch_mset_messages_t *) messages;
+
+ mset_messages->iterator++;
+}
+
+/* Glib objects force use to use a talloc destructor as well, (but not
+ * nearly as ugly as the for messages due to C++ objects). At
+ * this point, I'd really like to have some talloc-friendly
+ * equivalents for the few pieces of glib that I'm using. */
+static int
+_notmuch_threads_destructor (notmuch_threads_t *threads)
+{
+ g_hash_table_unref (threads->threads);
+
+ return 0;
+}
+
+notmuch_threads_t *
+notmuch_query_search_threads (notmuch_query_t *query)
+{
+ notmuch_threads_t *threads;
+
+ threads = talloc (query, notmuch_threads_t);
+ if (threads == NULL)
+ return NULL;
+
+ threads->query = query;
+ threads->threads = g_hash_table_new_full (g_str_hash, g_str_equal,
+ free, NULL);
+
+ threads->messages = notmuch_query_search_messages (query);
+
+ threads->thread_id = NULL;
+
+ talloc_set_destructor (threads, _notmuch_threads_destructor);
+
+ return threads;
+}
+
+void
+notmuch_query_destroy (notmuch_query_t *query)
+{
+ talloc_free (query);
+}
+
+notmuch_bool_t
+notmuch_threads_valid (notmuch_threads_t *threads)
+{
+ notmuch_message_t *message;
+
+ if (threads->thread_id)
+ return TRUE;
+
+ while (notmuch_messages_valid (threads->messages))
+ {
+ message = notmuch_messages_get (threads->messages);
+
+ threads->thread_id = notmuch_message_get_thread_id (message);
+
+ if (! g_hash_table_lookup_extended (threads->threads,
+ threads->thread_id,
+ NULL, NULL))
+ {
+ g_hash_table_insert (threads->threads,
+ xstrdup (threads->thread_id), NULL);
+ notmuch_messages_move_to_next (threads->messages);
+ return TRUE;
+ }
+
+ notmuch_messages_move_to_next (threads->messages);
+ }
+
+ threads->thread_id = NULL;
+ return FALSE;
+}
+
+notmuch_thread_t *
+notmuch_threads_get (notmuch_threads_t *threads)
+{
+ if (! notmuch_threads_valid (threads))
+ return NULL;
+
+ return _notmuch_thread_create (threads->query,
+ threads->query->notmuch,
+ threads->thread_id,
+ threads->query->query_string,
+ threads->query->sort);
+}
+
+void
+notmuch_threads_move_to_next (notmuch_threads_t *threads)
+{
+ threads->thread_id = NULL;
+}
+
+void
+notmuch_threads_destroy (notmuch_threads_t *threads)
+{
+ talloc_free (threads);
+}
+
+unsigned
+notmuch_query_count_messages (notmuch_query_t *query)
+{
+ notmuch_database_t *notmuch = query->notmuch;
+ const char *query_string = query->query_string;
+ Xapian::doccount count = 0;
+
+ try {
+ Xapian::Enquire enquire (*notmuch->xapian_db);
+ Xapian::Query mail_query (talloc_asprintf (query, "%s%s",
+ _find_prefix ("type"),
+ "mail"));
+ Xapian::Query string_query, final_query;
+ Xapian::MSet mset;
+ unsigned int flags = (Xapian::QueryParser::FLAG_BOOLEAN |
+ Xapian::QueryParser::FLAG_PHRASE |
+ Xapian::QueryParser::FLAG_LOVEHATE |
+ Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE |
+ Xapian::QueryParser::FLAG_WILDCARD |
+ Xapian::QueryParser::FLAG_PURE_NOT);
+
+ if (strcmp (query_string, "") == 0 ||
+ strcmp (query_string, "*") == 0)
+ {
+ final_query = mail_query;
+ } else {
+ string_query = notmuch->query_parser->
+ parse_query (query_string, flags);
+ final_query = Xapian::Query (Xapian::Query::OP_AND,
+ mail_query, string_query);
+ }
+
+ enquire.set_weighting_scheme(Xapian::BoolWeight());
+ enquire.set_docid_order(Xapian::Enquire::ASCENDING);
+
+#if DEBUG_QUERY
+ fprintf (stderr, "Final query is:\n%s\n", final_query.get_description().c_str());
+#endif
+
+ enquire.set_query (final_query);
+
+ mset = enquire.get_mset (0, notmuch->xapian_db->get_doccount ());
+
+ count = mset.get_matches_estimated();
+
+ } catch (const Xapian::Error &error) {
+ fprintf (stderr, "A Xapian exception occurred: %s\n",
+ error.get_msg().c_str());
+ fprintf (stderr, "Query string was: %s\n", query->query_string);
+ }
+
+ return count;
+}
diff --git a/lib/sha1.c b/lib/sha1.c
new file mode 100644
index 0000000..cc48108
--- /dev/null
+++ b/lib/sha1.c
@@ -0,0 +1,115 @@
+/* sha1.c - Interfaces to SHA-1 hash for the notmuch mail system
+ *
+ * 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 "libsha1.h"
+
+/* Just some simple interfaces on top of libsha1 so that we can leave
+ * libsha1 as untouched as possible. */
+
+static char *
+_hex_of_sha1_digest (const unsigned char digest[SHA1_DIGEST_SIZE])
+{
+ char *result, *r;
+ int i;
+
+ result = xcalloc (SHA1_DIGEST_SIZE * 2 + 1, 1);
+
+ for (r = result, i = 0;
+ i < SHA1_DIGEST_SIZE;
+ r += 2, i++)
+ {
+ sprintf (r, "%02x", digest[i]);
+ }
+
+ return result;
+}
+
+/* Create a hexadecimal string version of the SHA-1 digest of 'str'
+ * (including its null terminating character).
+ *
+ * This function returns a newly allocated string which the caller
+ * should free() when finished.
+ */
+char *
+notmuch_sha1_of_string (const char *str)
+{
+ sha1_ctx sha1;
+ unsigned char digest[SHA1_DIGEST_SIZE];
+
+ sha1_begin (&sha1);
+
+ sha1_hash ((unsigned char *) str, strlen (str) + 1, &sha1);
+
+ sha1_end (digest, &sha1);
+
+ return _hex_of_sha1_digest (digest);
+}
+
+/* Create a hexadecimal string version of the SHA-1 digest of the
+ * contents of the named file.
+ *
+ * This function returns a newly allocated string which the caller
+ * should free() when finished.
+ *
+ * If any error occurs while reading the file, (permission denied,
+ * file not found, etc.), this function returns NULL.
+ */
+char *
+notmuch_sha1_of_file (const char *filename)
+{
+ FILE *file;
+#define BLOCK_SIZE 4096
+ unsigned char block[BLOCK_SIZE];
+ size_t bytes_read;
+ sha1_ctx sha1;
+ unsigned char digest[SHA1_DIGEST_SIZE];
+ char *result;
+
+ file = fopen (filename, "r");
+ if (file == NULL)
+ return NULL;
+
+ sha1_begin (&sha1);
+
+ while (1) {
+ bytes_read = fread (block, 1, 4096, file);
+ if (bytes_read == 0) {
+ if (feof (file)) {
+ break;
+ } else if (ferror (file)) {
+ fclose (file);
+ return NULL;
+ }
+ } else {
+ sha1_hash (block, bytes_read, &sha1);
+ }
+ }
+
+ sha1_end (digest, &sha1);
+
+ result = _hex_of_sha1_digest (digest);
+
+ fclose (file);
+
+ return result;
+}
+
diff --git a/lib/tags.c b/lib/tags.c
new file mode 100644
index 0000000..8fe4a3f
--- /dev/null
+++ b/lib/tags.c
@@ -0,0 +1,120 @@
+/* tags.c - Iterator for tags returned from message or thread
+ *
+ * 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 <glib.h> /* GList */
+
+struct _notmuch_tags {
+ int sorted;
+ GList *tags;
+ GList *iterator;
+};
+
+/* XXX: Should write some talloc-friendly list to avoid the need for
+ * this. */
+static int
+_notmuch_tags_destructor (notmuch_tags_t *tags)
+{
+ g_list_free (tags->tags);
+
+ return 0;
+}
+
+/* Create a new notmuch_tags_t object, with 'ctx' as its talloc owner.
+ *
+ * This function can return NULL in case of out-of-memory.
+ */
+notmuch_tags_t *
+_notmuch_tags_create (void *ctx)
+{
+ notmuch_tags_t *tags;
+
+ tags = talloc (ctx, notmuch_tags_t);
+ if (unlikely (tags == NULL))
+ return NULL;
+
+ talloc_set_destructor (tags, _notmuch_tags_destructor);
+
+ tags->sorted = 1;
+ tags->tags = NULL;
+ tags->iterator = NULL;
+
+ return tags;
+}
+
+/* Add a new tag to 'tags'. The tags object will create its own copy
+ * of the string.
+ *
+ * Note: The tags object will not do anything to prevent duplicate
+ * tags being stored, so the caller really shouldn't pass
+ * duplicates. */
+void
+_notmuch_tags_add_tag (notmuch_tags_t *tags, const char *tag)
+{
+ tags->tags = g_list_prepend (tags->tags, talloc_strdup (tags, tag));
+ tags->sorted = 0;
+}
+
+/* Prepare 'tag' for iteration.
+ *
+ * The internal creator of 'tags' should call this function before
+ * returning 'tags' to the user to call the public functions such as
+ * notmuch_tags_valid, notmuch_tags_get, and
+ * notmuch_tags_move_to_next. */
+void
+_notmuch_tags_prepare_iterator (notmuch_tags_t *tags)
+{
+ if (! tags->sorted)
+ tags->tags = g_list_sort (tags->tags, (GCompareFunc) strcmp);
+ tags->sorted = 1;
+
+ tags->iterator = tags->tags;
+}
+
+notmuch_bool_t
+notmuch_tags_valid (notmuch_tags_t *tags)
+{
+ return tags->iterator != NULL;
+}
+
+const char *
+notmuch_tags_get (notmuch_tags_t *tags)
+{
+ if (tags->iterator == NULL)
+ return NULL;
+
+ return (char *) tags->iterator->data;
+}
+
+void
+notmuch_tags_move_to_next (notmuch_tags_t *tags)
+{
+ if (tags->iterator == NULL)
+ return;
+
+ tags->iterator = tags->iterator->next;
+}
+
+void
+notmuch_tags_destroy (notmuch_tags_t *tags)
+{
+ talloc_free (tags);
+}
diff --git a/lib/thread.cc b/lib/thread.cc
new file mode 100644
index 0000000..e514bf8
--- /dev/null
+++ b/lib/thread.cc
@@ -0,0 +1,427 @@
+/* thread.cc - Results of thread-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>
+
+#include <gmime/gmime.h>
+#include <glib.h> /* GHashTable */
+
+struct _notmuch_thread {
+ notmuch_database_t *notmuch;
+ char *thread_id;
+ char *subject;
+ GHashTable *authors_hash;
+ char *authors;
+ GHashTable *tags;
+
+ notmuch_message_list_t *message_list;
+ GHashTable *message_hash;
+ int total_messages;
+ int matched_messages;
+ time_t oldest;
+ time_t newest;
+};
+
+static int
+_notmuch_thread_destructor (notmuch_thread_t *thread)
+{
+ g_hash_table_unref (thread->authors_hash);
+ g_hash_table_unref (thread->tags);
+ g_hash_table_unref (thread->message_hash);
+
+ return 0;
+}
+
+static void
+_thread_add_author (notmuch_thread_t *thread,
+ const char *author)
+{
+ if (author == NULL)
+ return;
+
+ if (g_hash_table_lookup_extended (thread->authors_hash,
+ author, NULL, NULL))
+ return;
+
+ g_hash_table_insert (thread->authors_hash, xstrdup (author), NULL);
+
+ if (thread->authors)
+ thread->authors = talloc_asprintf (thread, "%s, %s",
+ thread->authors,
+ author);
+ else
+ thread->authors = talloc_strdup (thread, author);
+}
+
+/* Add 'message' as a message that belongs to 'thread'.
+ *
+ * The 'thread' will talloc_steal the 'message' and hold onto a
+ * reference to it.
+ */
+static void
+_thread_add_message (notmuch_thread_t *thread,
+ notmuch_message_t *message)
+{
+ notmuch_tags_t *tags;
+ const char *tag;
+ InternetAddressList *list;
+ InternetAddress *address;
+ const char *from, *author;
+
+ _notmuch_message_list_add_message (thread->message_list,
+ talloc_steal (thread, message));
+ thread->total_messages++;
+
+ g_hash_table_insert (thread->message_hash,
+ xstrdup (notmuch_message_get_message_id (message)),
+ message);
+
+ from = notmuch_message_get_header (message, "from");
+ list = internet_address_list_parse_string (from);
+ if (list) {
+ address = internet_address_list_get_address (list, 0);
+ if (address) {
+ author = internet_address_get_name (address);
+ if (author == NULL) {
+ InternetAddressMailbox *mailbox;
+ mailbox = INTERNET_ADDRESS_MAILBOX (address);
+ author = internet_address_mailbox_get_addr (mailbox);
+ }
+ _thread_add_author (thread, author);
+ }
+ g_object_unref (G_OBJECT (list));
+ }
+
+ for (tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ tag = notmuch_tags_get (tags);
+ g_hash_table_insert (thread->tags, xstrdup (tag), NULL);
+ }
+}
+
+static void
+_thread_add_matched_message (notmuch_thread_t *thread,
+ notmuch_message_t *message)
+{
+ time_t date;
+ notmuch_message_t *hashed_message;
+
+ date = notmuch_message_get_date (message);
+
+ if (date < thread->oldest || ! thread->matched_messages)
+ thread->oldest = date;
+
+ if (date > thread->newest || ! thread->matched_messages)
+ thread->newest = date;
+
+ if (! thread->subject) {
+ const char *subject;
+
+ subject = notmuch_message_get_header (message, "subject");
+
+ if ((strncasecmp (subject, "Re: ", 4) == 0) ||
+ (strncasecmp (subject, "Aw: ", 4) == 0) ||
+ (strncasecmp (subject, "Vs: ", 4) == 0) ||
+ (strncasecmp (subject, "Sv: ", 4) == 0))
+ {
+ thread->subject = talloc_strdup (thread, subject + 4);
+ }
+ else
+ {
+ thread->subject = talloc_strdup (thread, subject);
+ }
+ }
+
+ thread->matched_messages++;
+
+ if (g_hash_table_lookup_extended (thread->message_hash,
+ notmuch_message_get_message_id (message), NULL,
+ (void **) &hashed_message)) {
+ notmuch_message_set_flag (hashed_message,
+ NOTMUCH_MESSAGE_FLAG_MATCH, 1);
+ }
+}
+
+static void
+_resolve_thread_relationships (unused (notmuch_thread_t *thread))
+{
+ notmuch_message_node_t **prev, *node;
+ notmuch_message_t *message, *parent;
+ const char *in_reply_to;
+
+ prev = &thread->message_list->head;
+ while ((node = *prev)) {
+ message = node->message;
+ in_reply_to = _notmuch_message_get_in_reply_to (message);
+ if (in_reply_to && strlen (in_reply_to) &&
+ g_hash_table_lookup_extended (thread->message_hash,
+ in_reply_to, NULL,
+ (void **) &parent))
+ {
+ *prev = node->next;
+ if (thread->message_list->tail == &node->next)
+ thread->message_list->tail = prev;
+ node->next = NULL;
+ _notmuch_message_add_reply (parent, node);
+ } else {
+ prev = &((*prev)->next);
+ }
+ }
+
+ /* XXX: After scanning through the entire list looking for parents
+ * via "In-Reply-To", we should do a second pass that looks at the
+ * list of messages IDs in the "References" header instead. (And
+ * for this the parent would be the "deepest" message of all the
+ * messages found in the "References" list.)
+ *
+ * Doing this will allow messages and sub-threads to be positioned
+ * correctly in the thread even when an intermediate message is
+ * missing from the thread.
+ */
+}
+
+/* Create a new notmuch_thread_t object for the given thread ID,
+ * treating any messages matching 'query_string' as "matched".
+ *
+ * Creating the thread will trigger two database searches. The first
+ * is for all messages belonging to the thread, (to get the first
+ * subject line, the total count of messages, and all authors). The
+ * second search is for all messages that are in the thread and that
+ * also match the given query_string. This is to allow for a separate
+ * count of matched messages, and to allow a viewer to display these
+ * messages differently.
+ *
+ * Here, 'ctx' is talloc context for the resulting thread object.
+ *
+ * This function returns NULL in the case of any error.
+ */
+notmuch_thread_t *
+_notmuch_thread_create (void *ctx,
+ notmuch_database_t *notmuch,
+ const char *thread_id,
+ const char *query_string,
+ notmuch_sort_t sort)
+{
+ notmuch_thread_t *thread;
+ const char *thread_id_query_string;
+ notmuch_query_t *thread_id_query;
+
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ notmuch_bool_t matched_is_subset_of_thread;
+
+ thread_id_query_string = talloc_asprintf (ctx, "thread:%s", thread_id);
+ if (unlikely (query_string == NULL))
+ return NULL;
+
+ /* Under normal circumstances we need to do two database
+ * queries. One is for the thread itself (thread_id_query_string)
+ * and the second is to determine which messages in that thread
+ * match the original query (matched_query_string).
+ *
+ * But under two circumstances, we use only the
+ * thread_id_query_string:
+ *
+ * 1. If the original query_string *is* just the thread
+ * specification.
+ *
+ * 2. If the original query_string matches all messages ("" or
+ * "*").
+ *
+ * In either of these cases, we can be more efficient by running
+ * just the thread_id query (since we know all messages in the
+ * thread will match the query_string).
+ *
+ * Beyond the performance advantage, in the second case, it's
+ * important to not try to create a concatenated query because our
+ * parser handles "" and "*" as special cases and will not do the
+ * right thing with a query string of "* and thread:<foo>".
+ **/
+ matched_is_subset_of_thread = 1;
+ if (strcmp (query_string, thread_id_query_string) == 0 ||
+ strcmp (query_string, "") == 0 ||
+ strcmp (query_string, "*") == 0)
+ {
+ matched_is_subset_of_thread = 0;
+ }
+
+ thread_id_query = notmuch_query_create (notmuch, thread_id_query_string);
+ if (unlikely (thread_id_query == NULL))
+ return NULL;
+
+ thread = talloc (ctx, notmuch_thread_t);
+ if (unlikely (thread == NULL))
+ return NULL;
+
+ talloc_set_destructor (thread, _notmuch_thread_destructor);
+
+ thread->notmuch = notmuch;
+ thread->thread_id = talloc_strdup (thread, thread_id);
+ thread->subject = NULL;
+ thread->authors_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+ free, NULL);
+ thread->authors = NULL;
+ thread->tags = g_hash_table_new_full (g_str_hash, g_str_equal,
+ free, NULL);
+
+ thread->message_list = _notmuch_message_list_create (thread);
+ if (unlikely (thread->message_list == NULL))
+ return NULL;
+
+ thread->message_hash = g_hash_table_new_full (g_str_hash, g_str_equal,
+ free, NULL);
+
+ thread->total_messages = 0;
+ thread->matched_messages = 0;
+ thread->oldest = 0;
+ thread->newest = 0;
+
+ notmuch_query_set_sort (thread_id_query, sort);
+
+ for (messages = notmuch_query_search_messages (thread_id_query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ _thread_add_message (thread, message);
+
+ if (! matched_is_subset_of_thread)
+ _thread_add_matched_message (thread, message);
+
+ _notmuch_message_close (message);
+ }
+
+ notmuch_query_destroy (thread_id_query);
+
+ if (matched_is_subset_of_thread)
+ {
+ const char *matched_query_string;
+ notmuch_query_t *matched_query;
+
+ matched_query_string = talloc_asprintf (ctx, "%s AND (%s)",
+ thread_id_query_string,
+ query_string);
+ if (unlikely (matched_query_string == NULL))
+ return NULL;
+
+ matched_query = notmuch_query_create (notmuch, matched_query_string);
+ if (unlikely (matched_query == NULL))
+ return NULL;
+
+ notmuch_query_set_sort (matched_query, sort);
+
+ for (messages = notmuch_query_search_messages (matched_query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+ _thread_add_matched_message (thread, message);
+ _notmuch_message_close (message);
+ }
+
+ notmuch_query_destroy (matched_query);
+ }
+
+ _resolve_thread_relationships (thread);
+
+ return thread;
+}
+
+notmuch_messages_t *
+notmuch_thread_get_toplevel_messages (notmuch_thread_t *thread)
+{
+ return _notmuch_messages_create (thread->message_list);
+}
+
+const char *
+notmuch_thread_get_thread_id (notmuch_thread_t *thread)
+{
+ return thread->thread_id;
+}
+
+int
+notmuch_thread_get_total_messages (notmuch_thread_t *thread)
+{
+ return thread->total_messages;
+}
+
+int
+notmuch_thread_get_matched_messages (notmuch_thread_t *thread)
+{
+ return thread->matched_messages;
+}
+
+const char *
+notmuch_thread_get_authors (notmuch_thread_t *thread)
+{
+ return thread->authors;
+}
+
+const char *
+notmuch_thread_get_subject (notmuch_thread_t *thread)
+{
+ return thread->subject;
+}
+
+time_t
+notmuch_thread_get_oldest_date (notmuch_thread_t *thread)
+{
+ return thread->oldest;
+}
+
+time_t
+notmuch_thread_get_newest_date (notmuch_thread_t *thread)
+{
+ return thread->newest;
+}
+
+notmuch_tags_t *
+notmuch_thread_get_tags (notmuch_thread_t *thread)
+{
+ notmuch_tags_t *tags;
+ GList *keys, *l;
+
+ tags = _notmuch_tags_create (thread);
+ if (unlikely (tags == NULL))
+ return NULL;
+
+ keys = g_hash_table_get_keys (thread->tags);
+
+ for (l = keys; l; l = l->next)
+ _notmuch_tags_add_tag (tags, (char *) l->data);
+
+ g_list_free (keys);
+
+ _notmuch_tags_prepare_iterator (tags);
+
+ return tags;
+}
+
+void
+notmuch_thread_destroy (notmuch_thread_t *thread)
+{
+ talloc_free (thread);
+}
diff --git a/lib/xutil.c b/lib/xutil.c
new file mode 100644
index 0000000..268225b
--- /dev/null
+++ b/lib/xutil.c
@@ -0,0 +1,134 @@
+/* xutil.c - Various wrapper functions to abort on error.
+ *
+ * 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 <stdio.h>
+
+void *
+xcalloc (size_t nmemb, size_t size)
+{
+ void *ret;
+
+ ret = calloc (nmemb, size);
+ if (ret == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ exit (1);
+ }
+
+ return ret;
+}
+
+void *
+xmalloc (size_t size)
+{
+ void *ret;
+
+ ret = malloc (size);
+ if (ret == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ exit (1);
+ }
+
+ return ret;
+}
+
+void *
+xrealloc (void *ptr, size_t size)
+{
+ void *ret;
+
+ ret = realloc (ptr, size);
+ if (ret == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ exit (1);
+ }
+
+ return ret;
+}
+
+char *
+xstrdup (const char *s)
+{
+ char *ret;
+
+ ret = strdup (s);
+ if (ret == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ exit (1);
+ }
+
+ return ret;
+}
+
+char *
+xstrndup (const char *s, size_t n)
+{
+ char *ret;
+
+ if (strlen (s) <= n)
+ n = strlen (s);
+
+ ret = malloc (n + 1);
+ if (ret == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ exit (1);
+ }
+ memcpy (ret, s, n);
+ ret[n] = '\0';
+
+ return ret;
+}
+
+void
+xregcomp (regex_t *preg, const char *regex, int cflags)
+{
+ int rerr;
+
+ rerr = regcomp (preg, regex, cflags);
+ if (rerr) {
+ size_t error_size = regerror (rerr, preg, NULL, 0);
+ char *error = xmalloc (error_size);
+
+ regerror (rerr, preg, error, error_size);
+ INTERNAL_ERROR ("compiling regex %s: %s\n",
+ regex, error);
+ }
+}
+
+int
+xregexec (const regex_t *preg, const char *string,
+ size_t nmatch, regmatch_t pmatch[], int eflags)
+{
+ unsigned int i;
+ int rerr;
+
+ rerr = regexec (preg, string, nmatch, pmatch, eflags);
+ if (rerr)
+ return rerr;
+
+ for (i = 0; i < nmatch; i++) {
+ if (pmatch[i].rm_so == -1)
+ INTERNAL_ERROR ("matching regex against %s: Sub-match %d not found\n",
+ string, i);
+ }
+
+ return 0;
+}
diff --git a/lib/xutil.h b/lib/xutil.h
new file mode 100644
index 0000000..b973f7d
--- /dev/null
+++ b/lib/xutil.h
@@ -0,0 +1,51 @@
+/* xutil.h - Various wrapper functions to abort on error.
+ *
+ * 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_XUTIL_H
+#define NOTMUCH_XUTIL_H
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <regex.h>
+
+/* xutil.c */
+void *
+xcalloc (size_t nmemb, size_t size);
+
+void *
+xmalloc (size_t size);
+
+void *
+xrealloc (void *ptrr, size_t size);
+
+char *
+xstrdup (const char *s);
+
+char *
+xstrndup (const char *s, size_t n);
+
+void
+xregcomp (regex_t *preg, const char *regex, int cflags);
+
+int
+xregexec (const regex_t *preg, const char *string,
+ size_t nmatch, regmatch_t pmatch[], int eflags);
+
+#endif
diff --git a/notmuch-client.h b/notmuch-client.h
new file mode 100644
index 0000000..d36b9ec
--- /dev/null
+++ b/notmuch-client.h
@@ -0,0 +1,189 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#ifndef NOTMUCH_CLIENT_H
+#define NOTMUCH_CLIENT_H
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE /* for getline */
+#endif
+#include <stdio.h>
+
+#include "compat.h"
+
+#include <gmime/gmime.h>
+
+#include "notmuch.h"
+
+/* This is separate from notmuch-private.h because we're trying to
+ * keep notmuch.c from looking into any internals, (which helps us
+ * develop notmuch.h into a plausible library interface).
+ */
+#include "xutil.h"
+
+#include <stddef.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <errno.h>
+#include <signal.h>
+
+#include <talloc.h>
+
+#define unused(x) x __attribute__ ((unused))
+
+#define STRINGIFY(s) STRINGIFY_(s)
+#define STRINGIFY_(s) #s
+
+/* There's no point in continuing when we've detected that we've done
+ * something wrong internally (as opposed to the user passing in a
+ * bogus value).
+ *
+ * Note that __location__ comes from talloc.h.
+ */
+#define INTERNAL_ERROR(format, ...) \
+ do { \
+ fprintf(stderr, \
+ "Internal error: " format " (%s)\n", \
+ ##__VA_ARGS__, __location__); \
+ exit (1); \
+ } while (0)
+
+#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))
+
+#define STRNCMP_LITERAL(var, literal) \
+ strncmp ((var), (literal), sizeof (literal) - 1)
+
+static inline void
+chomp_newline (char *str)
+{
+ if (str && str[strlen(str)-1] == '\n')
+ str[strlen(str)-1] = '\0';
+}
+
+int
+notmuch_count_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_dump_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_new_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_reply_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_restore_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_search_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_setup_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_show_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_tag_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_search_tags_command (void *ctx, int argc, char *argv[]);
+
+int
+notmuch_part_command (void *ctx, int argc, char *argv[]);
+
+const char *
+notmuch_time_relative_date (const void *ctx, time_t then);
+
+void
+notmuch_time_print_formatted_seconds (double seconds);
+
+double
+notmuch_time_elapsed (struct timeval start, struct timeval end);
+
+char *
+query_string_from_args (void *ctx, int argc, char *argv[]);
+
+notmuch_status_t
+show_message_body (const char *filename,
+ void (*show_part) (GMimeObject *part, int *part_count));
+
+notmuch_status_t
+show_one_part (const char *filename, int part);
+
+char *
+json_quote_chararray (const void *ctx, const char *str, const size_t len);
+
+char *
+json_quote_str (const void *ctx, const char *str);
+
+/* notmuch-config.c */
+
+typedef struct _notmuch_config notmuch_config_t;
+
+notmuch_config_t *
+notmuch_config_open (void *ctx,
+ const char *filename,
+ notmuch_bool_t *is_new_ret);
+
+void
+notmuch_config_close (notmuch_config_t *config);
+
+int
+notmuch_config_save (notmuch_config_t *config);
+
+const char *
+notmuch_config_get_database_path (notmuch_config_t *config);
+
+void
+notmuch_config_set_database_path (notmuch_config_t *config,
+ const char *database_path);
+
+const char *
+notmuch_config_get_user_name (notmuch_config_t *config);
+
+void
+notmuch_config_set_user_name (notmuch_config_t *config,
+ const char *user_name);
+
+const char *
+notmuch_config_get_user_primary_email (notmuch_config_t *config);
+
+void
+notmuch_config_set_user_primary_email (notmuch_config_t *config,
+ const char *primary_email);
+
+char **
+notmuch_config_get_user_other_email (notmuch_config_t *config,
+ size_t *length);
+
+void
+notmuch_config_set_user_other_email (notmuch_config_t *config,
+ const char *other_email[],
+ size_t length);
+
+notmuch_bool_t
+debugger_is_active (void);
+
+#endif
diff --git a/notmuch-config.c b/notmuch-config.c
new file mode 100644
index 0000000..cc05f6c
--- /dev/null
+++ b/notmuch-config.c
@@ -0,0 +1,457 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+#include <pwd.h>
+#include <netdb.h>
+
+static const char toplevel_config_comment[] =
+ " .notmuch-config - Configuration file for the notmuch mail system\n"
+ "\n"
+ " For more information about notmuch, see http://notmuchmail.org";
+
+static const char database_config_comment[] =
+ " Database configuration\n"
+ "\n"
+ " The only value supported here is 'path' which should be the top-level\n"
+ " directory where your mail currently exists and to where mail will be\n"
+ " delivered in the future. Files should be individual email messages.\n"
+ " Notmuch will store its database within a sub-directory of the path\n"
+ " configured here named \".notmuch\".\n";
+
+static const char user_config_comment[] =
+ " User configuration\n"
+ "\n"
+ " Here is where you can let notmuch know how you would like to be\n"
+ " addressed. Valid settings are\n"
+ "\n"
+ "\tname Your full name.\n"
+ "\tprimary_email Your primary email address.\n"
+ "\tother_email A list (separated by ';') of other email addresses\n"
+ "\t at which you receive email.\n"
+ "\n"
+ " Notmuch will use the various email addresses configured here when\n"
+ " formatting replies. It will avoid including your own addresses in the\n"
+ " recipient list of replies, and will set the From address based on the\n"
+ " address to which the original email was addressed.\n";
+
+struct _notmuch_config {
+ char *filename;
+ GKeyFile *key_file;
+
+ char *database_path;
+ char *user_name;
+ char *user_primary_email;
+ char **user_other_email;
+ size_t user_other_email_length;
+};
+
+static int
+notmuch_config_destructor (notmuch_config_t *config)
+{
+ if (config->key_file)
+ g_key_file_free (config->key_file);
+
+ return 0;
+}
+
+static char *
+get_name_from_passwd_file (void *ctx)
+{
+ long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+ char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+ struct passwd passwd, *ignored;
+ char *name;
+ int e;
+
+ if (pw_buf_size == -1) pw_buf_size = 64;
+
+ while ((e = getpwuid_r (getuid (), &passwd, pw_buf,
+ pw_buf_size, &ignored)) == ERANGE) {
+ pw_buf_size = pw_buf_size * 2;
+ pw_buf = talloc_zero_size(ctx, pw_buf_size);
+ }
+
+ if (e == 0) {
+ char *comma = strchr (passwd.pw_gecos, ',');
+ if (comma)
+ name = talloc_strndup (ctx, passwd.pw_gecos,
+ comma - passwd.pw_gecos);
+ else
+ name = talloc_strdup (ctx, passwd.pw_gecos);
+ } else {
+ name = talloc_strdup (ctx, "");
+ }
+
+ talloc_free (pw_buf);
+
+ return name;
+}
+
+static char *
+get_username_from_passwd_file (void *ctx)
+{
+ long pw_buf_size = sysconf(_SC_GETPW_R_SIZE_MAX);
+ char *pw_buf = talloc_zero_size (ctx, pw_buf_size);
+ struct passwd passwd, *ignored;
+ char *name;
+ int e;
+
+ if (pw_buf_size == -1) pw_buf_size = 64;
+ while ((e = getpwuid_r (getuid (), &passwd, pw_buf,
+ pw_buf_size, &ignored)) == ERANGE) {
+ pw_buf_size = pw_buf_size * 2;
+ pw_buf = talloc_zero_size(ctx, pw_buf_size);
+ }
+
+ if (e == 0)
+ name = talloc_strdup (ctx, passwd.pw_name);
+ else
+ name = talloc_strdup (ctx, "");
+
+ talloc_free (pw_buf);
+
+ return name;
+}
+
+/* Open the named notmuch configuration file. If the filename is NULL,
+ * the value of the environment variable $NOTMUCH_CONFIG will be used.
+ * If $NOTMUCH_CONFIG is unset, the default configuration file
+ * ($HOME/.notmuch-config) will be used.
+ *
+ * If any error occurs, (out of memory, or a permission-denied error,
+ * etc.), this function will print a message to stderr and return
+ * NULL.
+ *
+ * Note: It is *not* an error if the specified configuration file does
+ * not exist. In this case, a default configuration will be created
+ * and returned. Subsequently calling notmuch_config_save will cause
+ * the configuration to be written to the filename specified at the
+ * time of notmuch_config_open.
+ *
+ * The default configuration settings are determined as follows:
+ *
+ * database_path: $HOME/mail
+ *
+ * user_name: From /etc/passwd
+ *
+ * user_primary_mail: $EMAIL variable if set, otherwise
+ * constructed from the username and
+ * hostname of the current machine.
+ *
+ * user_other_email: Not set.
+ *
+ * The default configuration also contains comments to guide the user
+ * in editing the file directly.
+ */
+notmuch_config_t *
+notmuch_config_open (void *ctx,
+ const char *filename,
+ notmuch_bool_t *is_new_ret)
+{
+ GError *error = NULL;
+ int is_new = 0;
+ char *notmuch_config_env = NULL;
+
+ if (is_new_ret)
+ *is_new_ret = 0;
+
+ notmuch_config_t *config = talloc (ctx, notmuch_config_t);
+ if (config == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return NULL;
+ }
+
+ talloc_set_destructor (config, notmuch_config_destructor);
+
+ if (filename) {
+ config->filename = talloc_strdup (config, filename);
+ } else if ((notmuch_config_env = getenv ("NOTMUCH_CONFIG"))) {
+ config->filename = talloc_strdup (config, notmuch_config_env);
+ } else {
+ config->filename = talloc_asprintf (config, "%s/.notmuch-config",
+ getenv ("HOME"));
+ }
+
+ config->key_file = g_key_file_new ();
+
+ config->database_path = NULL;
+ config->user_name = NULL;
+ config->user_primary_email = NULL;
+ config->user_other_email = NULL;
+ config->user_other_email_length = 0;
+
+ if (! g_key_file_load_from_file (config->key_file,
+ config->filename,
+ G_KEY_FILE_KEEP_COMMENTS,
+ &error))
+ {
+ /* We are capable of dealing with a non-existent configuration
+ * file, so be silent about that (unless the user had set a
+ * non-default configuration file with the NOTMUCH_CONFIG
+ * variable)
+ */
+ if (notmuch_config_env ||
+ !(error->domain == G_FILE_ERROR &&
+ error->code == G_FILE_ERROR_NOENT))
+ {
+ fprintf (stderr, "Error reading configuration file %s: %s\n",
+ config->filename, error->message);
+ talloc_free (config);
+ g_error_free (error);
+ return NULL;
+ }
+
+ g_error_free (error);
+ is_new = 1;
+ }
+
+ if (notmuch_config_get_database_path (config) == NULL) {
+ char *path = talloc_asprintf (config, "%s/mail",
+ getenv ("HOME"));
+ notmuch_config_set_database_path (config, path);
+ talloc_free (path);
+ }
+
+ if (notmuch_config_get_user_name (config) == NULL) {
+ char *name = get_name_from_passwd_file (config);
+ notmuch_config_set_user_name (config, name);
+ talloc_free (name);
+ }
+
+ if (notmuch_config_get_user_primary_email (config) == NULL) {
+ char *email = getenv ("EMAIL");
+ if (email) {
+ notmuch_config_set_user_primary_email (config, email);
+ } else {
+ char hostname[256];
+ struct hostent *hostent;
+ const char *domainname;
+
+ char *username = get_username_from_passwd_file (config);
+
+ gethostname (hostname, 256);
+ hostname[255] = '\0';
+
+ hostent = gethostbyname (hostname);
+ if (hostent && (domainname = strchr (hostent->h_name, '.')))
+ domainname += 1;
+ else
+ domainname = "(none)";
+
+ email = talloc_asprintf (config, "%s@%s.%s",
+ username, hostname, domainname);
+
+ notmuch_config_set_user_primary_email (config, email);
+
+ talloc_free (username);
+ talloc_free (email);
+ }
+ }
+
+ /* When we create a new configuration file here, we add some
+ * comments to help the user understand what can be done. */
+ if (is_new) {
+ g_key_file_set_comment (config->key_file, NULL, NULL,
+ toplevel_config_comment, NULL);
+ g_key_file_set_comment (config->key_file, "database", NULL,
+ database_config_comment, NULL);
+ g_key_file_set_comment (config->key_file, "user", NULL,
+ user_config_comment, NULL);
+ }
+
+ if (is_new_ret)
+ *is_new_ret = is_new;
+
+ return config;
+}
+
+/* Close the given notmuch_config_t object, freeing all resources.
+ *
+ * Note: Any changes made to the configuration are *not* saved by this
+ * function. To save changes, call notmuch_config_save before
+ * notmuch_config_close.
+*/
+void
+notmuch_config_close (notmuch_config_t *config)
+{
+ talloc_free (config);
+}
+
+/* Save any changes made to the notmuch configuration.
+ *
+ * Any comments originally in the file will be preserved.
+ *
+ * Returns 0 if successful, and 1 in case of any error, (after
+ * printing a description of the error to stderr).
+ */
+int
+notmuch_config_save (notmuch_config_t *config)
+{
+ size_t length;
+ char *data;
+ GError *error = NULL;
+
+ data = g_key_file_to_data (config->key_file, &length, NULL);
+ if (data == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ if (! g_file_set_contents (config->filename, data, length, &error)) {
+ fprintf (stderr, "Error saving configuration to %s: %s\n",
+ config->filename, error->message);
+ g_error_free (error);
+ g_free (data);
+ return 1;
+ }
+
+ g_free (data);
+ return 0;
+}
+
+const char *
+notmuch_config_get_database_path (notmuch_config_t *config)
+{
+ char *path;
+
+ if (config->database_path == NULL) {
+ path = g_key_file_get_string (config->key_file,
+ "database", "path", NULL);
+ if (path) {
+ config->database_path = talloc_strdup (config, path);
+ free (path);
+ }
+ }
+
+ return config->database_path;
+}
+
+void
+notmuch_config_set_database_path (notmuch_config_t *config,
+ const char *database_path)
+{
+ g_key_file_set_string (config->key_file,
+ "database", "path", database_path);
+
+ talloc_free (config->database_path);
+ config->database_path = NULL;
+}
+
+const char *
+notmuch_config_get_user_name (notmuch_config_t *config)
+{
+ char *name;
+
+ if (config->user_name == NULL) {
+ name = g_key_file_get_string (config->key_file,
+ "user", "name", NULL);
+ if (name) {
+ config->user_name = talloc_strdup (config, name);
+ free (name);
+ }
+ }
+
+ return config->user_name;
+}
+
+void
+notmuch_config_set_user_name (notmuch_config_t *config,
+ const char *user_name)
+{
+ g_key_file_set_string (config->key_file,
+ "user", "name", user_name);
+
+ talloc_free (config->user_name);
+ config->user_name = NULL;
+}
+
+const char *
+notmuch_config_get_user_primary_email (notmuch_config_t *config)
+{
+ char *email;
+
+ if (config->user_primary_email == NULL) {
+ email = g_key_file_get_string (config->key_file,
+ "user", "primary_email", NULL);
+ if (email) {
+ config->user_primary_email = talloc_strdup (config, email);
+ free (email);
+ }
+ }
+
+ return config->user_primary_email;
+}
+
+void
+notmuch_config_set_user_primary_email (notmuch_config_t *config,
+ const char *primary_email)
+{
+ g_key_file_set_string (config->key_file,
+ "user", "primary_email", primary_email);
+
+ talloc_free (config->user_primary_email);
+ config->user_primary_email = NULL;
+}
+
+char **
+notmuch_config_get_user_other_email (notmuch_config_t *config,
+ size_t *length)
+{
+ char **emails;
+ size_t emails_length;
+ unsigned int i;
+
+ if (config->user_other_email == NULL) {
+ emails = g_key_file_get_string_list (config->key_file,
+ "user", "other_email",
+ &emails_length, NULL);
+ if (emails) {
+ config->user_other_email = talloc_size (config,
+ sizeof (char *) *
+ (emails_length + 1));
+ for (i = 0; i < emails_length; i++)
+ config->user_other_email[i] = talloc_strdup (config->user_other_email,
+ emails[i]);
+ config->user_other_email[i] = NULL;
+
+ g_strfreev (emails);
+
+ config->user_other_email_length = emails_length;
+ }
+ }
+
+ *length = config->user_other_email_length;
+ return config->user_other_email;
+}
+
+void
+notmuch_config_set_user_other_email (notmuch_config_t *config,
+ const char *other_email[],
+ size_t length)
+{
+ g_key_file_set_string_list (config->key_file,
+ "user", "other_email",
+ other_email, length);
+
+ talloc_free (config->user_other_email);
+ config->user_other_email = NULL;
+}
diff --git a/notmuch-count.c b/notmuch-count.c
new file mode 100644
index 0000000..39f08c6
--- /dev/null
+++ b/notmuch-count.c
@@ -0,0 +1,110 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+
+int
+notmuch_count_command (void *ctx, int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ char *query_str;
+ int i;
+#if 0
+ char *opt, *end;
+ int i, first = 0, max_threads = -1;
+ notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
+#endif
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+#if 0
+ if (STRNCMP_LITERAL (argv[i], "--first=") == 0) {
+ opt = argv[i] + sizeof ("--first=") - 1;
+ first = strtoul (opt, &end, 10);
+ if (*opt == '\0' || *end != '\0') {
+ fprintf (stderr, "Invalid value for --first: %s\n", opt);
+ return 1;
+ }
+ } else if (STRNCMP_LITERAL (argv[i], "--max-threads=") == 0) {
+ opt = argv[i] + sizeof ("--max-threads=") - 1;
+ max_threads = strtoul (opt, &end, 10);
+ if (*opt == '\0' || *end != '\0') {
+ fprintf (stderr, "Invalid value for --max-threads: %s\n", opt);
+ return 1;
+ }
+ } else if (STRNCMP_LITERAL (argv[i], "--sort=") == 0) {
+ opt = argv[i] + sizeof ("--sort=") - 1;
+ if (strcmp (opt, "oldest-first") == 0) {
+ sort = NOTMUCH_SORT_OLDEST_FIRST;
+ } else if (strcmp (opt, "newest-first") == 0) {
+ sort = NOTMUCH_SORT_NEWEST_FIRST;
+ } else {
+ fprintf (stderr, "Invalid value for --sort: %s\n", opt);
+ return 1;
+ }
+ } else
+#endif
+ {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ argc -= i;
+ argv += i;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query_str = query_string_from_args (ctx, argc, argv);
+ if (query_str == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ if (*query_str == '\0') {
+ query_str = talloc_strdup (ctx, "");
+ }
+
+ query = notmuch_query_create (notmuch, query_str);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ printf ("%u\n", notmuch_query_count_messages(query));
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return 0;
+}
diff --git a/notmuch-dump.c b/notmuch-dump.c
new file mode 100644
index 0000000..7e7bc17
--- /dev/null
+++ b/notmuch-dump.c
@@ -0,0 +1,95 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+int
+notmuch_dump_command (unused (void *ctx), int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ FILE *output;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ notmuch_tags_t *tags;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query = notmuch_query_create (notmuch, "");
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+ notmuch_query_set_sort (query, NOTMUCH_SORT_MESSAGE_ID);
+
+ if (argc) {
+ output = fopen (argv[0], "w");
+ if (output == NULL) {
+ fprintf (stderr, "Error opening %s for writing: %s\n",
+ argv[0], strerror (errno));
+ return 1;
+ }
+ } else {
+ output = stdout;
+ }
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ int first = 1;
+ message = notmuch_messages_get (messages);
+
+ fprintf (output,
+ "%s (", notmuch_message_get_message_id (message));
+
+ for (tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ if (! first)
+ fprintf (output, " ");
+
+ fprintf (output, "%s", notmuch_tags_get (tags));
+
+ first = 0;
+ }
+
+ fprintf (output, ")\n");
+
+ notmuch_message_destroy (message);
+ }
+
+ if (output != stdout)
+ fclose (output);
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return 0;
+}
diff --git a/notmuch-new.c b/notmuch-new.c
new file mode 100644
index 0000000..484d946
--- /dev/null
+++ b/notmuch-new.c
@@ -0,0 +1,865 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+#include <unistd.h>
+
+typedef struct _filename_node {
+ char *filename;
+ struct _filename_node *next;
+} _filename_node_t;
+
+typedef struct _filename_list {
+ _filename_node_t *head;
+ _filename_node_t **tail;
+} _filename_list_t;
+
+typedef struct {
+ int output_is_a_tty;
+ int verbose;
+
+ int total_files;
+ int processed_files;
+ int added_messages;
+ struct timeval tv_start;
+
+ _filename_list_t *removed_files;
+ _filename_list_t *removed_directories;
+} add_files_state_t;
+
+static volatile sig_atomic_t do_add_files_print_progress = 0;
+
+static void
+handle_sigalrm (unused (int signal))
+{
+ do_add_files_print_progress = 1;
+}
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+ ssize_t ignored;
+ static char msg[] = "Stopping... \n";
+
+ ignored = write(2, msg, sizeof(msg)-1);
+ interrupted = 1;
+}
+
+static _filename_list_t *
+_filename_list_create (const void *ctx)
+{
+ _filename_list_t *list;
+
+ list = talloc (ctx, _filename_list_t);
+ if (list == NULL)
+ return NULL;
+
+ list->head = NULL;
+ list->tail = &list->head;
+
+ return list;
+}
+
+static void
+_filename_list_add (_filename_list_t *list,
+ const char *filename)
+{
+ _filename_node_t *node = talloc (list, _filename_node_t);
+
+ node->filename = talloc_strdup (list, filename);
+ node->next = NULL;
+
+ *(list->tail) = node;
+ list->tail = &node->next;
+}
+
+static void
+tag_inbox_and_unread (notmuch_message_t *message)
+{
+ notmuch_message_add_tag (message, "inbox");
+ notmuch_message_add_tag (message, "unread");
+}
+
+static void
+add_files_print_progress (add_files_state_t *state)
+{
+ struct timeval tv_now;
+ double elapsed_overall, rate_overall;
+
+ gettimeofday (&tv_now, NULL);
+
+ elapsed_overall = notmuch_time_elapsed (state->tv_start, tv_now);
+ rate_overall = (state->processed_files) / elapsed_overall;
+
+ printf ("Processed %d", state->processed_files);
+
+ if (state->total_files) {
+ double time_remaining;
+
+ time_remaining = ((state->total_files - state->processed_files) /
+ rate_overall);
+ printf (" of %d files (", state->total_files);
+ notmuch_time_print_formatted_seconds (time_remaining);
+ printf (" remaining). \r");
+ } else {
+ printf (" files (%d files/sec.) \r", (int) rate_overall);
+ }
+
+ fflush (stdout);
+}
+
+static int
+dirent_sort_inode (const struct dirent **a, const struct dirent **b)
+{
+ return ((*a)->d_ino < (*b)->d_ino) ? -1 : 1;
+}
+
+static int
+dirent_sort_strcmp_name (const struct dirent **a, const struct dirent **b)
+{
+ return strcmp ((*a)->d_name, (*b)->d_name);
+}
+
+/* Test if the directory looks like a Maildir directory.
+ *
+ * Search through the array of directory entries to see if we can find all
+ * three subdirectories typical for Maildir, that is "new", "cur", and "tmp".
+ *
+ * Return 1 if the directory looks like a Maildir and 0 otherwise.
+ */
+static int
+_entries_resemble_maildir (struct dirent **entries, int count)
+{
+ int i, found = 0;
+
+ for (i = 0; i < count; i++) {
+ if (entries[i]->d_type != DT_DIR && entries[i]->d_type != DT_UNKNOWN)
+ continue;
+
+ if (strcmp(entries[i]->d_name, "new") == 0 ||
+ strcmp(entries[i]->d_name, "cur") == 0 ||
+ strcmp(entries[i]->d_name, "tmp") == 0)
+ {
+ found++;
+ if (found == 3)
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+/* Examine 'path' recursively as follows:
+ *
+ * o Ask the filesystem for the mtime of 'path' (fs_mtime)
+ * o Ask the database for its timestamp of 'path' (db_mtime)
+ *
+ * o Ask the filesystem for files and directories within 'path'
+ * (via scandir and stored in fs_entries)
+ * o Ask the database for files and directories within 'path'
+ * (db_files and db_subdirs)
+ *
+ * o Pass 1: For each directory in fs_entries, recursively call into
+ * this same function.
+ *
+ * o Pass 2: If 'fs_mtime' > 'db_mtime', then walk fs_entries
+ * simultaneously with db_files and db_subdirs. Look for one of
+ * three interesting cases:
+ *
+ * 1. Regular file in fs_entries and not in db_files
+ * This is a new file to add_message into the database.
+ *
+ * 2. Filename in db_files not in fs_entries.
+ * This is a file that has been removed from the mail store.
+ *
+ * 3. Directory in db_subdirs not in fs_entries
+ * This is a directory that has been removed from the mail store.
+ *
+ * Note that the addition of a directory is not interesting here,
+ * since that will have been taken care of in pass 1. Also, we
+ * don't immediately act on file/directory removal since we must
+ * ensure that in the case of a rename that the new filename is
+ * added before the old filename is removed, (so that no
+ * information is lost from the database).
+ *
+ * o Tell the database to update its time of 'path' to 'fs_mtime'
+ */
+static notmuch_status_t
+add_files_recursive (notmuch_database_t *notmuch,
+ const char *path,
+ add_files_state_t *state)
+{
+ DIR *dir = NULL;
+ struct dirent *entry = NULL;
+ char *next = NULL;
+ time_t fs_mtime, db_mtime;
+ notmuch_status_t status, ret = NOTMUCH_STATUS_SUCCESS;
+ notmuch_message_t *message = NULL;
+ struct dirent **fs_entries = NULL;
+ int i, num_fs_entries;
+ notmuch_directory_t *directory;
+ notmuch_filenames_t *db_files = NULL;
+ notmuch_filenames_t *db_subdirs = NULL;
+ struct stat st;
+ notmuch_bool_t is_maildir, new_directory;
+
+ if (stat (path, &st)) {
+ fprintf (stderr, "Error reading directory %s: %s\n",
+ path, strerror (errno));
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ /* This is not an error since we may have recursed based on a
+ * symlink to a regular file, not a directory, and we don't know
+ * that until this stat. */
+ if (! S_ISDIR (st.st_mode))
+ return NOTMUCH_STATUS_SUCCESS;
+
+ fs_mtime = st.st_mtime;
+
+ directory = notmuch_database_get_directory (notmuch, path);
+ db_mtime = notmuch_directory_get_mtime (directory);
+
+ if (db_mtime == 0) {
+ new_directory = TRUE;
+ db_files = NULL;
+ db_subdirs = NULL;
+ } else {
+ new_directory = FALSE;
+ db_files = notmuch_directory_get_child_files (directory);
+ db_subdirs = notmuch_directory_get_child_directories (directory);
+ }
+
+ /* If the database knows about this directory, then we sort based
+ * on strcmp to match the database sorting. Otherwise, we can do
+ * inode-based sorting for faster filesystem operation. */
+ num_fs_entries = scandir (path, &fs_entries, 0,
+ new_directory ?
+ dirent_sort_inode : dirent_sort_strcmp_name);
+
+ if (num_fs_entries == -1) {
+ fprintf (stderr, "Error opening directory %s: %s\n",
+ path, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ /* Pass 1: Recurse into all sub-directories. */
+ is_maildir = _entries_resemble_maildir (fs_entries, num_fs_entries);
+
+ for (i = 0; i < num_fs_entries; i++) {
+ if (interrupted)
+ break;
+
+ entry = fs_entries[i];
+
+ /* We only want to descend into directories.
+ * But symlinks can be to directories too, of course.
+ *
+ * And if the filesystem doesn't tell us the file type in the
+ * scandir results, then it might be a directory (and if not,
+ * then we'll stat and return immediately in the next level of
+ * recursion). */
+ if (entry->d_type != DT_DIR &&
+ entry->d_type != DT_LNK &&
+ entry->d_type != DT_UNKNOWN)
+ {
+ continue;
+ }
+
+ /* Ignore special directories to avoid infinite recursion.
+ * Also ignore the .notmuch directory and any "tmp" directory
+ * that appears within a maildir.
+ */
+ /* XXX: Eventually we'll want more sophistication to let the
+ * user specify files to be ignored. */
+ if (strcmp (entry->d_name, ".") == 0 ||
+ strcmp (entry->d_name, "..") == 0 ||
+ (is_maildir && strcmp (entry->d_name, "tmp") == 0) ||
+ strcmp (entry->d_name, ".notmuch") ==0)
+ {
+ continue;
+ }
+
+ next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
+ status = add_files_recursive (notmuch, next, state);
+ if (status && ret == NOTMUCH_STATUS_SUCCESS)
+ ret = status;
+ talloc_free (next);
+ next = NULL;
+ }
+
+ /* If this directory hasn't been modified since the last
+ * "notmuch new", then we can skip the second pass entirely. */
+ if (fs_mtime <= db_mtime)
+ goto DONE;
+
+ /* Pass 2: Scan for new files, removed files, and removed directories. */
+ for (i = 0; i < num_fs_entries; i++)
+ {
+ if (interrupted)
+ break;
+
+ entry = fs_entries[i];
+
+ /* Check if we've walked past any names in db_files or
+ * db_subdirs. If so, these have been deleted. */
+ while (notmuch_filenames_valid (db_files) &&
+ strcmp (notmuch_filenames_get (db_files), entry->d_name) < 0)
+ {
+ char *absolute = talloc_asprintf (state->removed_files,
+ "%s/%s", path,
+ notmuch_filenames_get (db_files));
+
+ _filename_list_add (state->removed_files, absolute);
+
+ notmuch_filenames_move_to_next (db_files);
+ }
+
+ while (notmuch_filenames_valid (db_subdirs) &&
+ strcmp (notmuch_filenames_get (db_subdirs), entry->d_name) <= 0)
+ {
+ const char *filename = notmuch_filenames_get (db_subdirs);
+
+ if (strcmp (filename, entry->d_name) < 0)
+ {
+ char *absolute = talloc_asprintf (state->removed_directories,
+ "%s/%s", path, filename);
+
+ _filename_list_add (state->removed_directories, absolute);
+ }
+
+ notmuch_filenames_move_to_next (db_subdirs);
+ }
+
+ /* If we're looking at a symlink, we only want to add it if it
+ * links to a regular file, (and not to a directory, say).
+ *
+ * Similarly, if the file is of unknown type (due to filesytem
+ * limitations), then we also need to look closer.
+ *
+ * In either case, a stat does the trick.
+ */
+ if (entry->d_type == DT_LNK || entry->d_type == DT_UNKNOWN) {
+ int err;
+
+ next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
+ err = stat (next, &st);
+ talloc_free (next);
+ next = NULL;
+
+ /* Don't emit an error for a link pointing nowhere, since
+ * the directory-traversal pass will have already done
+ * that. */
+ if (err)
+ continue;
+
+ if (! S_ISREG (st.st_mode))
+ continue;
+ } else if (entry->d_type != DT_REG) {
+ continue;
+ }
+
+ /* Don't add a file that we've added before. */
+ if (notmuch_filenames_valid (db_files) &&
+ strcmp (notmuch_filenames_get (db_files), entry->d_name) == 0)
+ {
+ notmuch_filenames_move_to_next (db_files);
+ continue;
+ }
+
+ /* We're now looking at a regular file that doesn't yet exist
+ * in the database, so add it. */
+ next = talloc_asprintf (notmuch, "%s/%s", path, entry->d_name);
+
+ state->processed_files++;
+
+ if (state->verbose) {
+ if (state->output_is_a_tty)
+ printf("\r\033[K");
+
+ printf ("%i/%i: %s",
+ state->processed_files,
+ state->total_files,
+ next);
+
+ putchar((state->output_is_a_tty) ? '\r' : '\n');
+ fflush (stdout);
+ }
+
+ status = notmuch_database_add_message (notmuch, next, &message);
+ switch (status) {
+ /* success */
+ case NOTMUCH_STATUS_SUCCESS:
+ state->added_messages++;
+ tag_inbox_and_unread (message);
+ break;
+ /* Non-fatal issues (go on to next file) */
+ case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:
+ /* Stay silent on this one. */
+ break;
+ case NOTMUCH_STATUS_FILE_NOT_EMAIL:
+ fprintf (stderr, "Note: Ignoring non-mail file: %s\n",
+ next);
+ break;
+ /* Fatal issues. Don't process anymore. */
+ case NOTMUCH_STATUS_READ_ONLY_DATABASE:
+ case NOTMUCH_STATUS_XAPIAN_EXCEPTION:
+ case NOTMUCH_STATUS_OUT_OF_MEMORY:
+ fprintf (stderr, "Error: %s. Halting processing.\n",
+ notmuch_status_to_string (status));
+ ret = status;
+ goto DONE;
+ default:
+ case NOTMUCH_STATUS_FILE_ERROR:
+ case NOTMUCH_STATUS_NULL_POINTER:
+ case NOTMUCH_STATUS_TAG_TOO_LONG:
+ case NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW:
+ case NOTMUCH_STATUS_LAST_STATUS:
+ INTERNAL_ERROR ("add_message returned unexpected value: %d", status);
+ goto DONE;
+ }
+
+ if (message) {
+ notmuch_message_destroy (message);
+ message = NULL;
+ }
+
+ if (do_add_files_print_progress) {
+ do_add_files_print_progress = 0;
+ add_files_print_progress (state);
+ }
+
+ talloc_free (next);
+ next = NULL;
+ }
+
+ if (interrupted)
+ goto DONE;
+
+ /* Now that we've walked the whole filesystem list, anything left
+ * over in the database lists has been deleted. */
+ while (notmuch_filenames_valid (db_files))
+ {
+ char *absolute = talloc_asprintf (state->removed_files,
+ "%s/%s", path,
+ notmuch_filenames_get (db_files));
+
+ _filename_list_add (state->removed_files, absolute);
+
+ notmuch_filenames_move_to_next (db_files);
+ }
+
+ while (notmuch_filenames_valid (db_subdirs))
+ {
+ char *absolute = talloc_asprintf (state->removed_directories,
+ "%s/%s", path,
+ notmuch_filenames_get (db_subdirs));
+
+ _filename_list_add (state->removed_directories, absolute);
+
+ notmuch_filenames_move_to_next (db_subdirs);
+ }
+
+ if (! interrupted) {
+ status = notmuch_directory_set_mtime (directory, fs_mtime);
+ if (status && ret == NOTMUCH_STATUS_SUCCESS)
+ ret = status;
+ }
+
+ DONE:
+ if (next)
+ talloc_free (next);
+ if (entry)
+ free (entry);
+ if (dir)
+ closedir (dir);
+ if (fs_entries)
+ free (fs_entries);
+ if (db_subdirs)
+ notmuch_filenames_destroy (db_subdirs);
+ if (db_files)
+ notmuch_filenames_destroy (db_files);
+ if (directory)
+ notmuch_directory_destroy (directory);
+
+ return ret;
+}
+
+/* This is the top-level entry point for add_files. It does a couple
+ * of error checks, sets up the progress-printing timer and then calls
+ * into the recursive function. */
+static notmuch_status_t
+add_files (notmuch_database_t *notmuch,
+ const char *path,
+ add_files_state_t *state)
+{
+ notmuch_status_t status;
+ struct sigaction action;
+ struct itimerval timerval;
+ notmuch_bool_t timer_is_active = FALSE;
+ struct stat st;
+
+ if (state->output_is_a_tty && ! debugger_is_active () && ! state->verbose) {
+ /* 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;
+ }
+
+ if (stat (path, &st)) {
+ fprintf (stderr, "Error reading directory %s: %s\n",
+ path, strerror (errno));
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ if (! S_ISDIR (st.st_mode)) {
+ fprintf (stderr, "Error: %s is not a directory.\n", path);
+ return NOTMUCH_STATUS_FILE_ERROR;
+ }
+
+ status = add_files_recursive (notmuch, path, state);
+
+ 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 status;
+}
+
+/* XXX: This should be merged with the add_files function since it
+ * shares a lot of logic with it. */
+/* Recursively count all regular files in path and all sub-directories
+ * of path. The result is added to *count (which should be
+ * initialized to zero by the top-level caller before calling
+ * count_files). */
+static void
+count_files (const char *path, int *count)
+{
+ struct dirent *entry = NULL;
+ char *next;
+ struct stat st;
+ struct dirent **fs_entries = NULL;
+ int num_fs_entries = scandir (path, &fs_entries, 0, dirent_sort_inode);
+ int i = 0;
+
+ if (num_fs_entries == -1) {
+ fprintf (stderr, "Warning: failed to open directory %s: %s\n",
+ path, strerror (errno));
+ goto DONE;
+ }
+
+ while (!interrupted) {
+ if (i == num_fs_entries)
+ break;
+
+ entry = fs_entries[i++];
+
+ /* Ignore special directories to avoid infinite recursion.
+ * Also ignore the .notmuch directory.
+ */
+ /* XXX: Eventually we'll want more sophistication to let the
+ * user specify files to be ignored. */
+ if (strcmp (entry->d_name, ".") == 0 ||
+ strcmp (entry->d_name, "..") == 0 ||
+ strcmp (entry->d_name, ".notmuch") == 0)
+ {
+ continue;
+ }
+
+ if (asprintf (&next, "%s/%s", path, entry->d_name) == -1) {
+ next = NULL;
+ fprintf (stderr, "Error descending from %s to %s: Out of memory\n",
+ path, entry->d_name);
+ continue;
+ }
+
+ stat (next, &st);
+
+ if (S_ISREG (st.st_mode)) {
+ *count = *count + 1;
+ if (*count % 1000 == 0) {
+ printf ("Found %d files so far.\r", *count);
+ fflush (stdout);
+ }
+ } else if (S_ISDIR (st.st_mode)) {
+ count_files (next, count);
+ }
+
+ free (next);
+ }
+
+ DONE:
+ if (entry)
+ free (entry);
+ if (fs_entries)
+ free (fs_entries);
+}
+
+static void
+upgrade_print_progress (void *closure,
+ double progress)
+{
+ add_files_state_t *state = closure;
+
+ printf ("Upgrading database: %.2f%% complete", progress * 100.0);
+
+ if (progress > 0) {
+ struct timeval tv_now;
+ double elapsed, time_remaining;
+
+ gettimeofday (&tv_now, NULL);
+
+ elapsed = notmuch_time_elapsed (state->tv_start, tv_now);
+ time_remaining = (elapsed / progress) * (1.0 - progress);
+ printf (" (");
+ notmuch_time_print_formatted_seconds (time_remaining);
+ printf (" remaining)");
+ }
+
+ printf (". \r");
+
+ fflush (stdout);
+}
+
+/* Recursively remove all filenames from the database referring to
+ * 'path' (or to any of its children). */
+static void
+_remove_directory (void *ctx,
+ notmuch_database_t *notmuch,
+ const char *path,
+ int *renamed_files,
+ int *removed_files)
+{
+ notmuch_directory_t *directory;
+ notmuch_filenames_t *files, *subdirs;
+ notmuch_status_t status;
+ char *absolute;
+
+ directory = notmuch_database_get_directory (notmuch, path);
+
+ for (files = notmuch_directory_get_child_files (directory);
+ notmuch_filenames_valid (files);
+ notmuch_filenames_move_to_next (files))
+ {
+ absolute = talloc_asprintf (ctx, "%s/%s", path,
+ notmuch_filenames_get (files));
+ status = notmuch_database_remove_message (notmuch, absolute);
+ if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+ *renamed_files = *renamed_files + 1;
+ else
+ *removed_files = *removed_files + 1;
+ talloc_free (absolute);
+ }
+
+ for (subdirs = notmuch_directory_get_child_directories (directory);
+ notmuch_filenames_valid (subdirs);
+ notmuch_filenames_move_to_next (subdirs))
+ {
+ absolute = talloc_asprintf (ctx, "%s/%s", path,
+ notmuch_filenames_get (subdirs));
+ _remove_directory (ctx, notmuch, absolute, renamed_files, removed_files);
+ talloc_free (absolute);
+ }
+
+ notmuch_directory_destroy (directory);
+}
+
+int
+notmuch_new_command (void *ctx, int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ add_files_state_t add_files_state;
+ double elapsed;
+ struct timeval tv_now;
+ int ret = 0;
+ struct stat st;
+ const char *db_path;
+ char *dot_notmuch_path;
+ struct sigaction action;
+ _filename_node_t *f;
+ int renamed_files, removed_files;
+ notmuch_status_t status;
+ int i;
+
+ add_files_state.verbose = 0;
+ add_files_state.output_is_a_tty = isatty (fileno (stdout));
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (STRNCMP_LITERAL (argv[i], "--verbose") == 0) {
+ add_files_state.verbose = 1;
+ } else {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ db_path = notmuch_config_get_database_path (config);
+
+ dot_notmuch_path = talloc_asprintf (ctx, "%s/%s", db_path, ".notmuch");
+
+ if (stat (dot_notmuch_path, &st)) {
+ int count;
+
+ count = 0;
+ count_files (db_path, &count);
+ if (interrupted)
+ return 1;
+
+ printf ("Found %d total files (that's not much mail).\n", count);
+ notmuch = notmuch_database_create (db_path);
+ add_files_state.total_files = count;
+ } else {
+ notmuch = notmuch_database_open (db_path,
+ NOTMUCH_DATABASE_MODE_READ_WRITE);
+ if (notmuch == NULL)
+ return 1;
+
+ if (notmuch_database_needs_upgrade (notmuch)) {
+ printf ("Welcome to a new version of notmuch! Your database will now be upgraded.\n");
+ gettimeofday (&add_files_state.tv_start, NULL);
+ notmuch_database_upgrade (notmuch, upgrade_print_progress,
+ &add_files_state);
+ printf ("Your notmuch database has now been upgraded to database format version %u.\n",
+ notmuch_database_get_version (notmuch));
+ }
+
+ add_files_state.total_files = 0;
+ }
+
+ if (notmuch == NULL)
+ return 1;
+
+ /* Setup our handler for SIGINT. We do this after having
+ * potentially done a database upgrade we this interrupt handler
+ * won't support. */
+ memset (&action, 0, sizeof (struct sigaction));
+ action.sa_handler = handle_sigint;
+ sigemptyset (&action.sa_mask);
+ action.sa_flags = SA_RESTART;
+ sigaction (SIGINT, &action, NULL);
+
+ talloc_free (dot_notmuch_path);
+ dot_notmuch_path = NULL;
+
+ add_files_state.processed_files = 0;
+ add_files_state.added_messages = 0;
+ gettimeofday (&add_files_state.tv_start, NULL);
+
+ add_files_state.removed_files = _filename_list_create (ctx);
+ add_files_state.removed_directories = _filename_list_create (ctx);
+
+ ret = add_files (notmuch, db_path, &add_files_state);
+
+ removed_files = 0;
+ renamed_files = 0;
+ for (f = add_files_state.removed_files->head; f; f = f->next) {
+ status = notmuch_database_remove_message (notmuch, f->filename);
+ if (status == NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID)
+ renamed_files++;
+ else
+ removed_files++;
+ }
+
+ for (f = add_files_state.removed_directories->head; f; f = f->next) {
+ _remove_directory (ctx, notmuch, f->filename,
+ &renamed_files, &removed_files);
+ }
+
+ talloc_free (add_files_state.removed_files);
+ talloc_free (add_files_state.removed_directories);
+
+ gettimeofday (&tv_now, NULL);
+ elapsed = notmuch_time_elapsed (add_files_state.tv_start,
+ tv_now);
+
+ if (add_files_state.processed_files) {
+ printf ("Processed %d %s in ", add_files_state.processed_files,
+ add_files_state.processed_files == 1 ?
+ "file" : "total files");
+ notmuch_time_print_formatted_seconds (elapsed);
+ if (elapsed > 1) {
+ printf (" (%d files/sec.). \n",
+ (int) (add_files_state.processed_files / elapsed));
+ } else {
+ printf (". \n");
+ }
+ }
+
+ if (add_files_state.added_messages) {
+ printf ("Added %d new %s to the database.",
+ add_files_state.added_messages,
+ add_files_state.added_messages == 1 ?
+ "message" : "messages");
+ } else {
+ printf ("No new mail.");
+ }
+
+ if (removed_files) {
+ printf (" Removed %d %s.",
+ removed_files,
+ removed_files == 1 ? "message" : "messages");
+ }
+
+ if (renamed_files) {
+ printf (" Detected %d file %s.",
+ renamed_files,
+ renamed_files == 1 ? "rename" : "renames");
+ }
+
+ printf ("\n");
+
+ if (ret) {
+ printf ("\nNote: At least one error was encountered: %s\n",
+ notmuch_status_to_string (ret));
+ }
+
+ notmuch_database_close (notmuch);
+
+ return ret || interrupted;
+}
diff --git a/notmuch-reply.c b/notmuch-reply.c
new file mode 100644
index 0000000..230cacc
--- /dev/null
+++ b/notmuch-reply.c
@@ -0,0 +1,569 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ * Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+#include "gmime-filter-reply.h"
+#include "gmime-filter-headers.h"
+
+static void
+reply_part_content (GMimeObject *part)
+{
+ GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
+ GMimeDataWrapper *wrapper;
+ const char *charset;
+
+ charset = g_mime_object_get_content_type_parameter (part, "charset");
+ stream_stdout = g_mime_stream_file_new (stdout);
+ if (stream_stdout) {
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+ stream_filter = g_mime_stream_filter_new(stream_stdout);
+ if (charset) {
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
+ g_mime_filter_charset_new(charset, "UTF-8"));
+ }
+ }
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
+ g_mime_filter_reply_new(TRUE));
+ wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+ if (wrapper && stream_filter)
+ g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+ if (stream_filter)
+ g_object_unref(stream_filter);
+ if (stream_stdout)
+ g_object_unref(stream_stdout);
+}
+
+static void
+show_reply_headers (GMimeMessage *message)
+{
+ GMimeStream *stream_stdout = NULL, *stream_filter = NULL;
+
+ stream_stdout = g_mime_stream_file_new (stdout);
+ if (stream_stdout) {
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+ stream_filter = g_mime_stream_filter_new(stream_stdout);
+ if (stream_filter) {
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
+ g_mime_filter_headers_new());
+ g_mime_object_write_to_stream(GMIME_OBJECT(message), stream_filter);
+ g_object_unref(stream_filter);
+ }
+ g_object_unref(stream_stdout);
+ }
+}
+
+static void
+reply_part (GMimeObject *part, int *part_count)
+{
+ GMimeContentDisposition *disposition;
+ GMimeContentType *content_type;
+
+ (void) part_count;
+ 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));
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html"))
+ {
+ reply_part_content (part);
+ }
+ else
+ {
+ printf ("Attachment: %s (%s)\n", filename,
+ g_mime_content_type_to_string (content_type));
+ }
+
+ return;
+ }
+
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html"))
+ {
+ reply_part_content (part);
+ }
+ else
+ {
+ printf ("Non-text part: %s\n",
+ g_mime_content_type_to_string (content_type));
+ }
+}
+
+/* Is the given address configured as one of the user's "personal" or
+ * "other" addresses. */
+static int
+address_is_users (const char *address, notmuch_config_t *config)
+{
+ const char *primary;
+ char **other;
+ size_t i, other_len;
+
+ primary = notmuch_config_get_user_primary_email (config);
+ if (strcasecmp (primary, address) == 0)
+ return 1;
+
+ other = notmuch_config_get_user_other_email (config, &other_len);
+ for (i = 0; i < other_len; i++)
+ if (strcasecmp (other[i], address) == 0)
+ return 1;
+
+ return 0;
+}
+
+/* For each address in 'list' that is not configured as one of the
+ * user's addresses in 'config', add that address to 'message' as an
+ * address of 'type'.
+ *
+ * The first address encountered that *is* the user's address will be
+ * returned, (otherwise NULL is returned).
+ */
+static const char *
+add_recipients_for_address_list (GMimeMessage *message,
+ notmuch_config_t *config,
+ GMimeRecipientType type,
+ InternetAddressList *list)
+{
+ InternetAddress *address;
+ int i;
+ const char *ret = NULL;
+
+ for (i = 0; i < internet_address_list_length (list); i++) {
+ address = internet_address_list_get_address (list, i);
+ if (INTERNET_ADDRESS_IS_GROUP (address)) {
+ InternetAddressGroup *group;
+ InternetAddressList *group_list;
+
+ group = INTERNET_ADDRESS_GROUP (address);
+ group_list = internet_address_group_get_members (group);
+ if (group_list == NULL)
+ continue;
+
+ add_recipients_for_address_list (message, config,
+ type, group_list);
+ } else {
+ InternetAddressMailbox *mailbox;
+ const char *name;
+ const char *addr;
+
+ mailbox = INTERNET_ADDRESS_MAILBOX (address);
+
+ name = internet_address_get_name (address);
+ addr = internet_address_mailbox_get_addr (mailbox);
+
+ if (address_is_users (addr, config)) {
+ if (ret == NULL)
+ ret = addr;
+ } else {
+ g_mime_message_add_recipient (message, type, name, addr);
+ }
+ }
+ }
+
+ return ret;
+}
+
+/* For each address in 'recipients' that is not configured as one of
+ * the user's addresses in 'config', add that address to 'message' as
+ * an address of 'type'.
+ *
+ * The first address encountered that *is* the user's address will be
+ * returned, (otherwise NULL is returned).
+ */
+static const char *
+add_recipients_for_string (GMimeMessage *message,
+ notmuch_config_t *config,
+ GMimeRecipientType type,
+ const char *recipients)
+{
+ InternetAddressList *list;
+
+ list = internet_address_list_parse_string (recipients);
+ if (list == NULL)
+ return NULL;
+
+ return add_recipients_for_address_list (message, config, type, list);
+}
+
+/* Does the address in the Reply-To header of 'message' already appear
+ * in either the 'To' or 'Cc' header of the message?
+ */
+static int
+reply_to_header_is_redundant (notmuch_message_t *message)
+{
+ const char *header, *addr;
+ InternetAddressList *list;
+ InternetAddress *address;
+ InternetAddressMailbox *mailbox;
+
+ header = notmuch_message_get_header (message, "reply-to");
+ if (*header == '\0')
+ return 0;
+
+ list = internet_address_list_parse_string (header);
+
+ if (internet_address_list_length (list) != 1)
+ return 0;
+
+ address = internet_address_list_get_address (list, 0);
+ if (INTERNET_ADDRESS_IS_GROUP (address))
+ return 0;
+
+ mailbox = INTERNET_ADDRESS_MAILBOX (address);
+ addr = internet_address_mailbox_get_addr (mailbox);
+
+ if (strstr (notmuch_message_get_header (message, "to"), addr) != 0 ||
+ strstr (notmuch_message_get_header (message, "cc"), addr) != 0)
+ {
+ return 1;
+ }
+
+ return 0;
+}
+
+/* Augments the recipients of reply from the headers of message.
+ *
+ * If any of the user's addresses were found in these headers, the first
+ * of these returned, otherwise NULL is returned.
+ */
+static const char *
+add_recipients_from_message (GMimeMessage *reply,
+ notmuch_config_t *config,
+ notmuch_message_t *message)
+{
+ struct {
+ const char *header;
+ const char *fallback;
+ GMimeRecipientType recipient_type;
+ } reply_to_map[] = {
+ { "reply-to", "from", GMIME_RECIPIENT_TYPE_TO },
+ { "to", NULL, GMIME_RECIPIENT_TYPE_TO },
+ { "cc", NULL, GMIME_RECIPIENT_TYPE_CC },
+ { "bcc", NULL, GMIME_RECIPIENT_TYPE_BCC }
+ };
+ const char *from_addr = NULL;
+ unsigned int i;
+
+ /* Some mailing lists munge the Reply-To header despite it being A Bad
+ * Thing, see http://www.unicom.com/pw/reply-to-harmful.html
+ *
+ * The munging is easy to detect, because it results in a
+ * redundant reply-to header, (with an address that already exists
+ * in either To or Cc). So in this case, we ignore the Reply-To
+ * field and use the From header. Thie ensures the original sender
+ * will get the reply even if not subscribed to the list. Note
+ * that the address in the Reply-To header will always appear in
+ * the reply.
+ */
+ if (reply_to_header_is_redundant (message)) {
+ reply_to_map[0].header = "from";
+ reply_to_map[0].fallback = NULL;
+ }
+
+ for (i = 0; i < ARRAY_SIZE (reply_to_map); i++) {
+ const char *addr, *recipients;
+
+ recipients = notmuch_message_get_header (message,
+ reply_to_map[i].header);
+ if ((recipients == NULL || recipients[0] == '\0') && reply_to_map[i].fallback)
+ recipients = notmuch_message_get_header (message,
+ reply_to_map[i].fallback);
+
+ addr = add_recipients_for_string (reply, config,
+ reply_to_map[i].recipient_type,
+ recipients);
+ if (from_addr == NULL)
+ from_addr = addr;
+ }
+
+ return from_addr;
+}
+
+static const char *
+guess_from_received_header (notmuch_config_t *config, notmuch_message_t *message)
+{
+ const char *received,*primary;
+ char **other;
+ char *by,*mta,*ptr,*token;
+ char *domain=NULL;
+ char *tld=NULL;
+ const char *delim=". \t";
+ size_t i,other_len;
+
+ received = notmuch_message_get_header (message, "received");
+ by = strstr (received, " by ");
+ if (by && *(by+4)) {
+ /* sadly, the format of Received: headers is a bit inconsistent,
+ * depending on the MTA used. So we try to extract just the MTA
+ * here by removing leading whitespace and assuming that the MTA
+ * name ends at the next whitespace
+ * we test for *(by+4) to be non-'\0' to make sure there's something
+ * there at all - and then assume that the first whitespace delimited
+ * token that follows is the last receiving server
+ */
+ mta = strdup (by+4);
+ if (mta == NULL)
+ return NULL;
+ token = strtok(mta," \t");
+ if (token == NULL)
+ return NULL;
+ /* Now extract the last two components of the MTA host name
+ * as domain and tld
+ */
+ while ((ptr = strsep (&token, delim)) != NULL) {
+ if (*ptr == '\0')
+ continue;
+ domain = tld;
+ tld = ptr;
+ }
+
+ if (domain) {
+ /* recombine domain and tld and look for it among the configured
+ * email addresses
+ */
+ *(tld-1) = '.';
+ primary = notmuch_config_get_user_primary_email (config);
+ if (strcasestr (primary, domain)) {
+ free (mta);
+ return primary;
+ }
+ other = notmuch_config_get_user_other_email (config, &other_len);
+ for (i = 0; i < other_len; i++)
+ if (strcasestr (other[i], domain)) {
+ free (mta);
+ return other[i];
+ }
+ }
+
+ free (mta);
+ }
+
+ return NULL;
+}
+
+static int
+notmuch_reply_format_default(void *ctx, notmuch_config_t *config, notmuch_query_t *query)
+{
+ GMimeMessage *reply;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ const char *subject, *from_addr = NULL;
+ const char *in_reply_to, *orig_references, *references;
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ /* The 1 means we want headers in a "pretty" order. */
+ reply = g_mime_message_new (1);
+ if (reply == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ subject = notmuch_message_get_header (message, "subject");
+ if (strncasecmp (subject, "Re:", 3))
+ subject = talloc_asprintf (ctx, "Re: %s", subject);
+ g_mime_message_set_subject (reply, subject);
+
+ from_addr = add_recipients_from_message (reply, config, message);
+
+ if (from_addr == NULL)
+ from_addr = guess_from_received_header (config, message);
+
+ if (from_addr == NULL)
+ from_addr = notmuch_config_get_user_primary_email (config);
+
+ from_addr = talloc_asprintf (ctx, "%s <%s>",
+ notmuch_config_get_user_name (config),
+ from_addr);
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "From", from_addr);
+
+ g_mime_object_set_header (GMIME_OBJECT (reply), "Bcc",
+ notmuch_config_get_user_primary_email (config));
+
+ in_reply_to = talloc_asprintf (ctx, "<%s>",
+ notmuch_message_get_message_id (message));
+
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "In-Reply-To", in_reply_to);
+
+ orig_references = notmuch_message_get_header (message, "references");
+ references = talloc_asprintf (ctx, "%s%s%s",
+ orig_references ? orig_references : "",
+ orig_references ? " " : "",
+ in_reply_to);
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "References", references);
+
+ show_reply_headers (reply);
+
+ g_object_unref (G_OBJECT (reply));
+ reply = NULL;
+
+ printf ("On %s, %s wrote:\n",
+ notmuch_message_get_header (message, "date"),
+ notmuch_message_get_header (message, "from"));
+
+ show_message_body (notmuch_message_get_filename (message), reply_part);
+
+ notmuch_message_destroy (message);
+ }
+ return 0;
+}
+
+/* This format is currently tuned for a git send-email --notmuch hook */
+static int
+notmuch_reply_format_headers_only(void *ctx, notmuch_config_t *config, notmuch_query_t *query)
+{
+ GMimeMessage *reply;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ const char *in_reply_to, *orig_references, *references;
+ char *reply_headers;
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ /* The 0 means we do not want headers in a "pretty" order. */
+ reply = g_mime_message_new (0);
+ if (reply == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ in_reply_to = talloc_asprintf (ctx, "<%s>",
+ notmuch_message_get_message_id (message));
+
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "In-Reply-To", in_reply_to);
+
+
+ orig_references = notmuch_message_get_header (message, "references");
+
+ /* We print In-Reply-To followed by References because git format-patch treats them
+ * specially. Git does not interpret the other headers specially
+ */
+ references = talloc_asprintf (ctx, "%s%s%s",
+ orig_references ? orig_references : "",
+ orig_references ? " " : "",
+ in_reply_to);
+ g_mime_object_set_header (GMIME_OBJECT (reply),
+ "References", references);
+
+ (void)add_recipients_from_message (reply, config, message);
+
+ g_mime_object_set_header (GMIME_OBJECT (reply), "Bcc",
+ notmuch_config_get_user_primary_email (config));
+
+ reply_headers = g_mime_object_to_string (GMIME_OBJECT (reply));
+ printf ("%s", reply_headers);
+ free (reply_headers);
+
+ g_object_unref (G_OBJECT (reply));
+ reply = NULL;
+
+ notmuch_message_destroy (message);
+ }
+ return 0;
+}
+
+int
+notmuch_reply_command (void *ctx, int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ char *opt, *query_string;
+ int i, ret = 0;
+ int (*reply_format_func)(void *ctx, notmuch_config_t *config, notmuch_query_t *query);
+
+ reply_format_func = notmuch_reply_format_default;
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+ opt = argv[i] + sizeof ("--format=") - 1;
+ if (strcmp (opt, "default") == 0) {
+ reply_format_func = notmuch_reply_format_default;
+ } else if (strcmp (opt, "headers-only") == 0) {
+ reply_format_func = notmuch_reply_format_headers_only;
+ } else {
+ fprintf (stderr, "Invalid value for --format: %s\n", opt);
+ return 1;
+ }
+ } else {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ argc -= i;
+ argv += i;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ query_string = query_string_from_args (ctx, argc, argv);
+ if (query_string == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ if (*query_string == '\0') {
+ fprintf (stderr, "Error: notmuch reply requires at least one search term.\n");
+ return 1;
+ }
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ if (reply_format_func (ctx, config, query) != 0)
+ return 1;
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return ret;
+}
diff --git a/notmuch-restore.c b/notmuch-restore.c
new file mode 100644
index 0000000..b0a4e1c
--- /dev/null
+++ b/notmuch-restore.c
@@ -0,0 +1,152 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+int
+notmuch_restore_command (unused (void *ctx), int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ FILE *input;
+ char *line = NULL;
+ size_t line_size;
+ ssize_t line_len;
+ regex_t regex;
+ int rerr;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_WRITE);
+ if (notmuch == NULL)
+ return 1;
+
+ if (argc) {
+ input = fopen (argv[0], "r");
+ if (input == NULL) {
+ fprintf (stderr, "Error opening %s for reading: %s\n",
+ argv[0], strerror (errno));
+ return 1;
+ }
+ } else {
+ printf ("No filename given. Reading dump from stdin.\n");
+ input = stdin;
+ }
+
+ /* Dump output is one line per message. We match a sequence of
+ * non-space characters for the message-id, then one or more
+ * spaces, then a list of space-separated tags as a sequence of
+ * characters within literal '(' and ')'. */
+ xregcomp (&regex,
+ "^([^ ]+) \\(([^)]*)\\)$",
+ REG_EXTENDED);
+
+ while ((line_len = getline (&line, &line_size, input)) != -1) {
+ regmatch_t match[3];
+ char *message_id, *file_tags, *tag, *next;
+ notmuch_message_t *message = NULL;
+ notmuch_status_t status;
+ notmuch_tags_t *db_tags;
+ char *db_tags_str;
+
+ chomp_newline (line);
+
+ rerr = xregexec (&regex, line, 3, match, 0);
+ if (rerr == REG_NOMATCH)
+ {
+ fprintf (stderr, "Warning: Ignoring invalid input line: %s\n",
+ line);
+ continue;
+ }
+
+ message_id = xstrndup (line + match[1].rm_so,
+ match[1].rm_eo - match[1].rm_so);
+ file_tags = xstrndup (line + match[2].rm_so,
+ match[2].rm_eo - match[2].rm_so);
+
+ message = notmuch_database_find_message (notmuch, message_id);
+ if (message == NULL) {
+ fprintf (stderr, "Warning: Cannot apply tags to missing message: %s\n",
+ message_id);
+ goto NEXT_LINE;
+ }
+
+ db_tags_str = NULL;
+ for (db_tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (db_tags);
+ notmuch_tags_move_to_next (db_tags))
+ {
+ const char *tag = notmuch_tags_get (db_tags);
+
+ if (db_tags_str)
+ db_tags_str = talloc_asprintf_append (db_tags_str, " %s", tag);
+ else
+ db_tags_str = talloc_strdup (message, tag);
+ }
+
+ if (((file_tags == NULL || *file_tags == '\0') &&
+ (db_tags_str == NULL || *db_tags_str == '\0')) ||
+ (file_tags && db_tags_str && strcmp (file_tags, db_tags_str) == 0))
+ {
+ goto NEXT_LINE;
+ }
+
+ notmuch_message_freeze (message);
+ notmuch_message_remove_all_tags (message);
+
+ next = file_tags;
+ while (next) {
+ tag = strsep (&next, " ");
+ if (*tag == '\0')
+ continue;
+ status = notmuch_message_add_tag (message, tag);
+ if (status) {
+ fprintf (stderr,
+ "Error applying tag %s to message %s:\n",
+ tag, message_id);
+ fprintf (stderr, "%s\n",
+ notmuch_status_to_string (status));
+ }
+ }
+
+ notmuch_message_thaw (message);
+
+ NEXT_LINE:
+ if (message)
+ notmuch_message_destroy (message);
+ message = NULL;
+ free (message_id);
+ free (file_tags);
+ }
+
+ regfree (&regex);
+
+ if (line)
+ free (line);
+
+ notmuch_database_close (notmuch);
+ if (input != stdin)
+ fclose (input);
+
+ return 0;
+}
diff --git a/notmuch-search-tags.c b/notmuch-search-tags.c
new file mode 100644
index 0000000..6f3cfcc
--- /dev/null
+++ b/notmuch-search-tags.c
@@ -0,0 +1,98 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Jan Janak
+ *
+ * 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: Jan Janak <jan@ryngle.com>
+ */
+
+#include "notmuch-client.h"
+
+static void
+print_tags (notmuch_tags_t *tags)
+{
+ const char *t;
+
+ while ((t = notmuch_tags_get (tags))) {
+ printf ("%s\n", t);
+ notmuch_tags_move_to_next (tags);
+ }
+}
+
+int
+notmuch_search_tags_command (void *ctx, int argc, char *argv[])
+{
+ notmuch_messages_t *msgs;
+ notmuch_tags_t *tags;
+ notmuch_config_t *config;
+ notmuch_database_t *db;
+ notmuch_query_t *query;
+ char *query_str;
+
+ tags = NULL;
+ config = NULL;
+ db = NULL;
+ query = NULL;
+
+ if ((config = notmuch_config_open (ctx, NULL, NULL)) == NULL) {
+ goto error;
+ }
+
+ db = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (db == NULL) {
+ goto error;
+ }
+
+ if (argc > 0) {
+ if ((query_str = query_string_from_args (ctx, argc, argv)) == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ goto error;
+ }
+
+ if (*query_str == '\0') {
+ fprintf (stderr, "Error: Invalid search string.\n");
+ goto error;
+ }
+
+ if ((query = notmuch_query_create (db, query_str)) == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ goto error;
+ }
+
+
+ msgs = notmuch_query_search_messages (query);
+ if ((tags = notmuch_messages_collect_tags (msgs)) == NULL) goto error;
+ } else {
+ if ((tags = notmuch_database_get_all_tags (db)) == NULL) {
+ fprintf (stderr, "Error while getting tags from the database.\n");
+ goto error;
+ }
+ }
+
+ print_tags (tags);
+
+ notmuch_tags_destroy (tags);
+ if (query) notmuch_query_destroy (query);
+ notmuch_database_close (db);
+ return 0;
+
+error:
+ if (tags) notmuch_tags_destroy (tags);
+ if (query) notmuch_query_destroy (query);
+ if (db) notmuch_database_close (db);
+ return 1;
+}
diff --git a/notmuch-search.c b/notmuch-search.c
new file mode 100644
index 0000000..4e3514b
--- /dev/null
+++ b/notmuch-search.c
@@ -0,0 +1,276 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+typedef struct search_format {
+ const char *results_start;
+ const char *thread_start;
+ void (*thread) (const void *ctx,
+ const char *thread_id,
+ const time_t date,
+ const int matched,
+ const int total,
+ const char *authors,
+ const char *subject);
+ const char *tag_start;
+ const char *tag;
+ const char *tag_sep;
+ const char *tag_end;
+ const char *thread_sep;
+ const char *thread_end;
+ const char *results_end;
+} search_format_t;
+
+static void
+format_thread_text (const void *ctx,
+ const char *thread_id,
+ const time_t date,
+ const int matched,
+ const int total,
+ const char *authors,
+ const char *subject);
+static const search_format_t format_text = {
+ "",
+ "",
+ format_thread_text,
+ " (",
+ "%s", " ",
+ ")", "",
+ "\n",
+ "",
+};
+
+static void
+format_thread_json (const void *ctx,
+ const char *thread_id,
+ const time_t date,
+ const int matched,
+ const int total,
+ const char *authors,
+ const char *subject);
+static const search_format_t format_json = {
+ "[",
+ "{",
+ format_thread_json,
+ "\"tags\": [",
+ "\"%s\"", ", ",
+ "]", ",\n",
+ "}",
+ "]\n",
+};
+
+static void
+format_thread_text (const void *ctx,
+ const char *thread_id,
+ const time_t date,
+ const int matched,
+ const int total,
+ const char *authors,
+ const char *subject)
+{
+ printf ("thread:%s %12s [%d/%d] %s; %s",
+ thread_id,
+ notmuch_time_relative_date (ctx, date),
+ matched,
+ total,
+ authors,
+ subject);
+}
+
+static void
+format_thread_json (const void *ctx,
+ const char *thread_id,
+ const time_t date,
+ const int matched,
+ const int total,
+ const char *authors,
+ const char *subject)
+{
+ struct tm *tm;
+ char timestamp[40];
+ void *ctx_quote = talloc_new (ctx);
+
+ tm = gmtime (&date);
+ if (tm == NULL)
+ INTERNAL_ERROR ("gmtime failed on thread %s.", thread_id);
+
+ if (strftime (timestamp, sizeof (timestamp), "%s", tm) == 0)
+ INTERNAL_ERROR ("strftime failed on thread %s.", thread_id);
+
+ printf ("\"thread\": %s,\n"
+ "\"timestamp\": %s,\n"
+ "\"matched\": %d,\n"
+ "\"total\": %d,\n"
+ "\"authors\": %s,\n"
+ "\"subject\": %s,\n",
+ json_quote_str (ctx_quote, thread_id),
+ timestamp,
+ matched,
+ total,
+ json_quote_str (ctx_quote, authors),
+ json_quote_str (ctx_quote, subject));
+
+ talloc_free (ctx_quote);
+}
+
+static void
+do_search_threads (const void *ctx,
+ const search_format_t *format,
+ notmuch_query_t *query,
+ notmuch_sort_t sort)
+{
+ notmuch_thread_t *thread;
+ notmuch_threads_t *threads;
+ notmuch_tags_t *tags;
+ time_t date;
+ int first_thread = 1;
+
+ fputs (format->results_start, stdout);
+
+ for (threads = notmuch_query_search_threads (query);
+ notmuch_threads_valid (threads);
+ notmuch_threads_move_to_next (threads))
+ {
+ int first_tag = 1;
+
+ if (! first_thread)
+ fputs (format->thread_sep, stdout);
+
+ thread = notmuch_threads_get (threads);
+
+ if (sort == NOTMUCH_SORT_OLDEST_FIRST)
+ date = notmuch_thread_get_oldest_date (thread);
+ else
+ date = notmuch_thread_get_newest_date (thread);
+
+ fputs (format->thread_start, stdout);
+
+ format->thread (ctx,
+ notmuch_thread_get_thread_id (thread),
+ date,
+ notmuch_thread_get_matched_messages (thread),
+ notmuch_thread_get_total_messages (thread),
+ notmuch_thread_get_authors (thread),
+ notmuch_thread_get_subject (thread));
+
+ fputs (format->tag_start, stdout);
+
+ for (tags = notmuch_thread_get_tags (thread);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ if (! first_tag)
+ fputs (format->tag_sep, stdout);
+ printf (format->tag, notmuch_tags_get (tags));
+ first_tag = 0;
+ }
+
+ fputs (format->tag_end, stdout);
+ fputs (format->thread_end, stdout);
+
+ first_thread = 0;
+
+ notmuch_thread_destroy (thread);
+ }
+
+ fputs (format->results_end, stdout);
+}
+
+int
+notmuch_search_command (void *ctx, int argc, char *argv[])
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ char *query_str;
+ char *opt;
+ notmuch_sort_t sort = NOTMUCH_SORT_NEWEST_FIRST;
+ const search_format_t *format = &format_text;
+ int i;
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (STRNCMP_LITERAL (argv[i], "--sort=") == 0) {
+ opt = argv[i] + sizeof ("--sort=") - 1;
+ if (strcmp (opt, "oldest-first") == 0) {
+ sort = NOTMUCH_SORT_OLDEST_FIRST;
+ } else if (strcmp (opt, "newest-first") == 0) {
+ sort = NOTMUCH_SORT_NEWEST_FIRST;
+ } else {
+ fprintf (stderr, "Invalid value for --sort: %s\n", opt);
+ return 1;
+ }
+ } else if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+ opt = argv[i] + sizeof ("--format=") - 1;
+ if (strcmp (opt, "text") == 0) {
+ format = &format_text;
+ } else if (strcmp (opt, "json") == 0) {
+ format = &format_json;
+ } else {
+ fprintf (stderr, "Invalid value for --format: %s\n", opt);
+ return 1;
+ }
+ } else {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ argc -= i;
+ argv += i;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query_str = query_string_from_args (ctx, argc, argv);
+ if (query_str == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+ if (*query_str == '\0') {
+ fprintf (stderr, "Error: notmuch search requires at least one search term.\n");
+ return 1;
+ }
+
+ query = notmuch_query_create (notmuch, query_str);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ notmuch_query_set_sort (query, sort);
+
+ do_search_threads (ctx, format, query, sort);
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return 0;
+}
diff --git a/notmuch-setup.c b/notmuch-setup.c
new file mode 100644
index 0000000..622bbaa
--- /dev/null
+++ b/notmuch-setup.c
@@ -0,0 +1,169 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+static const char *
+make_path_absolute (void *ctx, const char *path)
+{
+ char *cwd;
+
+ if (*path == '/')
+ return path;
+
+ cwd = getcwd (NULL, 0);
+ if (cwd == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return NULL;
+ }
+
+ path = talloc_asprintf (ctx, "%s/%s", cwd, path);
+ if (path == NULL)
+ fprintf (stderr, "Out of memory.\n");
+
+ free (cwd);
+
+ return path;
+}
+
+static void
+welcome_message_pre_setup (void)
+{
+ printf (
+"Welcome to notmuch!\n\n"
+
+"The goal of notmuch is to help you manage and search your collection of\n"
+"email, and to efficiently keep up with the flow of email as it comes in.\n\n"
+
+"Notmuch needs to know a few things about you such as your name and email\n"
+"address, as well as the directory that contains your email. This is where\n"
+"you already have mail stored and where messages will be delivered in the\n"
+"future. This directory can contain any number of sub-directories. Regular\n"
+"files in these directories should be individual email messages. If there\n"
+"are other, non-email files (such as indexes maintained by other email\n"
+"programs) then notmuch will do its best to detect those and ignore them.\n\n"
+
+"If you already have your email being delivered to directories in either\n"
+"maildir or mh format, then that's perfect. Mail storage that uses mbox\n"
+"format, (where one mbox file contains many messages), will not work with\n"
+"notmuch. If that's how your mail is currently stored, we recommend you\n"
+"first convert it to maildir format with a utility such as mb2md. You can\n"
+"continue configuring notmuch now, but be sure to complete the conversion\n"
+"before you run \"notmuch new\" for the first time.\n\n");
+}
+
+static void
+welcome_message_post_setup (void)
+{
+ printf ("\n"
+"Notmuch is now configured, and the configuration settings are saved in\n"
+"a file in your home directory named .notmuch-config . If you'd like to\n"
+"change the configuration in the future, you can either edit that file\n"
+"directly or run \"notmuch setup\". To choose an alternate configuration\n"
+"location, set ${NOTMUCH_CONFIG}.\n\n"
+
+"The next step is to run \"notmuch new\" which will create a database\n"
+"that indexes all of your mail. Depending on the amount of mail you have\n"
+"the initial indexing process can take a long time, so expect that.\n"
+"Also, the resulting database will require roughly the same amount of\n"
+"storage space as your current collection of email. So please ensure you\n"
+"have sufficient storage space available now.\n\n");
+}
+
+int
+notmuch_setup_command (unused (void *ctx),
+ unused (int argc), unused (char *argv[]))
+{
+ char *response = NULL;
+ size_t response_size;
+ notmuch_config_t *config;
+ char **old_other_emails;
+ size_t old_other_emails_len;
+ GPtrArray *other_emails;
+ unsigned int i;
+ int is_new;
+
+#define prompt(format, ...) \
+ do { \
+ printf (format, ##__VA_ARGS__); \
+ fflush (stdout); \
+ if (getline (&response, &response_size, stdin) < 0) { \
+ printf ("Exiting.\n"); \
+ exit (1); \
+ } \
+ chomp_newline (response); \
+ } while (0)
+
+ config = notmuch_config_open (ctx, NULL, &is_new);
+
+ if (is_new)
+ welcome_message_pre_setup ();
+
+ prompt ("Your full name [%s]: ", notmuch_config_get_user_name (config));
+ if (strlen (response))
+ notmuch_config_set_user_name (config, response);
+
+ prompt ("Your primary email address [%s]: ",
+ notmuch_config_get_user_primary_email (config));
+ if (strlen (response))
+ notmuch_config_set_user_primary_email (config, response);
+
+ other_emails = g_ptr_array_new ();
+
+ old_other_emails = notmuch_config_get_user_other_email (config,
+ &old_other_emails_len);
+ for (i = 0; i < old_other_emails_len; i++) {
+ prompt ("Additional email address [%s]: ", old_other_emails[i]);
+ if (strlen (response))
+ g_ptr_array_add (other_emails, talloc_strdup (ctx, response));
+ else
+ g_ptr_array_add (other_emails, talloc_strdup (ctx,
+ old_other_emails[i]));
+ }
+
+ do {
+ prompt ("Additional email address [Press 'Enter' if none]: ");
+ if (strlen (response))
+ g_ptr_array_add (other_emails, talloc_strdup (ctx, response));
+ } while (strlen (response));
+ if (other_emails->len)
+ notmuch_config_set_user_other_email (config,
+ (const char **)
+ other_emails->pdata,
+ other_emails->len);
+ g_ptr_array_free (other_emails, TRUE);
+
+ prompt ("Top-level directory of your email archive [%s]: ",
+ notmuch_config_get_database_path (config));
+ if (strlen (response)) {
+ const char *absolute_path;
+
+ absolute_path = make_path_absolute (ctx, response);
+ notmuch_config_set_database_path (config, absolute_path);
+ }
+
+ if (! notmuch_config_save (config)) {
+ if (is_new)
+ welcome_message_post_setup ();
+ return 0;
+ } else {
+ return 1;
+ }
+}
diff --git a/notmuch-show.c b/notmuch-show.c
new file mode 100644
index 0000000..76873a1
--- /dev/null
+++ b/notmuch-show.c
@@ -0,0 +1,579 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+typedef struct show_format {
+ const char *message_set_start;
+ const char *message_start;
+ void (*message) (const void *ctx,
+ notmuch_message_t *message,
+ int indent);
+ const char *header_start;
+ void (*header) (const void *ctx,
+ notmuch_message_t *message);
+ const char *header_end;
+ const char *body_start;
+ void (*part) (GMimeObject *part,
+ int *part_count);
+ const char *body_end;
+ const char *message_end;
+ const char *message_set_sep;
+ const char *message_set_end;
+} show_format_t;
+
+static void
+format_message_text (unused (const void *ctx),
+ notmuch_message_t *message,
+ int indent);
+static void
+format_headers_text (const void *ctx,
+ notmuch_message_t *message);
+static void
+format_part_text (GMimeObject *part,
+ int *part_count);
+static const show_format_t format_text = {
+ "",
+ "\fmessage{ ", format_message_text,
+ "\fheader{\n", format_headers_text, "\fheader}\n",
+ "\fbody{\n", format_part_text, "\fbody}\n",
+ "\fmessage}\n", "",
+ ""
+};
+
+static void
+format_message_json (const void *ctx,
+ notmuch_message_t *message,
+ unused (int indent));
+static void
+format_headers_json (const void *ctx,
+ notmuch_message_t *message);
+static void
+format_part_json (GMimeObject *part,
+ int *part_count);
+static const show_format_t format_json = {
+ "[",
+ "{", format_message_json,
+ ", \"headers\": {", format_headers_json, "}",
+ ", \"body\": [", format_part_json, "]",
+ "}", ", ",
+ "]"
+};
+
+static const char *
+_get_tags_as_string (const void *ctx, notmuch_message_t *message)
+{
+ notmuch_tags_t *tags;
+ int first = 1;
+ const char *tag;
+ char *result;
+
+ result = talloc_strdup (ctx, "");
+ if (result == NULL)
+ return NULL;
+
+ for (tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ tag = notmuch_tags_get (tags);
+
+ result = talloc_asprintf_append (result, "%s%s",
+ first ? "" : " ", tag);
+ first = 0;
+ }
+
+ return result;
+}
+
+/* Get a nice, single-line summary of message. */
+static const char *
+_get_one_line_summary (const void *ctx, notmuch_message_t *message)
+{
+ const char *from;
+ time_t date;
+ const char *relative_date;
+ const char *tags;
+
+ from = notmuch_message_get_header (message, "from");
+
+ date = notmuch_message_get_date (message);
+ relative_date = notmuch_time_relative_date (ctx, date);
+
+ tags = _get_tags_as_string (ctx, message);
+
+ return talloc_asprintf (ctx, "%s (%s) (%s)",
+ from, relative_date, tags);
+}
+
+static void
+format_message_text (unused (const void *ctx), notmuch_message_t *message, int indent)
+{
+ printf ("id:%s depth:%d match:%d filename:%s\n",
+ notmuch_message_get_message_id (message),
+ indent,
+ notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH),
+ notmuch_message_get_filename (message));
+}
+
+static void
+format_message_json (const void *ctx, notmuch_message_t *message, unused (int indent))
+{
+ notmuch_tags_t *tags;
+ int first = 1;
+ void *ctx_quote = talloc_new (ctx);
+ time_t date;
+ const char *relative_date;
+
+ date = notmuch_message_get_date (message);
+ relative_date = notmuch_time_relative_date (ctx, date);
+
+ printf ("\"id\": %s, \"match\": %s, \"filename\": %s, \"date_unix\": %ld, \"date_relative\": \"%s\", \"tags\": [",
+ json_quote_str (ctx_quote, notmuch_message_get_message_id (message)),
+ notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH) ? "true" : "false",
+ json_quote_str (ctx_quote, notmuch_message_get_filename (message)),
+ date, relative_date);
+
+ for (tags = notmuch_message_get_tags (message);
+ notmuch_tags_valid (tags);
+ notmuch_tags_move_to_next (tags))
+ {
+ printf("%s%s", first ? "" : ",",
+ json_quote_str (ctx_quote, notmuch_tags_get (tags)));
+ first = 0;
+ }
+ printf("]");
+ talloc_free (ctx_quote);
+}
+
+static void
+format_headers_text (const void *ctx, notmuch_message_t *message)
+{
+ const char *headers[] = {
+ "Subject", "From", "To", "Cc", "Bcc", "Date"
+ };
+ const char *name, *value;
+ unsigned int i;
+
+ printf ("%s\n", _get_one_line_summary (ctx, message));
+
+ for (i = 0; i < ARRAY_SIZE (headers); i++) {
+ name = headers[i];
+ value = notmuch_message_get_header (message, name);
+ if (value && strlen (value))
+ printf ("%s: %s\n", name, value);
+ }
+}
+
+static void
+format_headers_json (const void *ctx, notmuch_message_t *message)
+{
+ const char *headers[] = {
+ "Subject", "From", "To", "Cc", "Bcc", "Date"
+ };
+ const char *name, *value;
+ unsigned int i;
+ int first_header = 1;
+ void *ctx_quote = talloc_new (ctx);
+
+ for (i = 0; i < ARRAY_SIZE (headers); i++) {
+ name = headers[i];
+ value = notmuch_message_get_header (message, name);
+ if (value)
+ {
+ if (!first_header)
+ fputs (", ", stdout);
+ first_header = 0;
+
+ printf ("%s: %s",
+ json_quote_str (ctx_quote, name),
+ json_quote_str (ctx_quote, value));
+ }
+ }
+
+ talloc_free (ctx_quote);
+}
+
+static void
+show_part_content (GMimeObject *part, GMimeStream *stream_out)
+{
+ GMimeStream *stream_filter = NULL;
+ GMimeDataWrapper *wrapper;
+ const char *charset;
+
+ charset = g_mime_object_get_content_type_parameter (part, "charset");
+
+ if (stream_out) {
+ stream_filter = g_mime_stream_filter_new(stream_out);
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
+ g_mime_filter_crlf_new(FALSE, FALSE));
+ if (charset) {
+ g_mime_stream_filter_add(GMIME_STREAM_FILTER(stream_filter),
+ g_mime_filter_charset_new(charset, "UTF-8"));
+ }
+ }
+
+ wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+ if (wrapper && stream_filter)
+ g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+ if (stream_filter)
+ g_object_unref(stream_filter);
+}
+
+static void
+format_part_text (GMimeObject *part, int *part_count)
+{
+ GMimeContentDisposition *disposition;
+ GMimeContentType *content_type;
+
+ 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));
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ printf ("\fattachment{ ID: %d, Content-type: %s\n",
+ *part_count,
+ g_mime_content_type_to_string (content_type));
+ printf ("Attachment: %s (%s)\n", filename,
+ g_mime_content_type_to_string (content_type));
+
+ if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html"))
+ {
+ GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+ show_part_content (part, stream_stdout);
+ g_object_unref(stream_stdout);
+ }
+
+ printf ("\fattachment}\n");
+
+ return;
+ }
+
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ printf ("\fpart{ ID: %d, Content-type: %s\n",
+ *part_count,
+ g_mime_content_type_to_string (content_type));
+
+ if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html"))
+ {
+ GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream_stdout), FALSE);
+ show_part_content (part, stream_stdout);
+ g_object_unref(stream_stdout);
+ }
+ else
+ {
+ printf ("Non-text part: %s\n",
+ g_mime_content_type_to_string (content_type));
+ }
+
+ printf ("\fpart}\n");
+}
+
+static void
+format_part_json (GMimeObject *part, int *part_count)
+{
+ GMimeContentType *content_type;
+ GMimeContentDisposition *disposition;
+ void *ctx = talloc_new (NULL);
+ GMimeStream *stream_memory = g_mime_stream_mem_new ();
+ GByteArray *part_content;
+
+ content_type = g_mime_object_get_content_type (GMIME_OBJECT (part));
+
+ if (*part_count > 1)
+ fputs (", ", stdout);
+
+ printf ("{\"id\": %d, \"content-type\": %s",
+ *part_count,
+ json_quote_str (ctx, g_mime_content_type_to_string (content_type)));
+
+ 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));
+
+ printf (", \"filename\": %s", json_quote_str (ctx, filename));
+ }
+
+ if (g_mime_content_type_is_type (content_type, "text", "*") &&
+ !g_mime_content_type_is_type (content_type, "text", "html"))
+ {
+ show_part_content (part, stream_memory);
+ part_content = g_mime_stream_mem_get_byte_array (GMIME_STREAM_MEM (stream_memory));
+
+ printf (", \"content\": %s", json_quote_chararray (ctx, (char *) part_content->data, part_content->len));
+ }
+
+ fputs ("}", stdout);
+
+ talloc_free (ctx);
+ if (stream_memory)
+ g_object_unref (stream_memory);
+}
+
+static void
+show_message (void *ctx, const show_format_t *format, notmuch_message_t *message, int indent)
+{
+ fputs (format->message_start, stdout);
+ if (format->message)
+ format->message(ctx, message, indent);
+
+ fputs (format->header_start, stdout);
+ if (format->header)
+ format->header(ctx, message);
+ fputs (format->header_end, stdout);
+
+ fputs (format->body_start, stdout);
+ if (format->part)
+ show_message_body (notmuch_message_get_filename (message), format->part);
+ fputs (format->body_end, stdout);
+
+ fputs (format->message_end, stdout);
+}
+
+
+static void
+show_messages (void *ctx, const show_format_t *format, notmuch_messages_t *messages, int indent,
+ notmuch_bool_t entire_thread)
+{
+ notmuch_message_t *message;
+ notmuch_bool_t match;
+ int first_set = 1;
+ int next_indent;
+
+ fputs (format->message_set_start, stdout);
+
+ for (;
+ notmuch_messages_valid (messages);
+ notmuch_messages_move_to_next (messages))
+ {
+ if (!first_set)
+ fputs (format->message_set_sep, stdout);
+ first_set = 0;
+
+ fputs (format->message_set_start, stdout);
+
+ message = notmuch_messages_get (messages);
+
+ match = notmuch_message_get_flag (message, NOTMUCH_MESSAGE_FLAG_MATCH);
+
+ next_indent = indent;
+
+ if (match || entire_thread) {
+ show_message (ctx, format, message, indent);
+ next_indent = indent + 1;
+
+ fputs (format->message_set_sep, stdout);
+ }
+
+ show_messages (ctx, format, notmuch_message_get_replies (message),
+ next_indent, entire_thread);
+
+ notmuch_message_destroy (message);
+
+ fputs (format->message_set_end, stdout);
+ }
+
+ fputs (format->message_set_end, stdout);
+}
+
+int
+notmuch_show_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ notmuch_threads_t *threads;
+ notmuch_thread_t *thread;
+ notmuch_messages_t *messages;
+ char *query_string;
+ char *opt;
+ const show_format_t *format = &format_text;
+ int entire_thread = 0;
+ int i;
+ int first_toplevel = 1;
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (STRNCMP_LITERAL (argv[i], "--format=") == 0) {
+ opt = argv[i] + sizeof ("--format=") - 1;
+ if (strcmp (opt, "text") == 0) {
+ format = &format_text;
+ } else if (strcmp (opt, "json") == 0) {
+ format = &format_json;
+ entire_thread = 1;
+ } else {
+ fprintf (stderr, "Invalid value for --format: %s\n", opt);
+ return 1;
+ }
+ } else if (STRNCMP_LITERAL (argv[i], "--entire-thread") == 0) {
+ entire_thread = 1;
+ } else {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ argc -= i;
+ argv += i;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ query_string = query_string_from_args (ctx, argc, argv);
+ if (query_string == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ if (*query_string == '\0') {
+ fprintf (stderr, "Error: notmuch show requires at least one search term.\n");
+ return 1;
+ }
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ fputs (format->message_set_start, stdout);
+
+ for (threads = notmuch_query_search_threads (query);
+ notmuch_threads_valid (threads);
+ notmuch_threads_move_to_next (threads))
+ {
+ thread = notmuch_threads_get (threads);
+
+ messages = notmuch_thread_get_toplevel_messages (thread);
+
+ if (messages == NULL)
+ INTERNAL_ERROR ("Thread %s has no toplevel messages.\n",
+ notmuch_thread_get_thread_id (thread));
+
+ if (!first_toplevel)
+ fputs (format->message_set_sep, stdout);
+ first_toplevel = 0;
+
+ show_messages (ctx, format, messages, 0, entire_thread);
+
+ notmuch_thread_destroy (thread);
+
+ }
+
+ fputs (format->message_set_end, stdout);
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return 0;
+}
+
+int
+notmuch_part_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ char *query_string;
+ int i;
+ int part = 0;
+
+ for (i = 0; i < argc && argv[i][0] == '-'; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (STRNCMP_LITERAL (argv[i], "--part=") == 0) {
+ part = atoi(argv[i] + sizeof ("--part=") - 1);
+ } else {
+ fprintf (stderr, "Unrecognized option: %s\n", argv[i]);
+ return 1;
+ }
+ }
+
+ argc -= i;
+ argv += i;
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ query_string = query_string_from_args (ctx, argc, argv);
+ if (query_string == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ if (*query_string == '\0') {
+ fprintf (stderr, "Error: notmuch part requires at least one search term.\n");
+ return 1;
+ }
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_ONLY);
+ if (notmuch == NULL)
+ return 1;
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory\n");
+ return 1;
+ }
+
+ if (notmuch_query_count_messages (query) != 1) {
+ fprintf (stderr, "Error: search term did not match precisely one message.\n");
+ return 1;
+ }
+
+ messages = notmuch_query_search_messages (query);
+ message = notmuch_messages_get (messages);
+
+ if (message == NULL) {
+ fprintf (stderr, "Error: cannot find matching message.\n");
+ return 1;
+ }
+
+ show_one_part (notmuch_message_get_filename (message), part);
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return 0;
+}
diff --git a/notmuch-tag.c b/notmuch-tag.c
new file mode 100644
index 0000000..fd54bc7
--- /dev/null
+++ b/notmuch-tag.c
@@ -0,0 +1,137 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+static volatile sig_atomic_t interrupted;
+
+static void
+handle_sigint (unused (int sig))
+{
+ ssize_t ignored;
+
+ static char msg[] = "Stopping... \n";
+ ignored = write(2, msg, sizeof(msg)-1);
+ interrupted = 1;
+}
+
+int
+notmuch_tag_command (void *ctx, unused (int argc), unused (char *argv[]))
+{
+ int *add_tags, *remove_tags;
+ int add_tags_count = 0;
+ int remove_tags_count = 0;
+ char *query_string;
+ notmuch_config_t *config;
+ notmuch_database_t *notmuch;
+ notmuch_query_t *query;
+ notmuch_messages_t *messages;
+ notmuch_message_t *message;
+ struct sigaction action;
+ int i;
+
+ /* Setup our handler for SIGINT */
+ memset (&action, 0, sizeof (struct sigaction));
+ action.sa_handler = handle_sigint;
+ sigemptyset (&action.sa_mask);
+ action.sa_flags = SA_RESTART;
+ sigaction (SIGINT, &action, NULL);
+
+ add_tags = talloc_size (ctx, argc * sizeof (int));
+ if (add_tags == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ remove_tags = talloc_size (ctx, argc * sizeof (int));
+ if (remove_tags == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ for (i = 0; i < argc; i++) {
+ if (strcmp (argv[i], "--") == 0) {
+ i++;
+ break;
+ }
+ if (argv[i][0] == '+') {
+ add_tags[add_tags_count++] = i;
+ } else if (argv[i][0] == '-') {
+ remove_tags[remove_tags_count++] = i;
+ } else {
+ break;
+ }
+ }
+
+ if (add_tags_count == 0 && remove_tags_count == 0) {
+ fprintf (stderr, "Error: 'notmuch tag' requires at least one tag to add or remove.\n");
+ return 1;
+ }
+
+ query_string = query_string_from_args (ctx, argc - i, &argv[i]);
+
+ if (*query_string == '\0') {
+ fprintf (stderr, "Error: notmuch tag requires at least one search term.\n");
+ return 1;
+ }
+
+ config = notmuch_config_open (ctx, NULL, NULL);
+ if (config == NULL)
+ return 1;
+
+ notmuch = notmuch_database_open (notmuch_config_get_database_path (config),
+ NOTMUCH_DATABASE_MODE_READ_WRITE);
+ if (notmuch == NULL)
+ return 1;
+
+ query = notmuch_query_create (notmuch, query_string);
+ if (query == NULL) {
+ fprintf (stderr, "Out of memory.\n");
+ return 1;
+ }
+
+ /* tagging is not interested in any special sort order */
+ notmuch_query_set_sort (query, NOTMUCH_SORT_UNSORTED);
+
+ for (messages = notmuch_query_search_messages (query);
+ notmuch_messages_valid (messages) && !interrupted;
+ notmuch_messages_move_to_next (messages))
+ {
+ message = notmuch_messages_get (messages);
+
+ notmuch_message_freeze (message);
+
+ for (i = 0; i < remove_tags_count; i++)
+ notmuch_message_remove_tag (message,
+ argv[remove_tags[i]] + 1);
+
+ for (i = 0; i < add_tags_count; i++)
+ notmuch_message_add_tag (message, argv[add_tags[i]] + 1);
+
+ notmuch_message_thaw (message);
+
+ notmuch_message_destroy (message);
+ }
+
+ notmuch_query_destroy (query);
+ notmuch_database_close (notmuch);
+
+ return interrupted;
+}
diff --git a/notmuch-time.c b/notmuch-time.c
new file mode 100644
index 0000000..e250c3d
--- /dev/null
+++ b/notmuch-time.c
@@ -0,0 +1,137 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+/* Format a nice representation of 'time' relative to the current time.
+ *
+ * Examples include:
+ *
+ * 5 mins. ago (For times less than 60 minutes ago)
+ * Today 12:30 (For times >60 minutes but still today)
+ * Yest. 12:30
+ * Mon. 12:30 (Before yesterday but fewer than 7 days ago)
+ * October 12 (Between 7 and 180 days ago (about 6 months))
+ * 2008-06-30 (More than 180 days ago)
+ *
+ * The returned string is either static data (a string literal) or
+ * newly talloced data belonging to 'ctx'. That is, the caller should
+ * not modify nor free the returned value. But when the caller
+ * arranges for 'ctx' to be talloc_freed, then memory allocated here
+ * (if any) will be reclaimed.
+ *
+ */
+#define MINUTE (60)
+#define HOUR (60 * MINUTE)
+#define DAY (24 * HOUR)
+#define RELATIVE_DATE_MAX 20
+const char *
+notmuch_time_relative_date (const void *ctx, time_t then)
+{
+ struct tm tm_now, tm_then;
+ time_t now = time(NULL);
+ time_t delta;
+ char *result;
+
+ localtime_r (&now, &tm_now);
+ localtime_r (&then, &tm_then);
+
+ result = talloc_zero_size (ctx, RELATIVE_DATE_MAX);
+ if (result == NULL)
+ return "when?";
+
+ if (then > now)
+ return "the future";
+
+ delta = now - then;
+
+ if (delta > 180 * DAY) {
+ strftime (result, RELATIVE_DATE_MAX,
+ "%F", &tm_then); /* 2008-06-30 */
+ return result;
+ }
+
+ if (delta < 3600) {
+ snprintf (result, RELATIVE_DATE_MAX,
+ "%d mins. ago", (int) (delta / 60));
+ return result;
+ }
+
+ if (delta <= 7 * DAY) {
+ if (tm_then.tm_wday == tm_now.tm_wday &&
+ delta < DAY)
+ {
+ strftime (result, RELATIVE_DATE_MAX,
+ "Today %R", &tm_then); /* Today 12:30 */
+ return result;
+ } else if ((tm_now.tm_wday + 7 - tm_then.tm_wday) % 7 == 1) {
+ strftime (result, RELATIVE_DATE_MAX,
+ "Yest. %R", &tm_then); /* Yest. 12:30 */
+ return result;
+ } else {
+ if (tm_then.tm_wday != tm_now.tm_wday) {
+ strftime (result, RELATIVE_DATE_MAX,
+ "%a. %R", &tm_then); /* Mon. 12:30 */
+ return result;
+ }
+ }
+ }
+
+ strftime (result, RELATIVE_DATE_MAX,
+ "%B %d", &tm_then); /* October 12 */
+ return result;
+}
+#undef MINUTE
+#undef HOUR
+#undef DAY
+
+void
+notmuch_time_print_formatted_seconds (double seconds)
+{
+ int hours;
+ int minutes;
+
+ if (seconds < 1) {
+ printf ("almost no time");
+ return;
+ }
+
+ if (seconds > 3600) {
+ hours = (int) seconds / 3600;
+ printf ("%dh ", hours);
+ seconds -= hours * 3600;
+ }
+
+ if (seconds > 60) {
+ minutes = (int) seconds / 60;
+ printf ("%dm ", minutes);
+ seconds -= minutes * 60;
+ }
+
+ printf ("%ds", (int) seconds);
+}
+
+/* Compute the number of seconds elapsed from start to end. */
+double
+notmuch_time_elapsed (struct timeval start, struct timeval end)
+{
+ return ((end.tv_sec - start.tv_sec) +
+ (end.tv_usec - start.tv_usec) / 1e6);
+}
diff --git a/notmuch.1 b/notmuch.1
new file mode 100644
index 0000000..86830f4
--- /dev/null
+++ b/notmuch.1
@@ -0,0 +1,511 @@
+.\" notmuch - Not much of an email program, (just index, search and tagging)
+.\"
+.\" Copyright © 2009 Carl Worth
+.\"
+.\" 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 this program. If not, see http://www.gnu.org/licenses/ .
+.\"
+.\" Author: Carl Worth <cworth@cworth.org>
+.TH NOTMUCH 1 2009-10-31 "Notmuch 0.1"
+.SH NAME
+notmuch \- thread-based email index, search, and tagging
+.SH SYNOPSIS
+.B notmuch
+.IR command " [" args " ...]"
+.SH DESCRIPTION
+Notmuch is a command-line based program for indexing, searching,
+reading, and tagging large collections of email messages.
+
+The quickest way to get started with Notmuch is to simply invoke the
+.B notmuch
+command with no arguments, which will interactively guide you through
+the process of indexing your mail.
+.SH NOTE
+While the command-line program
+.B notmuch
+provides powerful functionality, it does not provide the most
+convenient interface for that functionality. More sophisticated
+interfaces are expected to be built on top of either the command-line
+interface, or more likely, on top of the notmuch library
+interface. See http://notmuchmail.org for more about alternate
+interfaces to notmuch.
+.SH COMMANDS
+The
+.BR setup
+command is used to configure Notmuch for first use, (or to reconfigure
+it later).
+.RS 4
+.TP 4
+.B setup
+
+Interactively sets up notmuch for first use.
+
+The setup command will prompt for your full name, your primary email
+address, any alternate email addresses you use, and the directory
+containing your email archives. Your answers will be written to a
+configuration file in ${NOTMUCH_CONFIG} (if set) or
+${HOME}/.notmuch-config . This configuration file will be created with
+descriptive comments, making it easy to edit by hand later to change the
+configuration. Or you can run
+.B "notmuch setup"
+again to change the configuration.
+
+The mail directory you specify can contain any number of
+sub-directories and should primarily contain only files with individual
+email messages (eg. maildir or mh archives are perfect). If there are
+other, non-email files (such as indexes maintained by other email
+programs) then notmuch will do its best to detect those and ignore
+them.
+
+Mail storage that uses mbox format, (where one mbox file contains many
+messages), will not work with notmuch. If that's how your mail is
+currently stored, it is recommended you first convert it to maildir
+format with a utility such as mb2md before running
+.B "notmuch setup" .
+
+Invoking
+.B notmuch
+with no command argument will run
+.B setup
+if the setup command has not previously been completed.
+.RE
+
+The
+.B new
+command is used to incorporate new mail into the notmuch database.
+.RS 4
+.TP 4
+.B new
+
+Find and import any new messages to the database.
+
+The
+.B new
+command scans all sub-directories of the database, performing
+full-text indexing on new messages that are found. Each new message
+will automatically be tagged with both the
+.BR inbox " and " unread
+tags.
+
+You should run
+.B "notmuch new"
+once after first running
+.B "notmuch setup"
+to create the initial database. The first run may take a long time if
+you have a significant amount of mail (several hundred thousand
+messages or more). Subsequently, you should run
+.B "notmuch new"
+whenever new mail is delivered and you wish to incorporate it into the
+database. These subsequent runs will be much quicker than the initial
+run.
+
+Invoking
+.B notmuch
+with no command argument will run
+.B new
+if
+.B "notmuch setup"
+has previously been completed, but
+.B "notmuch new"
+has not previously been run.
+.RE
+
+Several of the notmuch commands accept search terms with a common
+syntax. See the
+.B "SEARCH SYNTAX"
+section below for more details on the supported syntax.
+
+The
+.BR search ", " show " and " count
+commands are used to query the email database.
+.RS 4
+.TP 4
+.BR search " [options...] <search-term>..."
+
+Search for messages matching the given search terms, and display as
+results the threads containing the matched messages.
+
+The output consists of one line per thread, giving a thread ID, the
+date of the newest (or oldest, depending on the sort option) matched
+message in the thread, the number of matched messages and total
+messages in the thread, the names of all participants in the thread,
+and the subject of the newest (or oldest) message.
+
+Supported options for
+.B search
+include
+.RS 4
+.TP 4
+.BR \-\-format= ( json | text )
+
+Presents the results in either JSON or plain-text (default).
+.RE
+.RS 4
+.TP 4
+.BR \-\-sort= ( newest\-first | oldest\-first )
+
+This option can be used to present results in either chronological order
+.RB ( oldest\-first )
+or reverse chronological order
+.RB ( newest\-first ).
+
+Note: The thread order will be distinct between these two options
+(beyond being simply reversed). When sorting by
+.B oldest\-first
+the threads will be sorted by the oldest message in each thread, but
+when sorting by
+.B newest\-first
+the threads will be sorted by the newest message in each thread.
+
+.RE
+.RS 4
+By default, results will be displayed in reverse chronological order,
+(that is, the newest results will be displayed first).
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+.RE
+.TP
+.BR show " [options...] <search-term>..."
+
+Shows all messages matching the search terms.
+
+The messages will be grouped and sorted based on the threading (all
+replies to a particular message will appear immediately after that
+message in date order). The output is not indented by default, but
+depth tags are printed so that proper indentation can be performed by
+a post-processor (such as the emacs interface to notmuch).
+
+Supported options for
+.B show
+include
+.RS 4
+.TP 4
+.B \-\-entire\-thread
+
+By default only those messages that match the search terms will be
+displayed. With this option, all messages in the same thread as any
+matched message will be displayed.
+.RE
+
+.RS 4
+.TP 4
+.B \-\-format=(json|text)
+
+.RS 4
+.TP 4
+.B text
+
+The default plain-text format has text-content MIME parts
+decoded. Various components in the output,
+.RB ( message ", " header ", " body ", " attachment ", and MIME " part ),
+will be delimited by easily-parsed markers. Each marker consists of a
+Control-L character (ASCII decimal 12), the name of the marker, and
+then either an opening or closing brace, ('{' or '}'), to either open
+or close the component.
+
+.RE
+.RS 4
+.TP 4
+.B json
+
+Format output as Javascript Object Notation (JSON). JSON output always
+includes all messages in a matching thread; in effect
+.B \-\-format=json
+implies
+.B \-\-entire\-thread
+
+.RE
+A common use of
+.B notmuch show
+is to display a single thread of email messages. For this, use a
+search term of "thread:<thread-id>" as can be seen in the first
+column of output from the
+.B notmuch search
+command.
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+.RE
+.TP
+.BR count " <search-term>..."
+
+Count messages matching the search terms.
+
+The number of matching messages is output to stdout.
+
+With no search terms, a count of all messages in the database will be
+displayed.
+.RE
+.RE
+
+The
+.B reply
+command is useful for preparing a template for an email reply.
+.RS 4
+.TP 4
+.BR reply " [options...] <search-term>..."
+
+Constructs a reply template for a set of messages.
+
+To make replying to email easier,
+.B notmuch reply
+takes an existing set of messages and constructs a suitable mail
+template. The Reply-to header (if any, otherwise From:) is used for
+the To: address. Vales from the To: and Cc: headers are copied, but
+not including any of the current user's email addresses (as configured
+in primary_mail or other_email in the .notmuch\-config file) in the
+recipient list
+
+It also builds a suitable new subject, including Re: at the front (if
+not already present), and adding the message IDs of the messages being
+replied to to the References list and setting the In\-Reply\-To: field
+correctly.
+
+Finally, the original contents of the emails are quoted by prefixing
+each line with '> ' and included in the body.
+
+The resulting message template is output to stdout.
+
+Supported options for
+.B reply
+include
+.RS
+.TP 4
+.BR \-\-format= ( default | headers\-only )
+.RS
+.TP 4
+.BR default
+Includes subject and quoted message body.
+.TP
+.BR headers\-only
+Only produces In\-Reply\-To, References, To, Cc, and Bcc headers.
+.RE
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+
+Note: It is most common to use
+.B "notmuch reply"
+with a search string matching a single message, (such as
+id:<message-id>), but it can be useful to reply to several messages at
+once. For example, when a series of patches are sent in a single
+thread, replying to the entire thread allows for the reply to comment
+on issue found in multiple patches.
+.RE
+.RE
+
+The
+.B tag
+command is the only command available for manipulating database
+contents.
+
+.RS 4
+.TP 4
+.BR tag " +<tag>|\-<tag> [...] [\-\-] <search-term>..."
+
+Add/remove tags for all messages matching the search terms.
+
+Tags prefixed by '+' are added while those prefixed by '\-' are
+removed. For each message, tag removal is performed before tag
+addition.
+
+The beginning of <search-terms> is recognized by the first
+argument that begins with neither '+' nor '\-'. Support for
+an initial search term beginning with '+' or '\-' is provided
+by allowing the user to specify a "\-\-" argument to separate
+the tags from the search terms.
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+.RE
+
+The
+.BR dump " and " restore
+commands can be used to create a textual dump of email tags for backup
+purposes, and to restore from that dump
+
+.RS 4
+.TP 4
+.BR dump " [<filename>]"
+
+Creates a plain-text dump of the tags of each message.
+
+The output is to the given filename, if any, or to stdout.
+
+These tags are the only data in the notmuch database that can't be
+recreated from the messages themselves. The output of notmuch dump is
+therefore the only critical thing to backup (and much more friendly to
+incremental backup than the native database files.)
+.TP
+.BR restore " <filename>"
+
+Restores the tags from the given file (see
+.BR "notmuch dump" "."
+
+Note: The dump file format is specifically chosen to be
+compatible with the format of files produced by sup-dump.
+So if you've previously been using sup for mail, then the
+.B "notmuch restore"
+command provides you a way to import all of your tags (or labels as
+sup calls them).
+.RE
+
+The
+.B part
+command can used to output a single part of a multi-part MIME message.
+
+.RS 4
+.TP 4
+.BR part " \-\-part=<part-number> <search-term>..."
+
+Output a single MIME part of a message.
+
+A single decoded MIME part, with no encoding or framing, is output to
+stdout. The search terms must match only a single message, otherwise
+this command will fail.
+
+The part number should match the part "id" field output by the
+"\-\-format=json" option of "notmuch show". If the message specified by
+the search terms does not include a part with the specified "id" there
+will be no output.
+
+See the
+.B "SEARCH SYNTAX"
+section below for details of the supported syntax for <search-terms>.
+.RE
+
+.SH SEARCH SYNTAX
+Several notmuch commands accept a common syntax for search terms.
+
+The search terms can consist of free-form text (and quoted phrases)
+which will match all messages that contain all of the given
+terms/phrases in the body, the subject, or any of the sender or
+recipient headers.
+
+As a special case, a search string consisting of exactly a single
+asterisk ("*") will match all messages.
+
+In addition to free text, the following prefixes can be used to force
+terms to match against specific portions of an email, (where
+<brackets> indicate user-supplied values):
+
+ from:<name-or-address>
+
+ to:<name-or-address>
+
+ subject:<word-or-quoted-phrase>
+
+ attachment:<word>
+
+ tag:<tag> (or is:<tag>)
+
+ id:<message-id>
+
+ thread:<thread-id>
+
+The
+.B from:
+prefix is used to match the name or address of the sender of an email
+message.
+
+The
+.B to:
+prefix is used to match the names or addresses of any recipient of an
+email message, (whether To, Cc, or Bcc).
+
+Any term prefixed with
+.B subject:
+will match only text from the subject of an email. Searching for a
+phrase in the subject is supported by including quotation marks around
+the phrase, immediately following
+.BR subject: .
+
+The
+.B attachment:
+prefix can be used to search for specific filenames (or extensions) of
+attachments to email messages.
+
+For
+.BR tag: " and " is:
+valid tag values include
+.BR inbox " and " unread
+by default for new messages added by
+.B notmuch new
+as well as any other tag values added manually with
+.BR "notmuch tag" .
+
+For
+.BR id: ,
+message ID values are the literal contents of the Message\-ID: header
+of email messages, but without the '<', '>' delimiters.
+
+The
+.B thread:
+prefix can be used with the thread ID values that are generated
+internally by notmuch (and do not appear in email messages). These
+thread ID values can be seen in the first column of output from
+.B "notmuch search"
+
+In addition to individual terms, multiple terms can be
+combined with Boolean operators (
+.BR and ", " or ", " not
+, etc.). Each term in the query will be implicitly connected by a
+logical AND if no explicit operator is provided, (except that terms
+with a common prefix will be implicitly combined with OR until we get
+Xapian defect #402 fixed).
+
+Parentheses can also be used to control the combination of the Boolean
+operators, but will have to be protected from interpretation by the
+shell, (such as by putting quotation marks around any parenthesized
+expression).
+
+Finally, results can be restricted to only messages within a
+particular time range, (based on the Date: header) with a syntax of:
+
+ <intial-timestamp>..<final-timestamp>
+
+Each timestamp is a number representing the number of seconds since
+1970\-01\-01 00:00:00 UTC. This is not the most convenient means of
+expressing date ranges, but until notmuch is fixed to accept a more
+convenient form, one can use the date program to construct
+timestamps. For example, with the bash shell the folowing syntax would
+specify a date range to return messages from 2009\-10\-01 until the
+current time:
+
+ $(date +%s \-d 2009\-10\-01)..$(date +%s)
+.SH ENVIRONMENT
+The following environment variables can be used to control the
+behavior of notmuch.
+.TP
+.B NOTMUCH_CONFIG
+Specifies the location of the notmuch configuration file. Notmuch will
+use ${HOME}/.notmuch\-config if this variable is not set.
+.SH SEE ALSO
+The emacs-based interface to notmuch (available as
+.B notmuch.el
+in the Notmuch distribution).
+
+The notmuch website:
+.B http://notmuchmail.org
+.SH CONTACT
+Feel free to send questions, comments, or kudos to the notmuch mailing
+list <notmuch@notmuchmail.org> . Subscription is not required before
+posting, but is available from the notmuchmail.org website.
+
+Real-time interaction with the Notmuch community is available via IRC
+(server: irc.freenode.net, channel: #notmuch).
diff --git a/notmuch.c b/notmuch.c
new file mode 100644
index 0000000..0eea5e1
--- /dev/null
+++ b/notmuch.c
@@ -0,0 +1,494 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ * Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+
+typedef int (*command_function_t) (void *ctx, int argc, char *argv[]);
+
+typedef struct command {
+ const char *name;
+ command_function_t function;
+ const char *arguments;
+ const char *summary;
+ const char *documentation;
+} command_t;
+
+static int
+notmuch_help_command (void *ctx, int argc, char *argv[]);
+
+static const char search_terms_help[] =
+ "\tSeveral notmuch commands accept a comman syntax for search\n"
+ "\tterms.\n"
+ "\n"
+ "\tThe search terms can consist of free-form text (and quoted\n"
+ "\tphrases) which will match all messages that contain all of\n"
+ "\tthe given terms/phrases in the body, the subject, or any of\n"
+ "\tthe sender or recipient headers.\n"
+ "\n"
+ "\tAs a special case, a search string consisting of exactly a\n"
+ "\tsingle asterisk (\"*\") will match all messages.\n"
+ "\n"
+ "\tIn addition to free text, the following prefixes can be used\n"
+ "\tto force terms to match against specific portions of an email,\n"
+ "\t(where <brackets> indicate user-supplied values):\n"
+ "\n"
+ "\t\tfrom:<name-or-address>\n"
+ "\t\tto:<name-or-address>\n"
+ "\t\tsubject:<word-or-quoted-phrase>\n"
+ "\t\tattachment:<word>\n"
+ "\t\ttag:<tag> (or is:<tag>)\n"
+ "\t\tid:<message-id>\n"
+ "\t\tthread:<thread-id>\n"
+ "\n"
+ "\tThe from: prefix is used to match the name or address of\n"
+ "\tthe sender of an email message.\n"
+ "\n"
+ "\tThe to: prefix is used to match the names or addresses of\n"
+ "\tany recipient of an email message, (whether To, Cc, or Bcc).\n"
+ "\n"
+ "\tAny term prefixed with subject: will match only text from\n"
+ "\tthe subject of an email. Quoted phrases are supported when\n"
+ "\tsearching with: subject:\"this is a phrase\".\n"
+ "\n"
+ "\tFor tag: and is:, valid tag values include \"inbox\" and \"unread\"\n"
+ "\tby default for new messages added by \"notmuch new\" as well\n"
+ "\tas any other tag values added manually with \"notmuch tag\".\n"
+ "\n"
+ "\tFor id:, message ID values are the literal contents of the\n"
+ "\tMessage-ID: header of email messages, but without the '<','>'\n"
+ "\tdelimiters.\n"
+ "\n"
+ "\tThe thread: prefix can be used with the thread ID values that\n"
+ "\tare generated internally by notmuch (and do not appear in email\n"
+ "\tmessages). These thread ID values can be seen in the first\n"
+ "\tcolumn of output from \"notmuch search\".\n"
+ "\n"
+ "\tIn addition to individual terms, multiple terms can be\n"
+ "\tcombined with Boolean operators (\"and\", \"or\", \"not\", etc.).\n"
+ "\tEach term in the query will be implicitly connected by a\n"
+ "\tlogical AND if no explicit operator is provided, (except\n"
+ "\tthat terms with a common prefix will be implicitly combined\n"
+ "\twith OR until we get Xapian defect #402 fixed).\n"
+ "\n"
+ "\tParentheses can also be used to control the combination of\n"
+ "\tthe Boolean operators, but will have to be protected from\n"
+ "\tinterpretation by the shell, (such as by putting quotation\n"
+ "\tmarks around any parenthesized expression).\n"
+ "\n"
+ "\tFinally, results can be restricted to only messages within a\n"
+ "\tparticular time range, (based on the Date: header) with:\n"
+ "\n"
+ "\t\t<intial-timestamp>..<final-timestamp>\n"
+ "\n"
+ "\tEach timestamp is a number representing the number of seconds\n"
+ "\tsince 1970-01-01 00:00:00 UTC. This is not the most convenient\n"
+ "\tmeans of expressing date ranges, but until notmuch is fixed to\n"
+ "\taccept a more convenient form, one can use the date program to\n"
+ "\tconstruct timestamps. For example, with the bash shell the\n"
+ "\tfollowing syntax would specify a date range to return messages\n"
+ "\tfrom 2009-10-01 until the current time:\n"
+ "\n"
+ "\t\t$(date +%%s -d 2009-10-01)..$(date +%%s)\n\n";
+
+command_t commands[] = {
+ { "setup", notmuch_setup_command,
+ NULL,
+ "Interactively setup notmuch for first use.",
+ "\tThe setup command will prompt for your full name, your primary\n"
+ "\temail address, any alternate email addresses you use, and the\n"
+ "\tdirectory containing your email archives. Your answers will be\n"
+ "\twritten to a configuration file in ${NOTMUCH_CONFIG} (if set)\n"
+ "\tor ${HOME}/.notmuch-config.\n"
+ "\n"
+ "\tThis configuration file will be created with descriptive\n"
+ "\tcomments, making it easy to edit by hand later to change the\n"
+ "\tconfiguration. Or you can run \"notmuch setup\" again.\n"
+ "\n"
+ "\tInvoking notmuch with no command argument will run setup if\n"
+ "\tthe setup command has not previously been completed." },
+ { "new", notmuch_new_command,
+ "[--verbose]",
+ "Find and import new messages to the notmuch database.",
+ "\tScans all sub-directories of the mail directory, performing\n"
+ "\tfull-text indexing on new messages that are found. Each new\n"
+ "\tmessage will be tagged as both \"inbox\" and \"unread\".\n"
+ "\n"
+ "\tYou should run \"notmuch new\" once after first running\n"
+ "\t\"notmuch setup\" to create the initial database. The first\n"
+ "\trun may take a long time if you have a significant amount of\n"
+ "\tmail (several hundred thousand messages or more).\n"
+ "\n"
+ "\tSubsequently, you should run \"notmuch new\" whenever new mail\n"
+ "\tis delivered and you wish to incorporate it into the database.\n"
+ "\tThese subsequent runs will be much quicker than the initial run.\n"
+ "\n"
+ "\tSupported options for new include:\n"
+ "\n"
+ "\t--verbose\n"
+ "\n"
+ "\t\tVerbose operation. Shows paths of message files as\n"
+ "\t\tthey are being indexed.\n"
+ "\n"
+ "\tInvoking notmuch with no command argument will run new if\n"
+ "\tthe setup command has previously been completed, but new has\n"
+ "\tnot previously been run." },
+ { "search", notmuch_search_command,
+ "[options...] <search-terms> [...]",
+ "Search for messages matching the given search terms.",
+ "\tNote that the individual mail messages will be matched\n"
+ "\tagainst the search terms, but the results will be the\n"
+ "\tthreads (one per line) containing the matched messages.\n"
+ "\n"
+ "\tSupported options for search include:\n"
+ "\n"
+ "\t--format=(json|text)\n"
+ "\n"
+ "\t\tPresents the results in either JSON or\n"
+ "\t\tplain-text (default)\n"
+ "\n"
+ "\t--sort=(newest-first|oldest-first)\n"
+ "\n"
+ "\t\tPresent results in either chronological order\n"
+ "\t\t(oldest-first) or reverse chronological order\n"
+ "\t\t(newest-first), which is the default.\n"
+ "\n"
+ "\tSee \"notmuch help search-terms\" for details of the search\n"
+ "\tterms syntax." },
+ { "show", notmuch_show_command,
+ "<search-terms> [...]",
+ "Show all messages matching the search terms.",
+ "\tThe messages are grouped and sorted based on the threading\n"
+ "\t(all replies to a particular message appear immediately\n"
+ "\tafter that message in date order).\n"
+ "\n"
+ "\tSupported options for show include:\n"
+ "\n"
+ "\t--entire-thread\n"
+ "\n"
+ "\t\tBy default only those messages that match the\n"
+ "\t\tsearch terms will be displayed. With this option,\n"
+ "\t\tall messages in the same thread as any matched\n"
+ "\t\tmessage will be displayed.\n"
+ "\n"
+ "\t--format=(json|text)\n"
+ "\n"
+ "\t\ttext\t(default)\n"
+ "\n"
+ "\t\tThe plain-text has all text-content MIME parts decoded.\n"
+ "\t\tVarious components in the output, ('message', 'header',\n"
+ "\t\t'body', 'attachment', and MIME 'part') are delimited by\n"
+ "\t\teasily-parsed markers. Each marker consists of a Control-L\n"
+ "\t\tcharacter (ASCII decimal 12), the name of the marker, and\n"
+ "\t\tthen either an opening or closing brace, '{' or '}' to\n"
+ "\t\teither open or close the component.\n"
+ "\n"
+ "\t\tjson\n"
+ "\n"
+ "\t\tFormat output as Javascript Object Notation (JSON).\n"
+ "\t\tJSON output always includes all messages in a matching,\n"
+ "\t\tthread i.e. '--output=json' implies '--entire-thread'\n"
+ "\n"
+ "\tA common use of \"notmuch show\" is to display a single\n"
+ "\tthread of email messages. For this, use a search term of\n"
+ "\t\"thread:<thread-id>\" as can be seen in the first column\n"
+ "\tof output from the \"notmuch search\" command.\n"
+ "\n"
+ "\tSee \"notmuch help search-terms\" for details of the search\n"
+ "\tterms syntax." },
+ { "count", notmuch_count_command,
+ "<search-terms> [...]",
+ "Count messages matching the search terms.",
+ "\tThe number of matching messages is output to stdout.\n"
+ "\n"
+ "\tWith no search terms, a count of all messages in the database\n"
+ "\twill be displayed.\n"
+ "\n"
+ "\tSee \"notmuch help search-terms\" for details of the search\n"
+ "\tterms syntax." },
+ { "reply", notmuch_reply_command,
+ "[options...] <search-terms> [...]",
+ "Construct a reply template for a set of messages.",
+ "\tConstructs a new message as a reply to a set of existing\n"
+ "\tmessages. The Reply-To: header (if any, otherwise From:) is\n"
+ "\tused for the To: address. The To: and Cc: headers are copied,\n"
+ "\tbut not including any of the user's configured addresses.\n"
+ "\n"
+ "\tA suitable subject is constructed. The In-Reply-to: and\n"
+ "\tReferences: headers are set appropriately, and the content\n"
+ "\tof the original messages is quoted and included in the body\n"
+ "\t(unless --format=headers-only is given).\n"
+ "\n"
+ "\tThe resulting message template is output to stdout.\n"
+ "\n"
+ "\tSupported options for reply include:\n"
+ "\n"
+ "\t--format=(default|headers-only)\n"
+ "\n"
+ "\t\tdefault:\n"
+ "\t\t\tIncludes subject and quoted message body.\n"
+ "\n"
+ "\t\theaders-only:\n"
+ "\t\t\tOnly produces In-Reply-To, References, To\n"
+ "\t\t\tCc, and Bcc headers.\n"
+ "\n"
+ "\tSee \"notmuch help search-terms\" for details of the search\n"
+ "\tterms syntax." },
+ { "tag", notmuch_tag_command,
+ "+<tag>|-<tag> [...] [--] <search-terms> [...]",
+ "Add/remove tags for all messages matching the search terms.",
+ "\tThe search terms are handled exactly as in 'search' so one\n"
+ "\tcan use that command first to see what will be modified.\n"
+ "\n"
+ "\tTags prefixed by '+' are added while those prefixed by\n"
+ "\t'-' are removed. For each message, tag removal is performed\n"
+ "\tbefore tag addition.\n"
+ "\n"
+ "\tThe beginning of <search-terms> is recognized by the first\n"
+ "\targument that begins with neither '+' nor '-'. Support for\n"
+ "\tan initial search term beginning with '+' or '-' is provided\n"
+ "\tby allowing the user to specify a \"--\" argument to separate\n"
+ "\tthe tags from the search terms.\n"
+ "\n"
+ "\tSee \"notmuch help search-terms\" for details of the search\n"
+ "\tterms syntax." },
+ { "dump", notmuch_dump_command,
+ "[<filename>]",
+ "Create a plain-text dump of the tags for each message.",
+ "\tOutput is to the given filename, if any, or to stdout.\n"
+ "\tThese tags are the only data in the notmuch database\n"
+ "\tthat can't be recreated from the messages themselves.\n"
+ "\tThe output of notmuch dump is therefore the only\n"
+ "\tcritical thing to backup (and much more friendly to\n"
+ "\tincremental backup than the native database files.)" },
+ { "restore", notmuch_restore_command,
+ "<filename>",
+ "Restore the tags from the given dump file (see 'dump').",
+ "\tNote: The dump file format is specifically chosen to be\n"
+ "\tcompatible with the format of files produced by sup-dump.\n"
+ "\tSo if you've previously been using sup for mail, then the\n"
+ "\t\"notmuch restore\" command provides you a way to import\n"
+ "\tall of your tags (or labels as sup calls them)." },
+ { "search-tags", notmuch_search_tags_command,
+ "[<search-terms> [...] ]",
+ "List all tags found in the database or matching messages.",
+ "\tRun this command without any search-term(s) to obtain a list\n"
+ "\tof all tags found in the database. If you provide one or more\n"
+ "\tsearch-terms as argument(s) then the resulting list will\n"
+ "\tcontain tags only from messages that match the search-term(s).\n"
+ "\n"
+ "\tIn both cases the list will be alphabetically sorted." },
+ { "part", notmuch_part_command,
+ "--part=<num> <search-terms>",
+ "Output a single MIME part of a message.",
+ "\tA single decoded MIME part, with no encoding or framing,\n"
+ "\tis output to stdout. The search terms must match only a single\n"
+ "\tmessage, otherwise this command will fail.\n"
+ "\n"
+ "\tThe part number should match the part \"id\" field output\n"
+ "\tby the \"--format=json\" option of \"notmuch show\". If the\n"
+ "\tmessage specified by the search terms does not include a\n"
+ "\tpart with the specified \"id\" there will be no output." },
+ { "help", notmuch_help_command,
+ "[<command>]",
+ "This message, or more detailed help for the named command.",
+ "\tExcept in this case, where there's not much more detailed\n"
+ "\thelp available." }
+};
+
+static void
+usage (FILE *out)
+{
+ command_t *command;
+ unsigned int i;
+
+ fprintf (out,
+ "Usage: notmuch --help\n"
+ " notmuch --version\n"
+ " notmuch <command> [args...]\n");
+ fprintf (out, "\n");
+ fprintf (out, "The available commands are as follows:\n");
+ fprintf (out, "\n");
+
+ for (i = 0; i < ARRAY_SIZE (commands); i++) {
+ command = &commands[i];
+
+ fprintf (out, " %-11s %s\n",
+ command->name, command->summary);
+ }
+
+ fprintf (out, "\n");
+ fprintf (out,
+ "Use \"notmuch help <command>\" for more details on each command\n"
+ "and \"notmuch help search-terms\" for the common search-terms syntax.\n\n");
+}
+
+static int
+notmuch_help_command (unused (void *ctx), int argc, char *argv[])
+{
+ command_t *command;
+ unsigned int i;
+
+ if (argc == 0) {
+ printf ("The notmuch mail system.\n\n");
+ usage (stdout);
+ return 0;
+ }
+
+ for (i = 0; i < ARRAY_SIZE (commands); i++) {
+ command = &commands[i];
+
+ if (strcmp (argv[0], command->name) == 0) {
+ printf ("Help for \"notmuch %s\":\n\n", argv[0]);
+ if (command->arguments)
+ printf ("%s %s\n\n\t%s\n\n%s\n\n",
+ command->name, command->arguments,
+ command->summary, command->documentation);
+ else
+ printf ("%s\t%s\n\n%s\n\n", command->name,
+ command->summary, command->documentation);
+ return 0;
+ }
+ }
+
+ if (strcmp (argv[0], "search-terms") == 0) {
+ printf ("Help for <%s>\n\n", argv[0]);
+ for (i = 0; i < ARRAY_SIZE (commands); i++) {
+ command = &commands[i];
+
+ if (command->arguments &&
+ strstr (command->arguments, "search-terms"))
+ {
+ printf ("\t%s\t%s\n",
+ command->name, command->arguments);
+ }
+ }
+ printf ("\n");
+ printf (search_terms_help);
+ return 0;
+ }
+
+ fprintf (stderr,
+ "\nSorry, %s is not a known command. There's not much I can do to help.\n\n",
+ argv[0]);
+ return 1;
+}
+
+/* Handle the case of "notmuch" being invoked with no command
+ * argument. For now we just call notmuch_setup_command, but we plan
+ * to be more clever about this in the future.
+ */
+static int
+notmuch (void *ctx)
+{
+ notmuch_config_t *config;
+ notmuch_bool_t is_new;
+ char *db_path;
+ struct stat st;
+
+ config = notmuch_config_open (ctx, NULL, &is_new);
+
+ /* If the user has never configured notmuch, then run
+ * notmuch_setup_command which will give a nice welcome message,
+ * and interactively guide the user through the configuration. */
+ if (is_new) {
+ notmuch_config_close (config);
+ return notmuch_setup_command (ctx, 0, NULL);
+ }
+
+ /* Notmuch is already configured, but is there a database? */
+ db_path = talloc_asprintf (ctx, "%s/%s",
+ notmuch_config_get_database_path (config),
+ ".notmuch");
+ if (stat (db_path, &st)) {
+ notmuch_config_close (config);
+ if (errno != ENOENT) {
+ fprintf (stderr, "Error looking for notmuch database at %s: %s\n",
+ db_path, strerror (errno));
+ return 1;
+ }
+ printf ("Notmuch is configured, but there's not yet a database at\n\n\t%s\n\n",
+ db_path);
+ printf ("You probably want to run \"notmuch new\" now to create that database.\n\n"
+ "Note that the first run of \"notmuch new\" can take a very long time\n"
+ "and that the resulting database will use roughly the same amount of\n"
+ "storage space as the email being indexed.\n\n");
+ return 0;
+ }
+
+ printf ("Notmuch is configured and appears to have a database. Excellent!\n\n"
+ "At this point you can start exploring the functionality of notmuch by\n"
+ "using commands such as:\n\n"
+ "\tnotmuch search tag:inbox\n\n"
+ "\tnotmuch search to:\"%s\"\n\n"
+ "\tnotmuch search from:\"%s\"\n\n"
+ "\tnotmuch search subject:\"my favorite things\"\n\n"
+ "See \"notmuch help search\" for more details.\n\n"
+ "You can also use \"notmuch show\" with any of the thread IDs resulting\n"
+ "from a search. Finally, you may want to explore using a more sophisticated\n"
+ "interface to notmuch such as the emacs interface implemented in notmuch.el\n"
+ "or any other interface described at http://notmuchmail.org\n\n"
+ "And don't forget to run \"notmuch new\" whenever new mail arrives.\n\n"
+ "Have fun, and may your inbox never have much mail.\n\n",
+ notmuch_config_get_user_name (config),
+ notmuch_config_get_user_primary_email (config));
+
+ notmuch_config_close (config);
+
+ return 0;
+}
+
+int
+main (int argc, char *argv[])
+{
+ void *local;
+ command_t *command;
+ unsigned int i;
+
+ local = talloc_new (NULL);
+
+ g_mime_init (0);
+
+ if (argc == 1)
+ return notmuch (local);
+
+ if (STRNCMP_LITERAL (argv[1], "--help") == 0)
+ return notmuch_help_command (NULL, 0, NULL);
+
+ if (STRNCMP_LITERAL (argv[1], "--version") == 0) {
+ printf ("notmuch " STRINGIFY(NOTMUCH_VERSION) "\n");
+ return 0;
+ }
+
+ for (i = 0; i < ARRAY_SIZE (commands); i++) {
+ command = &commands[i];
+
+ if (strcmp (argv[1], command->name) == 0)
+ return (command->function) (local, argc - 2, &argv[2]);
+ }
+
+ fprintf (stderr, "Error: Unknown command '%s' (see \"notmuch help\")\n",
+ argv[1]);
+
+ talloc_free (local);
+
+ return 1;
+}
diff --git a/notmuch.desktop b/notmuch.desktop
new file mode 100644
index 0000000..f160047
--- /dev/null
+++ b/notmuch.desktop
@@ -0,0 +1,7 @@
+[Desktop Entry]
+Name=Notmuch (emacs interface)
+Exec=emacs -f notmuch
+Icon=emblem-mail
+Terminal=false
+Type=Application
+Categories=Network;Email;
diff --git a/packaging/debian b/packaging/debian
new file mode 100644
index 0000000..a586660
--- /dev/null
+++ b/packaging/debian
@@ -0,0 +1,2 @@
+The debian packaging exists in the top-level "debian" directory within
+this source-code respository.
diff --git a/packaging/fedora/notmuch.spec b/packaging/fedora/notmuch.spec
new file mode 100644
index 0000000..cca3a1b
--- /dev/null
+++ b/packaging/fedora/notmuch.spec
@@ -0,0 +1,110 @@
+%global git 306635c2
+
+%if %($(pkg-config emacs) ; echo $?)
+%global emacs_version 23.1
+%global emacs_lispdir %{_datadir}/emacs/site-lisp
+%global emacs_startdir %{_datadir}/emacs/site-lisp/site-start.d
+%else
+%global emacs_version %(pkg-config emacs --modversion)
+%global emacs_lispdir %(pkg-config emacs --variable sitepkglispdir)
+%global emacs_startdir %(pkg-config emacs --variable sitestartdir)
+%endif
+
+Name: notmuch
+Version: 0.0
+Release: 0.3.%{git}%{?dist}
+Summary: Not much of an email program
+
+Group: Applications/Internet
+License: GPLv3+
+URL: http://notmuchmail.org/
+
+#
+# To create a tarball:
+#
+# git clone git://notmuchmail.org/git/notmuch
+# cd notmuch
+# git archive --format=tar --prefix=notmuch/ HEAD | bzip2 > notmuch-`git show-ref --hash=8 HEAD`.tar.bz2
+#
+Source0: notmuch-%{git}.tar.bz2
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+BuildRequires: xapian-core-devel
+BuildRequires: gmime-devel
+BuildRequires: libtalloc-devel
+BuildRequires: zlib-devel
+BuildRequires: emacs-el
+BuildRequires: emacs-nox
+
+Requires: emacs(bin) >= %{emacs_version}
+
+%description
+* "Not much mail" is what Notmuch thinks about your email
+ collection. Even if you receive 12000 messages per month or have on
+ the order of millions of messages that you've been saving for
+ decades. Regardless, Notmuch will be able to quickly search all of
+ it. It's just plain not much mail.
+
+* "Not much mail" is also what you should have in your inbox at any
+ time. Notmuch gives you what you need, (tags and fast search), so
+ that you can keep your inbox tamed and focus on what really matters
+ in your life, (which is surely not email).
+
+* Notmuch is an answer to Sup. Sup is a very good email program
+ written by William Morgan (and others) and is the direct inspiration
+ for Notmuch. Notmuch began as an effort to rewrite
+ performance-critical pieces of Sup in C rather than ruby. From
+ there, it grew into a separate project. One significant contribution
+ Notmuch makes compared to Sup is the separation of the
+ indexer/searcher from the user interface. (Notmuch provides a
+ library interface so that its indexing/searching/tagging features
+ can be integrated into any email program.)
+
+* Notmuch is not much of an email program. It doesn't receive messages
+ (no POP or IMAP suport). It doesn't send messages (no mail composer,
+ no network code at all). And for what it does do (email search) that
+ work is provided by an external library, Xapian. So if Notmuch
+ provides no user interface and Xapian does all the heavy lifting,
+ then what's left here? Not much.
+
+Notmuch is still in the early stages of development, but it does
+include one user interface, (implemented within emacs), which has at
+least two users using it for reading all of their incoming mail. If
+you've been looking for a fast, global-search and tag-based email
+reader to use within emacs, then Notmuch may be exactly what you've
+been looking for.
+
+Otherwise, if you're a developer of an existing email program and
+would love a good library interface for fast, global search with
+support for arbitrary tags, then Notmuch also may be exactly what
+you've been looking for.
+
+%prep
+%setup -q -n notmuch
+
+%build
+make %{?_smp_mflags} CFLAGS="%{optflags}"
+emacs -batch -f batch-byte-compile notmuch.el
+
+%install
+rm -rf %{buildroot}
+make install DESTDIR=%{buildroot} prefix=%{_prefix}
+mkdir -p %{buildroot}%{emacs_startdir}
+install -m0644 -p notmuch.el* %{buildroot}%{emacs_startdir}
+
+%clean
+rm -rf %{buildroot}
+
+%files
+%defattr(-,root,root,-)
+%doc AUTHORS COPYING COPYING-GPL-3 INSTALL README TODO
+
+%{_sysconfdir}/bash_completion.d/notmuch
+%{_bindir}/notmuch
+%{_mandir}/man1/notmuch.1*
+%{emacs_startdir}/notmuch.el*
+
+%changelog
+* Wed Nov 18 2009 Jeffrey C. Ollie <jeff@ocjtech.us> - 0.0-0.3.306635c2
+- First version
+
diff --git a/query-string.c b/query-string.c
new file mode 100644
index 0000000..6536512
--- /dev/null
+++ b/query-string.c
@@ -0,0 +1,56 @@
+/* notmuch - Not much of an email program, (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>
+ */
+
+#include "notmuch-client.h"
+
+/* Construct a single query string from the passed arguments, using
+ * 'ctx' as the talloc owner for all allocations.
+ *
+ * Currently, the arguments are just connected with space characters,
+ * but we might do more processing in the future, (such as inserting
+ * any AND operators needed to work around Xapian QueryParser bugs).
+ *
+ * This function returns NULL in case of insufficient memory.
+ */
+char *
+query_string_from_args (void *ctx, int argc, char *argv[])
+{
+ char *query_string;
+ int i;
+
+ query_string = talloc_strdup (ctx, "");
+ if (query_string == NULL)
+ return NULL;
+
+ for (i = 0; i < argc; i++) {
+ if (i != 0) {
+ query_string = talloc_strdup_append (query_string, " ");
+ if (query_string == NULL)
+ return NULL;
+ }
+
+ query_string = talloc_strdup_append (query_string, argv[i]);
+ if (query_string == NULL)
+ return NULL;
+ }
+
+ return query_string;
+}
+
diff --git a/show-message.c b/show-message.c
new file mode 100644
index 0000000..b1b61be
--- /dev/null
+++ b/show-message.c
@@ -0,0 +1,196 @@
+/* notmuch - Not much of an email program, (just index and search)
+ *
+ * Copyright © 2009 Carl Worth
+ * Copyright © 2009 Keith Packard
+ *
+ * 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: Carl Worth <cworth@cworth.org>
+ * Keith Packard <keithp@keithp.com>
+ */
+
+#include "notmuch-client.h"
+
+static void
+show_message_part (GMimeObject *part, int *part_count,
+ void (*show_part) (GMimeObject *part, int *part_count))
+{
+ if (GMIME_IS_MULTIPART (part)) {
+ GMimeMultipart *multipart = GMIME_MULTIPART (part);
+ int i;
+
+ for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
+ show_message_part (g_mime_multipart_get_part (multipart, i),
+ part_count, show_part);
+ }
+ return;
+ }
+
+ if (GMIME_IS_MESSAGE_PART (part)) {
+ GMimeMessage *mime_message;
+
+ mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+
+ show_message_part (g_mime_message_get_mime_part (mime_message),
+ part_count, show_part);
+
+ return;
+ }
+
+ if (! (GMIME_IS_PART (part))) {
+ fprintf (stderr, "Warning: Not displaying unknown mime part: %s.\n",
+ g_type_name (G_OBJECT_TYPE (part)));
+ return;
+ }
+
+ *part_count = *part_count + 1;
+
+ (*show_part) (part, part_count);
+}
+
+notmuch_status_t
+show_message_body (const char *filename,
+ void (*show_part) (GMimeObject *part, int *part_count))
+{
+ GMimeStream *stream = NULL;
+ GMimeParser *parser = NULL;
+ GMimeMessage *mime_message = NULL;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ FILE *file = NULL;
+ int part_count = 0;
+
+ file = fopen (filename, "r");
+ if (! file) {
+ fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ stream = g_mime_stream_file_new (file);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
+
+ parser = g_mime_parser_new_with_stream (stream);
+
+ mime_message = g_mime_parser_construct_message (parser);
+
+ show_message_part (g_mime_message_get_mime_part (mime_message),
+ &part_count, show_part);
+
+ DONE:
+ if (mime_message)
+ g_object_unref (mime_message);
+
+ if (parser)
+ g_object_unref (parser);
+
+ if (stream)
+ g_object_unref (stream);
+
+ if (file)
+ fclose (file);
+
+ return ret;
+}
+
+static void
+show_one_part_output (GMimeObject *part)
+{
+ GMimeStream *stream_filter = NULL;
+ GMimeDataWrapper *wrapper;
+ GMimeStream *stream_stdout = g_mime_stream_file_new (stdout);
+
+ stream_filter = g_mime_stream_filter_new(stream_stdout);
+ wrapper = g_mime_part_get_content_object (GMIME_PART (part));
+ if (wrapper && stream_filter)
+ g_mime_data_wrapper_write_to_stream (wrapper, stream_filter);
+ if (stream_filter)
+ g_object_unref(stream_filter);
+}
+
+static void
+show_one_part_worker (GMimeObject *part, int *part_count, int desired_part)
+{
+ if (GMIME_IS_MULTIPART (part)) {
+ GMimeMultipart *multipart = GMIME_MULTIPART (part);
+ int i;
+
+ for (i = 0; i < g_mime_multipart_get_count (multipart); i++) {
+ show_one_part_worker (g_mime_multipart_get_part (multipart, i),
+ part_count, desired_part);
+ }
+ return;
+ }
+
+ if (GMIME_IS_MESSAGE_PART (part)) {
+ GMimeMessage *mime_message;
+
+ mime_message = g_mime_message_part_get_message (GMIME_MESSAGE_PART (part));
+
+ show_one_part_worker (g_mime_message_get_mime_part (mime_message),
+ part_count, desired_part);
+
+ return;
+ }
+
+ if (! (GMIME_IS_PART (part)))
+ return;
+
+ *part_count = *part_count + 1;
+
+ if (*part_count == desired_part)
+ show_one_part_output (part);
+}
+
+notmuch_status_t
+show_one_part (const char *filename, int part)
+{
+ GMimeStream *stream = NULL;
+ GMimeParser *parser = NULL;
+ GMimeMessage *mime_message = NULL;
+ notmuch_status_t ret = NOTMUCH_STATUS_SUCCESS;
+ FILE *file = NULL;
+ int part_count = 0;
+
+ file = fopen (filename, "r");
+ if (! file) {
+ fprintf (stderr, "Error opening %s: %s\n", filename, strerror (errno));
+ ret = NOTMUCH_STATUS_FILE_ERROR;
+ goto DONE;
+ }
+
+ stream = g_mime_stream_file_new (file);
+ g_mime_stream_file_set_owner (GMIME_STREAM_FILE (stream), FALSE);
+
+ parser = g_mime_parser_new_with_stream (stream);
+
+ mime_message = g_mime_parser_construct_message (parser);
+
+ show_one_part_worker (g_mime_message_get_mime_part (mime_message),
+ &part_count, part);
+
+ DONE:
+ if (mime_message)
+ g_object_unref (mime_message);
+
+ if (parser)
+ g_object_unref (parser);
+
+ if (stream)
+ g_object_unref (stream);
+
+ if (file)
+ fclose (file);
+
+ return ret;
+}
diff --git a/test/notmuch-test b/test/notmuch-test
new file mode 100755
index 0000000..f0c0aad
--- /dev/null
+++ b/test/notmuch-test
@@ -0,0 +1,707 @@
+#!/bin/bash
+set -e
+
+find_notmuch_binary ()
+{
+ dir=$1
+
+ while [ -n "$dir" ]; do
+ bin=$dir/notmuch
+ if [ -x $bin ]; then
+ echo $bin
+ return
+ fi
+ dir=$(dirname $dir)
+ if [ "$dir" = "/" ]; then
+ break
+ fi
+ done
+
+ echo notmuch
+}
+
+increment_mtime_amount=0
+increment_mtime ()
+{
+ dir=$1
+
+ increment_mtime_amount=$((increment_mtime_amount + 1))
+ touch -d "+${increment_mtime_amount} seconds" $dir
+}
+
+# Generate a new message in the mail directory, with a unique message
+# ID and subject. The message is not added to the index.
+#
+# After this function returns, the filename of the generated message
+# is available as $gen_msg_filename and the message ID is available as
+# $gen_msg_id .
+#
+# This function supports named parameters with the bash syntax for
+# assigning a value to an associative array ([name]=value). The
+# supported parameters are:
+#
+# [dir]=directory/of/choice
+#
+# Generate the message in directory 'directory/of/choice' within
+# the mail store. The directory will be created if necessary.
+#
+# [body]=text
+#
+# Text to use as the body of the email message
+#
+# '[from]="Some User <user@example.com>"'
+# '[to]="Some User <user@example.com>"'
+# '[subject]="Subject of email message"'
+# '[date]="RFC 822 Date"'
+#
+# Values for email headers. If not provided, default values will
+# be generated instead.
+#
+# '[cc]="Some User <user@example.com>"'
+# [reply-to]=some-address
+# [in-reply-to]=<message-id>
+#
+# Additional values for email headers. If these are not provided
+# then the relevant headers will simply not appear in the
+# message.
+#
+# '[id]=<message-id>'
+#
+# Controls the message-id of the created message.
+gen_msg_cnt=0
+gen_msg_filename=""
+gen_msg_id=""
+generate_message ()
+{
+ # This is our (bash-specific) magic for doing named parameters
+ local -A template="($@)"
+ local additional_headers
+
+ if [ -z "${template[id]}" ]; then
+ gen_msg_cnt=$((gen_msg_cnt + 1))
+ gen_msg_name=msg-$(printf "%03d" $gen_msg_cnt)
+ gen_msg_id="${gen_msg_name}@notmuch-test-suite"
+ else
+ gen_msg_name="msg-${template[id]}"
+ gen_msg_id="${template[id]}"
+ fi
+
+ if [ -z "${template[dir]}" ]; then
+ gen_msg_filename="${MAIL_DIR}/$gen_msg_name"
+ else
+ gen_msg_filename="${MAIL_DIR}/${template[dir]}/$gen_msg_name"
+ mkdir -p $(dirname $gen_msg_filename)
+ fi
+
+ if [ -z "${template[body]}" ]; then
+ template[body]="This is just test message (#${gen_msg_cnt})"
+ fi
+
+ if [ -z "${template[from]}" ]; then
+ template[from]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+ fi
+
+ if [ -z "${template[to]}" ]; then
+ template[to]="Notmuch Test Suite <test_suite@notmuchmail.org>"
+ fi
+
+ if [ -z "${template[subject]}" ]; then
+ template[subject]="Test message #${gen_msg_cnt}"
+ fi
+
+ if [ -z "${template[date]}" ]; then
+ template[date]="Tue, 05 Jan 2001 15:43:57 -0800"
+ fi
+
+ additional_headers=""
+ if [ ! -z "${template[reply-to]}" ]; then
+ additional_headers="Reply-To: ${template[reply-to]}
+${additional_headers}"
+ fi
+
+ if [ ! -z "${template[in-reply-to]}" ]; then
+ additional_headers="In-Reply-To: ${template[in-reply-to]}
+${additional_headers}"
+ fi
+
+ if [ ! -z "${template[cc]}" ]; then
+ additional_headers="Cc: ${template[cc]}
+${additional_headers}"
+ fi
+
+cat <<EOF >$gen_msg_filename
+From: ${template[from]}
+To: ${template[to]}
+Message-Id: <${gen_msg_id}>
+Subject: ${template[subject]}
+Date: ${template[date]}
+${additional_headers}
+${template[body]}
+EOF
+
+ # Ensure that the mtime of the containing directory is updated
+ increment_mtime $(dirname ${gen_msg_filename})
+}
+
+# Generate a new message and add it to the index.
+#
+# All of the arguments and return values supported by generate_message
+# are also supported here, so see that function for details.
+add_message ()
+{
+ generate_message "$@"
+
+ $NOTMUCH new > /dev/null
+}
+
+tests=0
+test_failures=0
+
+pass_if_equal ()
+{
+ output=$1
+ expected=$2
+
+ tests=$((tests + 1))
+
+ if [ "$output" = "$expected" ]; then
+ echo " PASS"
+ else
+ echo " FAIL"
+ echo " Expected output: $expected"
+ echo " Actual output: $output"
+ test_failures=$((test_failures + 1))
+ fi
+}
+
+TEST_DIR=$(pwd)/test.$$
+MAIL_DIR=${TEST_DIR}/mail
+export NOTMUCH_CONFIG=${TEST_DIR}/notmuch-config
+NOTMUCH=$(find_notmuch_binary $(pwd))
+
+NOTMUCH_NEW ()
+{
+ $NOTMUCH new | grep -v -E -e '^Processed [0-9]*( total)? file|Found [0-9]* total file'
+}
+
+NOTMUCH_SEARCH_THREAD_ID_SQUELCH='s/thread:................/thread:XXX/'
+notmuch_search_sanitize ()
+{
+ sed -e "$NOTMUCH_SEARCH_THREAD_ID_SQUELCH"
+}
+
+rm -rf ${TEST_DIR}
+mkdir ${TEST_DIR}
+cd ${TEST_DIR}
+
+mkdir ${MAIL_DIR}
+
+cat <<EOF > ${NOTMUCH_CONFIG}
+[database]
+path=${MAIL_DIR}
+
+[user]
+name=Notmuch Test Suite
+primary_email=test_suite@notmuchmail.org
+other_email=test_suite_other@notmuchmail.org
+EOF
+
+printf "Testing \"notmuch new\" in several variations:\n"
+printf " No new messages...\t\t\t\t"
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail."
+
+printf " Single new message...\t\t\t\t"
+generate_message
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " Multiple new messages...\t\t\t"
+generate_message
+generate_message
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 2 new messages to the database."
+
+printf " No new messages (non-empty DB)...\t\t"
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail."
+
+printf " New directories...\t\t\t\t"
+rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
+mkdir ${MAIL_DIR}/def
+mkdir ${MAIL_DIR}/ghi
+generate_message [dir]=def
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " Alternate inode order...\t\t\t"
+
+rm -rf ${MAIL_DIR}/.notmuch
+mv ${MAIL_DIR}/ghi ${MAIL_DIR}/abc
+rm ${MAIL_DIR}/def/*
+generate_message [dir]=abc
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " Message moved in...\t\t\t\t"
+rm -rf ${MAIL_DIR}/* ${MAIL_DIR}/.notmuch
+generate_message
+tmp_msg_filename=tmp/$gen_msg_filename
+mkdir -p $(dirname $tmp_msg_filename)
+mv $gen_msg_filename $tmp_msg_filename
+increment_mtime ${MAIL_DIR}
+$NOTMUCH new > /dev/null
+mv $tmp_msg_filename $gen_msg_filename
+increment_mtime ${MAIL_DIR}
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " Renamed message...\t\t\t\t"
+
+generate_message
+$NOTMUCH new > /dev/null
+mv $gen_msg_filename ${gen_msg_filename}-renamed
+increment_mtime ${MAIL_DIR}
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Detected 1 file rename."
+
+printf " Deleted message...\t\t\t\t"
+
+rm ${gen_msg_filename}-renamed
+increment_mtime ${MAIL_DIR}
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Removed 1 message."
+
+printf " Renamed directory...\t\t\t\t"
+
+generate_message [dir]=dir
+generate_message [dir]=dir
+generate_message [dir]=dir
+
+$NOTMUCH new > /dev/null
+
+mv ${MAIL_DIR}/dir ${MAIL_DIR}/dir-renamed
+increment_mtime ${MAIL_DIR}
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Detected 3 file renames."
+
+printf " Deleted directory...\t\t\t\t"
+
+rm -rf ${MAIL_DIR}/dir-renamed
+increment_mtime ${MAIL_DIR}
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Removed 3 messages."
+
+printf " New directory (at end of list)...\t\t"
+
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+generate_message [dir]=zzz
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 3 new messages to the database."
+
+printf " Deleted directory (end of list)...\t\t"
+
+rm -rf ${MAIL_DIR}/zzz
+increment_mtime ${MAIL_DIR}
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Removed 3 messages."
+
+printf " New symlink to directory...\t\t\t"
+
+rm -rf ${MAIL_DIR}/.notmuch
+mv ${MAIL_DIR} ${TEST_DIR}/actual_maildir
+
+mkdir ${MAIL_DIR}
+ln -s ${TEST_DIR}/actual_maildir ${MAIL_DIR}/symlink
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " New symlink to a file...\t\t\t"
+generate_message
+external_msg_filename=${TEST_DIR}/external/$(basename $gen_msg_filename)
+mkdir -p $(dirname $external_msg_filename)
+mv $gen_msg_filename $external_msg_filename
+ln -s $external_msg_filename $gen_msg_filename
+increment_mtime ${MAIL_DIR}
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+
+printf " New two-level directory...\t\t\t"
+
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+generate_message [dir]=two/levels
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 3 new messages to the database."
+
+printf " Deleted two-level directory...\t\t\t"
+
+rm -rf ${MAIL_DIR}/two
+increment_mtime ${MAIL_DIR}
+
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "No new mail. Removed 3 messages."
+
+printf "\nTesting \"notmuch search\" in several variations:\n"
+
+printf " Search body...\t\t\t\t\t"
+add_message '[subject]="body search"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [body]=bodysearchtest
+output=$($NOTMUCH search bodysearchtest | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)"
+
+printf " Search by from:...\t\t\t\t"
+add_message '[subject]="search by from"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom
+output=$($NOTMUCH search from:searchbyfrom | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)"
+
+printf " Search by to:...\t\t\t\t"
+add_message '[subject]="search by to"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto
+output=$($NOTMUCH search to:searchbyto | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)"
+
+printf " Search by subject:...\t\t\t\t"
+add_message [subject]=subjectsearchtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$($NOTMUCH search subject:subjectsearchtest | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)"
+
+printf " Search by id:...\t\t\t\t"
+add_message '[subject]="search by id"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$($NOTMUCH search id:${gen_msg_id} | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)"
+
+printf " Search by tag:...\t\t\t\t"
+add_message '[subject]="search by tag"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+$NOTMUCH tag +searchbytag id:${gen_msg_id}
+output=$($NOTMUCH search tag:searchbytag | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)"
+
+printf " Search by thread:...\t\t\t\t"
+add_message '[subject]="search by thread"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+thread_id=$($NOTMUCH search id:${gen_msg_id} | sed -e 's/thread:\([a-f0-9]*\).*/\1/')
+output=$($NOTMUCH search thread:${thread_id} | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)"
+
+printf " Search body (phrase)...\t\t\t"
+add_message '[subject]="body search (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="body search (phrase)"'
+add_message '[subject]="negative result"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[body]="This phrase should not match the body search"'
+output=$($NOTMUCH search '\"body search (phrase)\"' | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)"
+
+printf " Search by from: (address)...\t\t\t"
+add_message '[subject]="search by from (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [from]=searchbyfrom@example.com
+output=$($NOTMUCH search from:searchbyfrom@example.com | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)"
+
+printf " Search by from: (name)...\t\t\t"
+add_message '[subject]="search by from (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[from]="Search By From Name <test@example.com>"'
+output=$($NOTMUCH search from:'Search By From Name' | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)"
+
+printf " Search by to: (address)...\t\t\t"
+add_message '[subject]="search by to (address)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' [to]=searchbyto@example.com
+output=$($NOTMUCH search to:searchbyto@example.com | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)"
+
+printf " Search by to: (name)...\t\t\t"
+add_message '[subject]="search by to (name)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' '[to]="Search By To Name <test@example.com>"'
+output=$($NOTMUCH search to:'Search By To Name' | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)"
+
+printf " Search by subject: (phrase)...\t\t\t"
+add_message '[subject]="subject search test (phrase)"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+add_message '[subject]="this phrase should not match the subject search test"' '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$($NOTMUCH search 'subject:\"subject search test (phrase)\"' | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)"
+
+printf " Search for all messages (\"*\"):...\t\t"
+output=$($NOTMUCH search '*' | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Test message #6 (inbox unread)
+thread:XXX 2001-01-05 [1/1] Notmuch Test Suite; Test message #14 (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (inbox unread)
+thread:XXX 2000-01-01 [1/1] searchbyfrom; search by from (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subjectsearchtest (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by id (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by tag (inbox searchbytag unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by thread (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; body search (phrase) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; negative result (inbox unread)
+thread:XXX 2000-01-01 [1/1] searchbyfrom@example.com; search by from (address) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Search By From Name; search by from (name) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (address) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; search by to (name) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; subject search test (phrase) (inbox unread)
+thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; this phrase should not match the subject search test (inbox unread)"
+
+printf "\nTesting naming of threads with changing subject:\n"
+add_message '[subject]="thread-naming: Initial thread subject"' \
+ '[date]="Fri, 05 Jan 2001 15:43:56 -0800"'
+parent=${gen_msg_id}
+add_message '[subject]="thread-naming: Older changed subject"' \
+ '[date]="Sat, 06 Jan 2001 15:43:56 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+add_message '[subject]="thread-naming: Newer changed subject"' \
+ '[date]="Sun, 07 Jan 2001 15:43:56 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+add_message '[subject]="thread-naming: Final thread subject"' \
+ '[date]="Mon, 08 Jan 2001 15:43:56 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+final=${gen_msg_id}
+
+printf " Initial thread name (oldest-first search)...\t"
+output=$($NOTMUCH search --sort=oldest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-05 [4/4] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+printf " Initial thread name (newest-first search)...\t"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-08 [4/4] Notmuch Test Suite; thread-naming: Final thread subject (inbox unread)"
+
+# Remove oldest and newest messages from search results
+$NOTMUCH tag -inbox id:$parent or id:$final
+
+printf " Changed thread name (oldest-first search)...\t"
+output=$($NOTMUCH search --sort=oldest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-06 [2/4] Notmuch Test Suite; thread-naming: Older changed subject (inbox unread)"
+
+printf " Changed thread name (newest-first search)...\t"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-07 [2/4] Notmuch Test Suite; thread-naming: Newer changed subject (inbox unread)"
+
+printf " Ignore added reply prefix (Re:)...\t\t"
+add_message '[subject]="Re: thread-naming: Initial thread subject"' \
+ '[date]="Tue, 09 Jan 2001 15:43:45 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-09 [3/5] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+printf " Ignore added reply prefix (Aw:)...\t\t"
+add_message '[subject]="Aw: thread-naming: Initial thread subject"' \
+ '[date]="Wed, 10 Jan 2001 15:43:45 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-10 [4/6] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+printf " Ignore added reply prefix (Vs:)...\t\t"
+add_message '[subject]="Vs: thread-naming: Initial thread subject"' \
+ '[date]="Thu, 11 Jan 2001 15:43:45 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-11 [5/7] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+printf " Ignore added reply prefix (Sv:)...\t\t"
+add_message '[subject]="Sv: thread-naming: Initial thread subject"' \
+ '[date]="Fri, 12 Jan 2001 15:43:45 -0800"' \
+ "[in-reply-to]=\<$parent\>"
+output=$($NOTMUCH search --sort=newest-first thread-naming and tag:inbox | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2001-01-12 [6/8] Notmuch Test Suite; thread-naming: Initial thread subject (inbox unread)"
+
+printf "\nTesting \"notmuch reply\" in several variations:\n"
+
+printf " Basic reply...\t\t\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite@notmuchmail.org \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="basic reply test"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> basic reply test"
+
+printf " Multiple recipients...\t\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ '[to]="test_suite@notmuchmail.org, Someone Else <someone@example.com>"' \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="Multiple recipients"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, Someone Else <someone@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> Multiple recipients"
+
+printf " Reply with CC...\t\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite@notmuchmail.org \
+ '[cc]="Other Parties <cc@example.com>"' \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="reply with CC"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Cc: Other Parties <cc@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> reply with CC"
+
+printf " Reply from alternate address...\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite_other@notmuchmail.org \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="reply from alternate address"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite_other@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> reply from alternate address"
+
+printf " Support for Reply-To...\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ [to]=test_suite@notmuchmail.org \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="support for reply-to"' \
+ '[reply-to]="Sender <elsewhere@example.com>"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <elsewhere@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> support for reply-to"
+
+printf " Un-munging Reply-To...\t\t\t\t"
+add_message '[from]="Sender <sender@example.com>"' \
+ '[to]="Some List <list@example.com>"' \
+ [subject]=notmuch-reply-test \
+ '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \
+ '[body]="Un-munging Reply-To"' \
+ '[reply-to]="Evil Munging List <list@example.com>"'
+
+output=$($NOTMUCH reply id:${gen_msg_id})
+pass_if_equal "$output" "From: Notmuch Test Suite <test_suite@notmuchmail.org>
+Subject: Re: notmuch-reply-test
+To: Sender <sender@example.com>, Some List <list@example.com>
+Bcc: test_suite@notmuchmail.org
+In-Reply-To: <${gen_msg_id}>
+References: <${gen_msg_id}>
+
+On Tue, 05 Jan 2010 15:43:56 -0800, Sender <sender@example.com> wrote:
+> Un-munging Reply-To"
+
+printf "\nTesting handling of uuencoded data:\n"
+
+add_message [subject]=uuencodetest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' \
+'[body]="This message is used to ensure that notmuch correctly handles a
+message containing a block of uuencoded data. First, we have a marker
+this content beforeuudata . Then we beging the uunencoded data itself:
+
+begin 644 bogus-uuencoded-data
+M0123456789012345678901234567890123456789012345678901234567890
+MOBVIOUSLY, THIS IS NOT ANY SORT OF USEFUL UUNECODED DATA.
+MINSTEAD THIS IS JUST A WAY TO ENSURE THAT THIS BLOCK OF DATA
+MIS CORRECTLY IGNORED WHEN NOTMUCH CREATES ITS INDEX. SO WE
+MINCLUDE A DURINGUUDATA MARKER THAT SHOULD NOT RESULT IN ANY
+MSEARCH RESULT.
+\`
+end
+
+Finally, we have our afteruudata marker as well."'
+
+printf " Ensure content before uu data is indexed...\t"
+output=$($NOTMUCH search beforeuudata | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+printf " Ensure uu data is not indexed...\t\t"
+output=$($NOTMUCH search DURINGUUDATA | notmuch_search_sanitize)
+pass_if_equal "$output" ""
+printf " Ensure content after uu data is indexed...\t"
+output=$($NOTMUCH search afteruudata | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; uuencodetest (inbox unread)"
+
+printf "\nTesting \"notmuch dump\" and \"notmuch restore\":\n"
+
+printf " Dumping all tags...\t\t\t\t"
+$NOTMUCH dump dump.expected
+pass_if_equal "$?" "0"
+
+printf " Clearing all tags...\t\t\t\t"
+sed -e 's/(\([^(]*\))$/()/' < dump.expected > clear.expected
+$NOTMUCH restore clear.expected
+$NOTMUCH dump clear.actual
+pass_if_equal "$(< clear.actual)" "$(< clear.expected)"
+
+printf " Restoring original tags...\t\t\t"
+$NOTMUCH restore dump.expected
+$NOTMUCH dump dump.actual
+pass_if_equal "$(< dump.actual)" "$(< dump.expected)"
+
+printf " Restore with nothing to do...\t\t\t"
+$NOTMUCH restore dump.expected
+pass_if_equal "$?" "0"
+
+printf "\nTesting threading when messages received out of order:\n"
+printf " Adding initial child message...\t\t"
+generate_message [body]=foo '[in-reply-to]=\<parent-id\>' [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+printf " Searching returns the message...\t\t"
+output=$($NOTMUCH search foo | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [1/1] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+printf " Adding second child message...\t\t\t"
+generate_message [body]=foo '[in-reply-to]=\<parent-id\>' [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+printf " Searching returns both messages in one thread..."
+output=$($NOTMUCH search foo | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [2/2] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+printf " Adding parent message...\t\t\t"
+generate_message [body]=foo [id]=parent-id [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"'
+output=$(NOTMUCH_NEW)
+pass_if_equal "$output" "Added 1 new message to the database."
+printf " Searching returns all three messages in one thread..."
+output=$($NOTMUCH search foo | notmuch_search_sanitize)
+pass_if_equal "$output" "thread:XXX 2000-01-01 [3/3] Notmuch Test Suite; brokenthreadtest (inbox unread)"
+
+echo ""
+echo "Notmuch test suite complete."
+
+if [ "$test_failures" = "0" ]; then
+ echo "All $tests tests passed."
+ rm -rf ${TEST_DIR}
+else
+ echo "$test_failures/$tests tests failed. The failures can be investigated in:"
+ echo "${TEST_DIR}"
+fi
+
+echo ""
+
+exit $test_failures
diff --git a/version b/version
new file mode 100644
index 0000000..3b04cfb
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+0.2
diff --git a/vim/Makefile b/vim/Makefile
new file mode 100644
index 0000000..89e18be
--- /dev/null
+++ b/vim/Makefile
@@ -0,0 +1,24 @@
+.PHONY: all help install link symlink
+
+FILES = plugin/notmuch.vim \
+ $(wildcard syntax/notmuch-*.vim)
+
+PREFIX = $(shell ls -d ~/.vim/)
+
+OUT_FILES = $(FILES:%=${PREFIX}/%)
+
+all: help
+
+help:
+ @echo "I don't actually build anything, but I will help you install"
+ @echo "notmuch support for vim."
+ @echo
+ @echo " make install - copy plugin scripts and syntax files to ~/.vim"
+ @echo " make symlink - create symlinks in ~/.vim (useful for development)"
+
+install: ${OUT_FILES}
+link symlink:
+ ${MAKE} SYMLINK=1 install
+
+${OUT_FILES}: ${PREFIX}/%: %
+ $(if ${SYMLINK},ln -fs,cp) `pwd`/$< $@
diff --git a/vim/README b/vim/README
new file mode 100644
index 0000000..8cd3b1a
--- /dev/null
+++ b/vim/README
@@ -0,0 +1,90 @@
+This directory contains a vim script that allows reading notmuch mail
+through vim.
+
+NOTE: this is a work in progress. Patches welcome. <bart@jukie.net>
+
+Dependencies:
+ notmuch:
+ Naturally, it expects you have notmuch installed and configured.
+
+ mail:
+ To send mail, notmuch.vim uses the UNIX mail command.
+
+ git-diff:
+ The vim interface makes use of the git-diff.vim syntax file
+ which is available from
+ http://github.com/motemen/git-vim/blob/master/syntax/git-diff.vim
+
+
+To install:
+ make install
+
+
+To run:
+ vim -c ':NotMuch'
+
+ from vim:
+ :NotMuch
+ :NotMuch new to:bart@jukie.net 'subject:this is a test'
+
+
+Buffer types:
+ [notmuch-folders]
+ Folder list, or technically a list of saved searches.
+
+ Keybindings:
+ <Enter> - show the selected search
+ m - compose a new message
+ s - enter search criteria
+ = - refresh display
+
+ [notmuch-search]
+ You are presented with the search results when you run :NotMuch.
+
+ Keybindings:
+ <Space> - show the selected thread colapsing unmatched items
+ <Enter> - show the entire selected thread
+ a - archive message (remove inbox tag)
+ f - filter the current search terms
+ o - toggle search screen order
+ m - compose a new message
+ r - reply to thread
+ s - enter search criteria
+ ,s - alter search criteria
+ t - filter the current search terms with tags
+ q - return to folder display, or undo filter
+ + - add tag(s) to selected message
+ - - remove tag(s) from selected message
+ = - refresh display
+ ? - reveal the thread ID of what's under cursor
+ ^] - search using word under cursor
+
+ [notmuch-show]
+ This is the display of the message.
+
+ Keybindings:
+ <Space> - mark read, archive, go to next matching message
+ ^n - next message
+ ^p - previous message
+ b - toggle folding of message bodies
+ c - toggle folding of citations
+ h - toggle folding of extra header lines
+ i - toggle folding of signatures
+ m - compose a new message
+ r - reply to the message
+ s - enter search criteria
+ q - return to search display
+ ? - reveal the message and thread IDs of what's under cursor
+ ^] - search using word under cursor
+
+ [notmuch-compose]
+ When you're writing an email, you're in this mode.
+
+ Insert-mode keybindings:
+ <Tab> - go to the next header line
+
+ Normal-mode keybindings:
+ <Tab> - go to the next header line
+ ,s - send this message
+ ,q - abort this message
+
diff --git a/vim/notmuch.yaml b/vim/notmuch.yaml
new file mode 100644
index 0000000..3d8422c
--- /dev/null
+++ b/vim/notmuch.yaml
@@ -0,0 +1,8 @@
+addon: notmuch
+description: "notmuch mail user interface"
+files:
+ - plugin/notmuch.vim
+ - syntax/notmuch-compose.vim
+ - syntax/notmuch-folders.vim
+ - syntax/notmuch-search.vim
+ - syntax/notmuch-show.vim
diff --git a/vim/plugin/notmuch.vim b/vim/plugin/notmuch.vim
new file mode 100644
index 0000000..a9754f2
--- /dev/null
+++ b/vim/plugin/notmuch.vim
@@ -0,0 +1,1438 @@
+" notmuch.vim plugin --- run notmuch within vim
+"
+" 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: Bart Trojanowski <bart@jukie.net>
+
+" --- configuration defaults {{{1
+
+let s:notmuch_defaults = {
+ \ 'g:notmuch_cmd': 'notmuch' ,
+ \ 'g:notmuch_debug': 0 ,
+ \
+ \ 'g:notmuch_search_newest_first': 1 ,
+ \ 'g:notmuch_search_from_column_width': 20 ,
+ \
+ \ 'g:notmuch_show_fold_signatures': 1 ,
+ \ 'g:notmuch_show_fold_citations': 1 ,
+ \ 'g:notmuch_show_fold_bodies': 0 ,
+ \ 'g:notmuch_show_fold_headers': 1 ,
+ \
+ \ 'g:notmuch_show_message_begin_regexp': ' message{' ,
+ \ 'g:notmuch_show_message_end_regexp': ' message}' ,
+ \ 'g:notmuch_show_header_begin_regexp': ' header{' ,
+ \ 'g:notmuch_show_header_end_regexp': ' header}' ,
+ \ 'g:notmuch_show_body_begin_regexp': ' body{' ,
+ \ 'g:notmuch_show_body_end_regexp': ' body}' ,
+ \ 'g:notmuch_show_attachment_begin_regexp': ' attachment{' ,
+ \ 'g:notmuch_show_attachment_end_regexp': ' attachment}' ,
+ \ 'g:notmuch_show_part_begin_regexp': ' part{' ,
+ \ 'g:notmuch_show_part_end_regexp': ' part}' ,
+ \ 'g:notmuch_show_marker_regexp': ' \\(message\\|header\\|body\\|attachment\\|part\\)[{}].*$',
+ \
+ \ 'g:notmuch_show_message_parse_regexp': '\(id:[^ ]*\) depth:\([0-9]*\) match:\([0-9]*\) filename:\(.*\)$',
+ \ 'g:notmuch_show_tags_regexp': '(\([^)]*\))$' ,
+ \
+ \ 'g:notmuch_show_signature_regexp': '^\(-- \?\|_\+\)$' ,
+ \ 'g:notmuch_show_signature_lines_max': 12 ,
+ \
+ \ 'g:notmuch_show_citation_regexp': '^\s*>' ,
+ \
+ \ 'g:notmuch_compose_insert_mode_start': 1 ,
+ \ 'g:notmuch_compose_header_help': 1 ,
+ \ 'g:notmuch_compose_temp_file_dir': '~/.notmuch/compose/' ,
+ \ }
+
+" defaults for g:notmuch_initial_search_words
+" override with: let g:notmuch_initial_search_words = [ ... ]
+let s:notmuch_initial_search_words_defaults = [
+ \ 'tag:inbox and tag:unread',
+ \ ]
+
+" defaults for g:notmuch_show_headers
+" override with: let g:notmuch_show_headers = [ ... ]
+let s:notmuch_show_headers_defaults = [
+ \ 'Subject',
+ \ 'To',
+ \ 'Cc',
+ \ 'Bcc',
+ \ 'Date',
+ \ ]
+
+" defaults for g:notmuch_folders
+" override with: let g:notmuch_folders = [ ... ]
+let s:notmuch_folders_defaults = [
+ \ [ 'new', 'tag:inbox and tag:unread' ],
+ \ [ 'inbox', 'tag:inbox' ],
+ \ [ 'unread', 'tag:unread' ],
+ \ ]
+
+" defaults for g:notmuch_signature
+" override with: let g:notmuch_signature = [ ... ]
+let s:notmuch_signature_defaults = [
+ \ '',
+ \ '-- ',
+ \ 'email sent from notmuch.vim plugin'
+ \ ]
+
+" defaults for g:notmuch_compose_headers
+" override with: let g:notmuch_compose_headers = [ ... ]
+let s:notmuch_compose_headers_defaults = [
+ \ 'From',
+ \ 'To',
+ \ 'Cc',
+ \ 'Bcc',
+ \ 'Subject'
+ \ ]
+
+" --- keyboard mapping definitions {{{1
+
+" --- --- bindings for folders mode {{{2
+
+let g:notmuch_folders_maps = {
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ 's': ':call <SID>NM_search_prompt()<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '=': ':call <SID>NM_folders_refresh_view()<CR>',
+ \ '<Enter>': ':call <SID>NM_folders_show_search()<CR>',
+ \ }
+
+" --- --- bindings for search screen {{{2
+let g:notmuch_search_maps = {
+ \ '<Space>': ':call <SID>NM_search_show_thread(0)<CR>',
+ \ '<Enter>': ':call <SID>NM_search_show_thread(1)<CR>',
+ \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
+ \ 'a': ':call <SID>NM_search_archive_thread()<CR>',
+ \ 'f': ':call <SID>NM_search_filter()<CR>',
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ 'o': ':call <SID>NM_search_toggle_order()<CR>',
+ \ 'r': ':call <SID>NM_search_reply_to_thread()<CR>',
+ \ 's': ':call <SID>NM_search_prompt()<CR>',
+ \ ',s': ':call <SID>NM_search_edit()<CR>',
+ \ 't': ':call <SID>NM_search_filter_by_tag()<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '+': ':call <SID>NM_search_add_tags([])<CR>',
+ \ '-': ':call <SID>NM_search_remove_tags([])<CR>',
+ \ '=': ':call <SID>NM_search_refresh_view()<CR>',
+ \ '?': ':echo <SID>NM_search_thread_id() . '' @ '' . join(<SID>NM_get_search_words())<CR>',
+ \ }
+
+" --- --- bindings for show screen {{{2
+let g:notmuch_show_maps = {
+ \ '<C-P>': ':call <SID>NM_show_previous(1, 0)<CR>',
+ \ '<C-N>': ':call <SID>NM_show_next(1, 0)<CR>',
+ \ '<C-]>': ':call <SID>NM_search_expand(''<cword>'')<CR>',
+ \ 'q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ 's': ':call <SID>NM_search_prompt()<CR>',
+ \
+ \ 'b': ':call <SID>NM_show_fold_toggle(''b'', ''bdy'', !g:notmuch_show_fold_bodies)<CR>',
+ \ 'c': ':call <SID>NM_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)<CR>',
+ \ 'h': ':call <SID>NM_show_fold_toggle(''h'', ''hdr'', !g:notmuch_show_fold_headers)<CR>',
+ \ 'i': ':call <SID>NM_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)<CR>',
+ \
+ \ 'a': ':call <SID>NM_show_archive_thread()<CR>',
+ \ 'A': ':call <SID>NM_show_mark_read_then_archive_thread()<CR>',
+ \ 'N': ':call <SID>NM_show_mark_read_then_next_open_message()<CR>',
+ \ 'v': ':call <SID>NM_show_view_all_mime_parts()<CR>',
+ \ '+': ':call <SID>NM_show_add_tag()<CR>',
+ \ '-': ':call <SID>NM_show_remove_tag()<CR>',
+ \ '<Space>': ':call <SID>NM_show_advance_marking_read_and_archiving()<CR>',
+ \ '\|': ':call <SID>NM_show_pipe_message()<CR>',
+ \
+ \ '<S-Tab>': ':call <SID>NM_show_previous_fold()<CR>',
+ \ '<Tab>': ':call <SID>NM_show_next_fold()<CR>',
+ \ '<Enter>': ':call <SID>NM_show_toggle_fold()<CR>',
+ \
+ \ 'r': ':call <SID>NM_show_reply()<CR>',
+ \ 'm': ':call <SID>NM_new_mail()<CR>',
+ \ '?': ':echo <SID>NM_show_message_id() . '' @ '' . join(<SID>NM_get_search_words())<CR>',
+ \ }
+
+" --- --- bindings for compose screen {{{2
+let g:notmuch_compose_nmaps = {
+ \ ',s': ':call <SID>NM_compose_send()<CR>',
+ \ ',a': ':call <SID>NM_compose_attach()<CR>',
+ \ ',q': ':call <SID>NM_kill_this_buffer()<CR>',
+ \ '<Tab>': ':call <SID>NM_compose_next_entry_area()<CR>',
+ \ }
+let g:notmuch_compose_imaps = {
+ \ '<Tab>': '<C-r>=<SID>NM_compose_next_entry_area()<CR>',
+ \ }
+
+" --- implement folders screen {{{1
+
+function! s:NM_cmd_folders(words)
+ if len(a:words)
+ throw 'Not expecting any arguments for folders command.'
+ endif
+ let cmd = ['count']
+ let disp = []
+ let searches = []
+ for entry in g:notmuch_folders
+ let [ name, search ] = entry
+ let data = s:NM_run(cmd + [search])
+ let cnt = matchlist(data, '\(\d\+\)')[1]
+ call add(disp, printf('%9d %-20s (%s)', cnt, name, search))
+ call add(searches, search)
+ endfor
+
+ call <SID>NM_newBuffer('', 'folders', join(disp, "\n"))
+ let b:nm_searches = searches
+ let b:nm_timestamp = reltime()
+
+ call <SID>NM_cmd_folders_mksyntax()
+ call <SID>NM_set_map('n', g:notmuch_folders_maps)
+ setlocal cursorline
+ setlocal nowrap
+endfunction
+
+function! s:NM_cmd_folders_mksyntax()
+endfunction
+
+" --- --- folders screen action functions {{{2
+
+function! s:NM_folders_refresh_view()
+ let lno = line('.')
+ setlocal bufhidden=delete
+ call s:NM_cmd_folders([])
+ exec printf('norm %dG', lno)
+endfunction
+
+function! s:NM_folders_show_search()
+ let line = line('.')
+ let search = b:nm_searches[line-1]
+
+ let prev_bufnr = bufnr('%')
+ setlocal bufhidden=hide
+ call <SID>NM_cmd_search([search])
+ setlocal bufhidden=delete
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+
+" --- implement search screen {{{1
+
+function! s:NM_cmd_search(words)
+ let cmd = ['search']
+ if g:notmuch_search_newest_first
+ let cmd = cmd + ['--sort=newest-first']
+ else
+ let cmd = cmd + ['--sort=oldest-first']
+ endif
+ let data = s:NM_run(cmd + a:words)
+ let lines = split(data, "\n")
+ let disp = copy(lines)
+ call map(disp, 's:NM_cmd_search_fmtline(v:val)')
+
+ call <SID>NM_newBuffer('', 'search', join(disp, "\n"))
+ let b:nm_raw_lines = lines
+ let b:nm_search_words = a:words
+
+ call <SID>NM_cmd_search_mksyntax()
+ call <SID>NM_set_map('n', g:notmuch_search_maps)
+ setlocal cursorline
+ setlocal nowrap
+endfunction
+function! s:NM_cmd_search_fmtline(line)
+ let m = matchlist(a:line, '^\(thread:\S\+\)\s\([^]]\+\]\) \([^;]\+\); \(.*\) (\([^(]*\))$')
+ if !len(m)
+ return 'ERROR PARSING: ' . a:line
+ endif
+ let max = g:notmuch_search_from_column_width
+ let from = m[3]
+ if strlen(from) >= max
+ let from = substitute(m[3][0:max-4], '[^A-Za-z1-9_]*$', '', '') . '...'
+ endif
+ return printf('%-20s %-20s | %s (%s)', m[2], from, m[4], m[5])
+endfunction
+function! s:NM_cmd_search_mksyntax()
+ syntax clear nmSearchFrom
+ exec printf('syntax match nmSearchFrom /\(\] \)\@<=.\{%d\}/ oneline contained', g:notmuch_search_from_column_width)
+endfunction
+
+" --- --- search screen action functions {{{2
+
+function! s:NM_search_show_thread(everything)
+ let words = [ <SID>NM_search_thread_id() ]
+ if !a:everything && exists('b:nm_search_words')
+ call extend(words, ['AND', '('])
+ call extend(words, b:nm_search_words)
+ call add(words, ')')
+ endif
+ call <SID>NM_cmd_show(words)
+ let b:nm_show_everything = a:everything
+endfunction
+
+function! s:NM_search_prompt()
+ " TODO: input() can support completion
+ let text = input('NotMuch Search: ')
+ if strlen(text)
+ let tags = split(text)
+ else
+ let tags = s:notmuch_initial_search_words_defaults
+ endif
+ let prev_bufnr = bufnr('%')
+ if b:nm_type == 'search' && exists('b:nm_prev_bufnr')
+ " TODO: we intend to replace the current buffer,
+ " ... maybe we could just clear it
+ let prev_bufnr = b:nm_prev_bufnr
+ setlocal bufhidden=delete
+ else
+ setlocal bufhidden=hide
+ endif
+ call <SID>NM_cmd_search(tags)
+ setlocal bufhidden=delete
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+function! s:NM_search_edit()
+ " TODO: input() can support completion
+ let text = input('NotMuch Search: ', join(b:nm_search_words, ' '))
+ if strlen(text)
+ call <SID>NM_cmd_search(split(text))
+ endif
+endfunction
+
+function! s:NM_search_archive_thread()
+ call <SID>NM_add_remove_tags_on_screen('', '-', ['inbox'])
+ call <SID>NM_add_remove_tags([], '-', ['inbox'])
+ norm j
+endfunction
+
+function! s:NM_search_filter()
+ call <SID>NM_search_filter_helper('Filter: ', '', '')
+endfunction
+
+function! s:NM_search_filter_by_tag()
+ call <SID>NM_search_filter_helper('Filter Tag(s): ', 'tag:', 'and')
+endfunction
+
+function! s:NM_search_filter_helper(prompt, prefix, joiner)
+ " TODO: input() can support completion
+ let text = substitute(input(a:prompt), '\v(^\s*|\s*$|\n)', '', 'g')
+ if !strlen(text)
+ return
+ endif
+
+ let tags = b:nm_search_words + ['AND']
+ \ + <SID>NM_combine_tags(a:prefix, split(text), a:joiner, '()')
+
+ let prev_bufnr = bufnr('%')
+ setlocal bufhidden=hide
+ call <SID>NM_cmd_search(tags)
+ setlocal bufhidden=delete
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+function! s:NM_search_toggle_order()
+ let g:notmuch_search_newest_first = !g:notmuch_search_newest_first
+ " FIXME: maybe this would be better done w/o reading re-reading the lines
+ " reversing the b:nm_raw_lines and the buffer lines would be better
+ call <SID>NM_search_refresh_view()
+endfunction
+
+function! s:NM_search_reply_to_thread()
+ let cmd = ['reply']
+ call add(cmd, <SID>NM_search_thread_id())
+ call add(cmd, 'AND')
+ call extend(cmd, <SID>NM_get_search_words())
+
+ let data = <SID>NM_run(cmd)
+ let lines = split(data, "\n")
+ call <SID>NM_newComposeBuffer(lines, 0)
+endfunction
+
+function! s:NM_search_add_tags(tags)
+ call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)
+endfunction
+
+function! s:NM_search_remove_tags(tags)
+ call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)
+endfunction
+
+function! s:NM_search_refresh_view()
+ let lno = line('.')
+ let prev_bufnr = b:nm_prev_bufnr
+ setlocal bufhidden=delete
+ call <SID>NM_cmd_search(b:nm_search_words)
+ let b:nm_prev_bufnr = prev_bufnr
+ " FIXME: should find the line of the thread we were on if possible
+ exec printf('norm %dG', lno)
+endfunction
+
+" --- --- search screen helper functions {{{2
+
+function! s:NM_search_thread_id()
+ if !exists('b:nm_raw_lines')
+ throw 'Eeek! no b:nm_raw_lines'
+ endif
+ let mnum = line('.') - 1
+ if len(b:nm_raw_lines) <= mnum
+ return ''
+ endif
+ let info = b:nm_raw_lines[mnum]
+ let what = split(info, '\s\+')[0]
+ return what
+endfunction
+
+function! s:NM_search_add_remove_tags(prompt, prefix, intags)
+ if type(a:intags) != type([]) || len(a:intags) == 0
+ " TODO: input() can support completion
+ let text = input(a:prompt)
+ if !strlen(text)
+ return
+ endif
+ let tags = split(text, ' ')
+ else
+ let tags = a:intags
+ endif
+ call <SID>NM_add_remove_tags([], a:prefix, tags)
+ call <SID>NM_add_remove_tags_on_screen('', a:prefix, tags)
+endfunction
+
+" --- implement show screen {{{1
+
+function! s:NM_cmd_show(words)
+ let prev_bufnr = bufnr('%')
+ let data = s:NM_run(['show', '--entire-thread'] + a:words)
+ let lines = split(data, "\n")
+
+ let info = s:NM_cmd_show_parse(lines)
+
+ setlocal bufhidden=hide
+ call <SID>NM_newBuffer('', 'show', join(info['disp'], "\n"))
+ setlocal bufhidden=delete
+ let b:nm_search_words = a:words
+ let b:nm_raw_info = info
+ let b:nm_prev_bufnr = prev_bufnr
+
+ call <SID>NM_cmd_show_mkfolds()
+ call <SID>NM_cmd_show_mksyntax()
+ call <SID>NM_set_map('n', g:notmuch_show_maps)
+ setlocal foldtext=NM_cmd_show_foldtext()
+ setlocal fillchars=
+ setlocal foldcolumn=6
+
+endfunction
+
+function! s:NM_show_previous(can_change_thread, find_matching)
+ let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0
+ let info = b:nm_raw_info
+ let lnum = line('.')
+ for msg in reverse(copy(info['msgs']))
+ if a:find_matching && msg['match'] == '0'
+ continue
+ endif
+ if lnum <= msg['start']
+ continue
+ endif
+
+ exec printf('norm %dGzt', msg['start'])
+ " TODO: try to fit the message on screen
+ return
+ endfor
+ if !a:can_change_thread
+ return
+ endif
+ call <SID>NM_kill_this_buffer()
+ if line('.') > 1
+ norm k
+ call <SID>NM_search_show_thread(everything)
+ norm G
+ call <SID>NM_show_previous(0, a:find_matching)
+ else
+ echo 'No more messages.'
+ endif
+endfunction
+
+function! s:NM_show_next(can_change_thread, find_matching)
+ let info = b:nm_raw_info
+ let lnum = line('.')
+ for msg in info['msgs']
+ if a:find_matching && msg['match'] == '0'
+ continue
+ endif
+ if lnum >= msg['start']
+ continue
+ endif
+
+ exec printf('norm %dGzt', msg['start'])
+ " TODO: try to fit the message on screen
+ return
+ endfor
+ if a:can_change_thread
+ call <SID>NM_show_next_thread()
+ endif
+endfunction
+
+function! s:NM_show_next_thread()
+ let everything = exists('b:nm_show_everything') ? b:nm_show_everything : 0
+ call <SID>NM_kill_this_buffer()
+ if line('.') != line('$')
+ norm j
+ call <SID>NM_search_show_thread(everything)
+ else
+ echo 'No more messages.'
+ endif
+endfunction
+
+function! s:NM_show_archive_thread()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_mark_read_then_archive_thread()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_mark_read_then_next_open_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_previous_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_reply()
+ let cmd = ['reply']
+ call add(cmd, <SID>NM_show_message_id())
+ call add(cmd, 'AND')
+ call extend(cmd, <SID>NM_get_search_words())
+
+ let data = <SID>NM_run(cmd)
+ let lines = split(data, "\n")
+ call <SID>NM_newComposeBuffer(lines, 0)
+endfunction
+
+function! s:NM_show_view_all_mime_parts()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_view_raw_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_add_tag()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_remove_tag()
+ echo 'not implemented'
+endfunction
+
+" if entire message is not visible scroll down 1/2 page or less to get to the bottom of message
+" otherwise go to next message
+" any message that is viewed entirely has inbox and unread tags removed
+function! s:NM_show_advance_marking_read_and_archiving()
+ let advance_tags = ['unread', 'inbox']
+
+ let vis_top = line('w0')
+ let vis_bot = line('w$')
+
+ let msg_top = <SID>NM_show_get_message_for_line(vis_top)
+ if !has_key(msg_top,'id')
+ throw "No top visible message."
+ endif
+
+ " if the top message is the last message, just expunge the entire thread and move on
+ if msg_top['end'] == line('$')
+ let ids = []
+ for msg in b:nm_raw_info['msgs']
+ if has_key(msg,'match') && msg['match'] != '0'
+ call add(ids, msg['id'])
+ endif
+ endfor
+ let filter = <SID>NM_combine_tags('tag:', advance_tags, 'OR', '()')
+ \ + ['AND']
+ \ + <SID>NM_combine_tags('', ids, 'OR', '()')
+ call <SID>NM_add_remove_tags(filter, '-', advance_tags)
+ call <SID>NM_show_next(1, 1)
+ return
+ endif
+
+ let msg_bot = <SID>NM_show_get_message_for_line(vis_bot)
+ if !has_key(msg_bot,'id')
+ throw "No bottom visible message."
+ endif
+
+ " if entire message fits on the screen, read/archive it, move to the next one
+ if msg_top['id'] != msg_bot['id'] || msg_top['end'] <= vis_bot
+ call <SID>NM_add_remove_tags_on_screen(msg_top['start'], '-', advance_tags)
+ exec printf('norm %dG', vis_top)
+ call <SID>NM_show_next(0, 1)
+ if has_key(msg_top,'match') && msg_top['match'] != '0'
+ redraw
+ " do this last to hide the latency
+ let filter = <SID>NM_combine_tags('tag:', advance_tags, 'OR', '()')
+ \ + ['AND', msg_top['id']]
+ call <SID>NM_add_remove_tags(filter, '-', advance_tags)
+ endif
+ return
+ endif
+
+ " entire message does not fit on the screen, scroll down to bottom, max 1/2 screen
+ let jmp = winheight(winnr()) / 2
+ let max = msg_bot['end'] - vis_bot
+ if jmp > max
+ let jmp = max
+ endif
+ exec printf('norm %dGzt', vis_top + jmp)
+ return
+endfunction
+
+function! s:NM_show_pipe_message()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_previous_fold()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_next_fold()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_show_toggle_fold()
+ echo 'not implemented'
+endfunction
+
+
+" --- --- show screen helper functions {{{2
+
+function! s:NM_show_get_message_for_line(line)
+ for msg in b:nm_raw_info['msgs']
+ if a:line > msg['end']
+ continue
+ endif
+ return msg
+ endfor
+ return {}
+endfunction
+
+function! s:NM_show_message_id()
+ if !exists('b:nm_raw_info')
+ throw 'Eeek! no b:nm_raw_info'
+ endif
+ let msg = <SID>NM_show_get_message_for_line(line('.'))
+ if has_key(msg,'id')
+ return msg['id']
+ endif
+ return ''
+endfunction
+
+function! s:NM_show_fold_toggle(key, type, fold)
+ let info = b:nm_raw_info
+ let act = 'open'
+ if a:fold
+ let act = 'close'
+ endif
+ for fld in info['folds']
+ if fld[0] != a:type
+ continue
+ endif
+ "let idx = fld[3]
+ "let msg = info['msgs'][idx]
+ "if has_key(msg,'match') && msg['match'] == '0'
+ " continue
+ "endif
+ let cls = foldclosed(fld[1])
+ if cls != -1 && cls != fld[1]
+ continue
+ endif
+ exec printf('%dfold%s', fld[1], act)
+ endfor
+ exec printf('nnoremap <buffer> %s :call <SID>NM_show_fold_toggle(''%s'', ''%s'', %d)<CR>', a:key, a:key, a:type, !a:fold)
+endfunction
+
+
+" s:NM_cmd_show_parse returns the following dictionary:
+" 'disp': lines to display
+" 'msgs': message info dicts { start, end, id, depth, filename, descr, header }
+" 'folds': fold info arrays [ type, start, end ]
+" 'foldtext': fold text indexed by start line
+function! s:NM_cmd_show_parse(inlines)
+ let info = { 'disp': [],
+ \ 'msgs': [],
+ \ 'folds': [],
+ \ 'foldtext': {} }
+ let msg = {}
+ let hdr = {}
+
+ let in_message = 0
+ let in_header = 0
+ let in_body = 0
+ let in_part = ''
+
+ let body_start = -1
+ let part_start = -1
+
+ let mode_type = ''
+ let mode_start = -1
+
+ let inlnum = 0
+ for line in a:inlines
+ let inlnum = inlnum + 1
+ let foldinfo = []
+
+ if strlen(in_part)
+ let part_end = 0
+
+ if match(line, g:notmuch_show_part_end_regexp) != -1
+ let part_end = len(info['disp'])
+ else
+ call add(info['disp'], line)
+ endif
+
+ if in_part == 'text/plain'
+ if !part_end && mode_type == ''
+ if match(line, g:notmuch_show_signature_regexp) != -1
+ let mode_type = 'sig'
+ let mode_start = len(info['disp'])
+ elseif match(line, g:notmuch_show_citation_regexp) != -1
+ let mode_type = 'cit'
+ let mode_start = len(info['disp'])
+ endif
+ elseif mode_type == 'cit'
+ if part_end || match(line, g:notmuch_show_citation_regexp) == -1
+ let outlnum = len(info['disp'])
+ let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']),
+ \ printf('[ %d-line citation. Press "c" to show. ]', outlnum - mode_start) ]
+ let mode_type = ''
+ endif
+ elseif mode_type == 'sig'
+ let outlnum = len(info['disp'])
+ if (outlnum - mode_start) > g:notmuch_show_signature_lines_max
+ let mode_type = ''
+ elseif part_end
+ let foldinfo = [ mode_type, mode_start, outlnum-1, len(info['msgs']),
+ \ printf('[ %d-line signature. Press "s" to show. ]', outlnum - mode_start) ]
+ let mode_type = ''
+ endif
+ endif
+ endif
+
+ if part_end
+ " FIXME: this is a hack for handling two folds being added for one line
+ " we should handle addinga fold in a function
+ if len(foldinfo) && foldinfo[1] < foldinfo[2]
+ call add(info['folds'], foldinfo[0:3])
+ let info['foldtext'][foldinfo[1]] = foldinfo[4]
+ endif
+
+ let foldinfo = [ 'text', part_start, part_end, len(info['msgs']),
+ \ printf('[ %d-line %s. Press "p" to show. ]', part_end - part_start, in_part) ]
+ let in_part = ''
+ call add(info['disp'], '')
+ endif
+
+ elseif in_body
+ if !has_key(msg,'body_start')
+ let msg['body_start'] = len(info['disp']) + 1
+ endif
+ if match(line, g:notmuch_show_body_end_regexp) != -1
+ let body_end = len(info['disp'])
+ let foldinfo = [ 'bdy', body_start, body_end, len(info['msgs']),
+ \ printf('[ BODY %d - %d lines ]', len(info['msgs']), body_end - body_start) ]
+
+ let in_body = 0
+
+ elseif match(line, g:notmuch_show_part_begin_regexp) != -1
+ let m = matchlist(line, 'ID: \(\d\+\), Content-type: \(\S\+\)')
+ let in_part = 'unknown'
+ if len(m)
+ let in_part = m[2]
+ endif
+ call add(info['disp'],
+ \ printf('--- %s ---', in_part))
+ let part_start = len(info['disp']) + 1
+ endif
+
+ elseif in_header
+ if in_header == 1
+ let msg['descr'] = line
+ call add(info['disp'], line)
+ let in_header = 2
+ let msg['hdr_start'] = len(info['disp']) + 1
+
+ else
+ if match(line, g:notmuch_show_header_end_regexp) != -1
+ let hdr_start = msg['hdr_start']+1
+ let hdr_end = len(info['disp'])
+ let foldinfo = [ 'hdr', hdr_start, hdr_end, len(info['msgs']),
+ \ printf('[ %d-line headers. Press "h" to show. ]', hdr_end + 1 - hdr_start) ]
+ let msg['header'] = hdr
+ let in_header = 0
+ let hdr = {}
+ else
+ let m = matchlist(line, '^\(\w\+\):\s*\(.*\)$')
+ if len(m)
+ let hdr[m[1]] = m[2]
+ if match(g:notmuch_show_headers, m[1]) != -1
+ call add(info['disp'], line)
+ endif
+ endif
+ endif
+ endif
+
+ elseif in_message
+ if match(line, g:notmuch_show_message_end_regexp) != -1
+ let msg['end'] = len(info['disp'])
+ call add(info['disp'], '')
+
+ let foldinfo = [ 'msg', msg['start'], msg['end'], len(info['msgs']),
+ \ printf('[ MSG %d - %s ]', len(info['msgs']), msg['descr']) ]
+
+ call add(info['msgs'], msg)
+ let msg = {}
+ let in_message = 0
+ let in_header = 0
+ let in_body = 0
+ let in_part = ''
+
+ elseif match(line, g:notmuch_show_header_begin_regexp) != -1
+ let in_header = 1
+ continue
+
+ elseif match(line, g:notmuch_show_body_begin_regexp) != -1
+ let body_start = len(info['disp']) + 1
+ let in_body = 1
+ continue
+ endif
+
+ else
+ if match(line, g:notmuch_show_message_begin_regexp) != -1
+ let msg['start'] = len(info['disp']) + 1
+
+ let m = matchlist(line, g:notmuch_show_message_parse_regexp)
+ if len(m)
+ let msg['id'] = m[1]
+ let msg['depth'] = m[2]
+ let msg['match'] = m[3]
+ let msg['filename'] = m[4]
+ endif
+
+ let in_message = 1
+ endif
+ endif
+
+ if len(foldinfo) && foldinfo[1] < foldinfo[2]
+ call add(info['folds'], foldinfo[0:3])
+ let info['foldtext'][foldinfo[1]] = foldinfo[4]
+ endif
+ endfor
+ return info
+endfunction
+
+function! s:NM_cmd_show_mkfolds()
+ let info = b:nm_raw_info
+
+ for afold in info['folds']
+ exec printf('%d,%dfold', afold[1], afold[2])
+ let state = 'open'
+ if (afold[0] == 'sig' && g:notmuch_show_fold_signatures)
+ \ || (afold[0] == 'cit' && g:notmuch_show_fold_citations)
+ \ || (afold[0] == 'bdy' && g:notmuch_show_fold_bodies)
+ \ || (afold[0] == 'hdr' && g:notmuch_show_fold_headers)
+ let state = 'close'
+ elseif afold[0] == 'msg'
+ let idx = afold[3]
+ let msg = info['msgs'][idx]
+ if has_key(msg,'match') && msg['match'] == '0'
+ let state = 'close'
+ endif
+ endif
+ exec printf('%dfold%s', afold[1], state)
+ endfor
+endfunction
+
+function! s:NM_cmd_show_mksyntax()
+ let info = b:nm_raw_info
+ let cnt = 0
+ for msg in info['msgs']
+ let cnt = cnt + 1
+ let start = msg['start']
+ let hdr_start = msg['hdr_start']
+ let body_start = msg['body_start']
+ let end = msg['end']
+ exec printf('syntax region nmShowMsg%dDesc start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgDesc', cnt, start, start+1)
+ exec printf('syntax region nmShowMsg%dHead start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgHead', cnt, hdr_start, body_start)
+ exec printf('syntax region nmShowMsg%dBody start=''\%%%dl'' end=''\%%%dl'' contains=@nmShowMsgBody', cnt, body_start, end)
+ endfor
+endfunction
+
+function! NM_cmd_show_foldtext()
+ let foldtext = b:nm_raw_info['foldtext']
+ return foldtext[v:foldstart]
+endfunction
+
+
+" --- implement compose screen {{{1
+
+function! s:NM_cmd_compose(words, body_lines)
+ let lines = []
+ let start_on_line = 0
+
+ let hdrs = { }
+ for word in a:words
+ let m = matchlist(word, '^\(\w[^:]*\):\s*\(.*\)\s*$')
+ if !len(m)
+ throw 'Eeek! bad parameter ''' . string(word) . ''''
+ endif
+ let key = substitute(m[1], '\<\w', '\U&', 'g')
+ if !has_key(hdrs, key)
+ let hdrs[key] = []
+ endif
+ if strlen(m[2])
+ call add(hdrs[key], m[2])
+ endif
+ endfor
+
+ if !has_key(hdrs, 'From') || !len(hdrs['From'])
+ let me = <SID>NM_compose_get_user_email()
+ let hdrs['From'] = [ me ]
+ endif
+
+ for key in g:notmuch_compose_headers
+ let text = has_key(hdrs, key) ? join(hdrs[key], ', ') : ''
+ call add(lines, key . ': ' . text)
+ if !start_on_line && !strlen(text)
+ let start_on_line = len(lines)
+ endif
+ endfor
+
+ for [key,val] in items(hdrs)
+ if match(g:notmuch_compose_headers, key) == -1
+ let line = key . ': ' . join(val, ', ')
+ call add(lines, line)
+ endif
+ endfor
+
+ call add(lines, '')
+ if !start_on_line
+ let start_on_line = len(lines) + 1
+ endif
+
+ if len(a:body_lines)
+ call extend(lines, a:body_lines)
+ else
+ call extend(lines, [ '', '' ])
+ endif
+
+ call <SID>NM_newComposeBuffer(lines, start_on_line)
+endfunction
+
+function! s:NM_compose_send()
+ call <SID>NM_assert_buffer_type('compose')
+ let fname = expand('%')
+ let lnum = 1
+ let line = getline(lnum)
+ let hdrs = {}
+ let lst_hdr = ''
+ while match(line, '^$') == -1
+ if match(line, '^Notmuch-Help:') != -1
+ " skip it
+ elseif strlen(lst_hdr) && match(line, '^\s') != -1
+ let hdrs[lst_hdr][-1] = hdrs[lst_hdr][-1] . substitute(line, '^\s*', ' ', '')
+ else
+ let m = matchlist(line, '^\(\w[^:]*\):\s*\(.*\)\s*$')
+ if !len(m)
+ cursor(lnum, 0)
+ throw printf('Eeek! invalid header on line %d', lnum)
+ endif
+ let key = substitute(m[1], '\<\w', '\U&', 'g')
+ if strlen(m[2])
+ if !has_key(hdrs, key)
+ let hdrs[key] = []
+ endif
+ call add(hdrs[key], m[2])
+ endif
+ let lst_hdr = key
+ endif
+ let lnum = lnum + 1
+ let line = getline(lnum)
+ endwhile
+ let body_starts = lnum
+
+ "[-a header] [-b bcc-addr] [-c cc-addr] [-s subject] to-addr
+ let cmd = ['mail']
+ let tos = []
+ for [key, vals] in items(hdrs)
+ if key == 'To'
+ call extend(tos, vals)
+ elseif key == 'Bcc'
+ for adr in vals
+ call add(cmd, '-b')
+ call add(cmd, adr)
+ endfor
+ elseif key == 'Cc'
+ for adr in vals
+ call add(cmd, '-c')
+ call add(cmd, adr)
+ endfor
+ elseif key == 'Subject'
+ for txt in vals
+ call add(cmd, '-s')
+ call add(cmd, txt)
+ endfor
+ else
+ for val in vals
+ call add(cmd, '-a')
+ call add(cmd, key . ': ' . val)
+ endfor
+ endif
+ endfor
+ call extend(cmd, tos)
+
+ " TODO: make sure we have at least one target
+ " TODO: ask about empty jubject, etc
+
+ exec printf('0,%dd', body_starts)
+ write
+
+ call map(cmd, 's:NM_shell_escape(v:val)')
+ let cmdtxt = join(cmd) . '< ' . fname
+ let out = system(cmdtxt)
+ let err = v:shell_error
+ if err
+ undo
+ write
+ call <SID>NM_newBuffer('new', 'error',
+ \ "While running...\n" .
+ \ ' ' . cmdtxt . "\n" .
+ \ "\n" .
+ \ "Failed with...\n" .
+ \ substitute(out, '^', ' ', 'g'))
+ echohl Error
+ echo 'Eeek! unable to send mail'
+ echohl None
+ return
+ endif
+
+ if !exists('b:nm_prev_bufnr')
+ bdelete
+ else
+ let prev_bufnr = b:nm_prev_bufnr
+ bdelete
+ if prev_bufnr == bufnr('%')
+ exec printf("buffer %d", prev_bufnr)
+ endif
+ endif
+ call delete(fname)
+ echo 'Mail sent successfully.'
+endfunction
+
+function! s:NM_compose_attach()
+ echo 'not implemented'
+endfunction
+
+function! s:NM_compose_next_entry_area()
+ let lnum = line('.')
+ let hdr_end = <SID>NM_compose_find_line_match(1,'^$',1)
+ if lnum < hdr_end
+ let lnum = lnum + 1
+ let line = getline(lnum)
+ if match(line, '^\([^:]\+\):\s*$') == -1
+ call cursor(lnum, strlen(line) + 1)
+ return ''
+ endif
+ while match(getline(lnum+1), '^\s') != -1
+ let lnum = lnum + 1
+ endwhile
+ call cursor(lnum, strlen(getline(lnum)) + 1)
+ return ''
+
+ elseif lnum == hdr_end
+ call cursor(lnum+1, strlen(getline(lnum+1)) + 1)
+ return ''
+ endif
+ if mode() == 'i'
+ if !getbufvar(bufnr('.'), '&et')
+ return "\t"
+ endif
+ let space = ''
+ let shiftwidth = a:shiftwidth
+ let shiftwidth = shiftwidth - ((virtcol('.')-1) % shiftwidth)
+ " we assume no one has shiftwidth set to more than 40 :)
+ return ' '[0:shiftwidth]
+ endif
+endfunction
+
+" --- --- compose screen helper functions {{{2
+
+function! s:NM_compose_get_user_email()
+ let name = substitute(system('id -u -n'), '\v(^\s*|\s*$|\n)', '', 'g')
+ let fqdn = substitute(system('hostname -f'), '\v(^\s*|\s*$|\n)', '', 'g')
+
+ " TODO: do this properly
+ return name . '@' . fqdn
+endfunction
+
+function! s:NM_compose_find_line_match(start, pattern, failure)
+ let lnum = a:start
+ let lend = line('$')
+ while lnum < lend
+ if match(getline(lnum), a:pattern) != -1
+ return lnum
+ endif
+ let lnum = lnum + 1
+ endwhile
+ return a:failure
+endfunction
+
+
+" --- notmuch helper functions {{{1
+
+function! s:NM_newBuffer(how, type, content)
+ if strlen(a:how)
+ exec a:how
+ else
+ enew
+ endif
+ setlocal buftype=nofile readonly modifiable scrolloff=0 sidescrolloff=0
+ silent put=a:content
+ keepjumps 0d
+ setlocal nomodifiable
+ execute printf('set filetype=notmuch-%s', a:type)
+ execute printf('set syntax=notmuch-%s', a:type)
+ let b:nm_type = a:type
+endfunction
+
+function! s:NM_newFileBuffer(fdir, fname, type, lines)
+ let fdir = expand(a:fdir)
+ if !isdirectory(fdir)
+ call mkdir(fdir, 'p')
+ endif
+ let file_name = <SID>NM_mktemp(fdir, a:fname)
+ if writefile(a:lines, file_name)
+ throw 'Eeek! couldn''t write to temporary file ' . file_name
+ endif
+ exec printf('edit %s', file_name)
+ setlocal buftype= noreadonly modifiable scrolloff=0 sidescrolloff=0
+ execute printf('set filetype=notmuch-%s', a:type)
+ execute printf('set syntax=notmuch-%s', a:type)
+ let b:nm_type = a:type
+endfunction
+
+function! s:NM_newComposeBuffer(lines, start_on_line)
+ let lines = a:lines
+ let start_on_line = a:start_on_line
+ let real_hdr_start = 1
+ if g:notmuch_compose_header_help
+ let help_lines = [
+ \ 'Notmuch-Help: Type in your message here; to help you use these bindings:',
+ \ 'Notmuch-Help: ,a - attach a file',
+ \ 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)',
+ \ 'Notmuch-Help: ,q - abort the message',
+ \ 'Notmuch-Help: <Tab> - skip through header lines',
+ \ ]
+ call extend(lines, help_lines, 0)
+ let real_hdr_start = len(help_lines)
+ if start_on_line > 0
+ let start_on_line = start_on_line + len(help_lines)
+ endif
+ endif
+ call extend(lines, g:notmuch_signature)
+
+
+ let prev_bufnr = bufnr('%')
+ setlocal bufhidden=hide
+ call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail',
+ \ 'compose', lines)
+ setlocal bufhidden=hide
+ let b:nm_prev_bufnr = prev_bufnr
+
+ call <SID>NM_set_map('n', g:notmuch_compose_nmaps)
+ call <SID>NM_set_map('i', g:notmuch_compose_imaps)
+
+ if start_on_line > 0 && start_on_line <= len(lines)
+ call cursor(start_on_line, strlen(getline(start_on_line)) + 1)
+ else
+ call cursor(real_hdr_start, strlen(getline(real_hdr_start)) + 1)
+ call <SID>NM_compose_next_entry_area()
+ endif
+
+ if g:notmuch_compose_insert_mode_start
+ startinsert!
+ endif
+ echo 'Type your message, use <TAB> to jump to next header and then body.'
+endfunction
+
+function! s:NM_assert_buffer_type(type)
+ if !exists('b:nm_type') || b:nm_type != a:type
+ throw printf('Eeek! expected type %s, but got %s.', a:type,
+ \ exists(b:nm_type) ? b:nm_type : 'something else')
+ endif
+endfunction
+
+function! s:NM_mktemp(dir, name)
+ let time_stamp = strftime('%Y%m%d-%H%M%S')
+ let file_name = substitute(a:dir,'/*$','/','') . printf(a:name, time_stamp)
+ " TODO: check if it exists, try again
+ return file_name
+endfunction
+
+function! s:NM_shell_escape(word)
+ " TODO: use shellescape()
+ let word = substitute(a:word, '''', '\\''', 'g')
+ return '''' . word . ''''
+endfunction
+
+" this function was taken from git.vim, then fixed up
+" http://github.com/motemen/git-vim
+function! s:NM_shell_split(cmd)
+ let l:split_cmd = []
+ let cmd = a:cmd
+ let iStart = 0
+ while 1
+ let t = match(cmd, '\S', iStart)
+ if t < iStart
+ break
+ endif
+ let iStart = t
+
+ let iSpace = match(cmd, '\v(\s|$)', iStart)
+ if iSpace < iStart
+ break
+ endif
+
+ let iQuote1 = match(cmd, '\(^["'']\|[^\\]\@<=["'']\)', iStart)
+ if iQuote1 > iSpace || iQuote1 < iStart
+ let iEnd = iSpace - 1
+ let l:split_cmd += [ cmd[iStart : iEnd] ]
+ else
+ let q = cmd[iQuote1]
+ let iQuote2 = match(cmd, '[^\\]\@<=[' . q . ']', iQuote1 + 1)
+ if iQuote2 < iQuote1
+ throw 'No matching ' . q . ' quote'
+ endif
+ let iEnd = iQuote2
+ let l:split_cmd += [ cmd[iStart+1 : iEnd-1 ] ]
+ endif
+
+
+ let iStart = iEnd + 1
+ endwhile
+
+ return l:split_cmd
+endfunction
+
+
+function! s:NM_run(args)
+ let words = a:args
+ call map(words, 's:NM_shell_escape(v:val)')
+ let cmd = g:notmuch_cmd . ' ' . join(words) . '< /dev/null'
+
+ let start = reltime()
+ let out = system(cmd)
+ let err = v:shell_error
+ let delta = reltime(start)
+
+ if exists('g:notmuch_debug') && g:notmuch_debug
+ echo printf('[%s] {%s} %s', reltimestr(delta), string(err), string(cmd))
+ endif
+
+ if err
+ echohl Error
+ echo substitute(out, '\n*$', '', '')
+ echohl None
+ return ''
+ else
+ return out
+ endif
+endfunction
+
+" --- external mail handling helpers {{{1
+
+function! s:NM_new_mail()
+ call <SID>NM_cmd_compose([], [])
+endfunction
+
+" --- tag manipulation helpers {{{1
+
+" used to combine an array of words with prefixes and separators
+" example:
+" NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()')
+" -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')']
+function! s:NM_combine_tags(word_prefix, words, separator, brackets)
+ let res = []
+ for word in a:words
+ if len(res) && strlen(a:separator)
+ call add(res, a:separator)
+ endif
+ call add(res, a:word_prefix . word)
+ endfor
+ if len(res) > 1 && strlen(a:brackets)
+ if strlen(a:brackets) != 2
+ throw 'Eeek! brackets arg to NM_combine_tags must be 2 chars'
+ endif
+ call insert(res, a:brackets[0])
+ call add(res, a:brackets[1])
+ endif
+ return res
+endfunction
+
+" --- other helpers {{{1
+
+function! s:NM_get_search_words()
+ if !exists('b:nm_search_words')
+ throw 'Eeek! no b:nm_search_words'
+ endif
+ return b:nm_search_words
+endfunction
+
+function! s:NM_kill_this_buffer()
+ if exists('b:nm_prev_bufnr')
+ let prev_bufnr = b:nm_prev_bufnr
+ bdelete!
+ exec printf("buffer %d", prev_bufnr)
+ else
+ echo "This is the last buffer; use :q<CR> to quit."
+ endif
+endfunction
+
+function! s:NM_search_expand(arg)
+ let word = expand(a:arg)
+ let prev_bufnr = bufnr('%')
+ setlocal bufhidden=hide
+ call <SID>NM_cmd_search([word])
+ setlocal bufhidden=delete
+ let b:nm_prev_bufnr = prev_bufnr
+endfunction
+
+function! s:NM_add_remove_tags(filter, prefix, tags)
+ let filter = len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]
+ if !len(filter)
+ throw 'Eeek! I couldn''t find the thead id!'
+ endif
+ call map(a:tags, 'a:prefix . v:val')
+ let args = ['tag']
+ call extend(args, a:tags)
+ call add(args, '--')
+ call extend(args, filter)
+ " TODO: handle errors
+ call <SID>NM_run(args)
+endfunction
+
+function! s:NM_add_remove_tags_on_screen(online, prefix, tags)
+ setlocal modifiable
+ if a:prefix == '-'
+ for tagname in a:tags
+ exec printf('silent! %ss/(\([^)]*\)\<%s\>\([^)]*\))$/(\1\2)/', string(a:online), tagname)
+ endfor
+ else
+ for tagname in a:tags
+ exec printf('silent! %ss/(\([^)]*\))$/(\1 %s)/', string(a:online), tagname)
+ endfor
+ endif
+ setlocal nomodifiable
+endfunction
+
+" --- process and set the defaults {{{1
+
+function! NM_set_defaults(force)
+ for [key, dflt] in items(s:notmuch_defaults)
+ let cmd = ''
+ if !a:force && exists(key) && type(dflt) == type(eval(key))
+ continue
+ elseif type(dflt) == type(0)
+ let cmd = printf('let %s = %d', key, dflt)
+ elseif type(dflt) == type('')
+ let cmd = printf('let %s = ''%s''', key, dflt)
+ " FIXME: not sure why this didn't work when dflt is an array
+ "elseif type(dflt) == type([])
+ " let cmd = printf('let %s = %s', key, string(dflt))
+ else
+ echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,%s]',
+ \ a:force, key, string(dflt))
+ continue
+ endif
+ exec cmd
+ endfor
+endfunction
+call NM_set_defaults(0)
+
+" for some reason NM_set_defaults() didn't work for arrays...
+if !exists('g:notmuch_show_headers')
+ let g:notmuch_show_headers = s:notmuch_show_headers_defaults
+endif
+if !exists('g:notmuch_initial_search_words')
+ let g:notmuch_initial_search_words = s:notmuch_initial_search_words_defaults
+endif
+if !exists('g:notmuch_folders')
+ let g:notmuch_folders = s:notmuch_folders_defaults
+endif
+
+if !exists('g:notmuch_signature')
+ let g:notmuch_signature = s:notmuch_signature_defaults
+endif
+if !exists('g:notmuch_compose_headers')
+ let g:notmuch_compose_headers = s:notmuch_compose_headers_defaults
+endif
+
+" --- assign keymaps {{{1
+
+function! s:NM_set_map(type, maps)
+ nmapclear
+ for [key, code] in items(a:maps)
+ exec printf('%snoremap <buffer> %s %s', a:type, key, code)
+ endfor
+ " --- this is a hack for development :)
+ nnoremap ,nmr :runtime! plugin/notmuch.vim<CR>
+endfunction
+
+" --- command handler {{{1
+
+function! NotMuch(args)
+ let args = a:args
+ if !strlen(args)
+ let args = 'folders'
+ endif
+
+ let words = <SID>NM_shell_split(args)
+ if words[0] == 'folders' || words[0] == 'f'
+ let words = words[1:]
+ call <SID>NM_cmd_folders(words)
+
+ elseif words[0] == 'search' || words[0] == 's'
+ if len(words) > 1
+ let words = words[1:]
+ elseif exists('b:nm_search_words')
+ let words = b:nm_search_words
+ else
+ let words = g:notmuch_initial_search_words
+ endif
+ call <SID>NM_cmd_search(words)
+
+ elseif words[0] == 'show'
+ echoe 'show is not yet implemented.'
+
+ elseif words[0] == 'new' || words[0] == 'compose'
+ let words = words[1:]
+ call <SID>NM_cmd_compose(words, [])
+ endif
+endfunction
+function! CompleteNotMuch(arg_lead, cmd_line, cursor_pos)
+ return []
+endfunction
+
+
+" --- glue {{{1
+
+command! -nargs=* -complete=customlist,CompleteNotMuch NotMuch call NotMuch(<q-args>)
+cabbrev notmuch <c-r>=(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch')<CR>
+
+" vim: set ft=vim ts=8 sw=8 et foldmethod=marker :
diff --git a/vim/syntax/notmuch-compose.vim b/vim/syntax/notmuch-compose.vim
new file mode 100644
index 0000000..19adb75
--- /dev/null
+++ b/vim/syntax/notmuch-compose.vim
@@ -0,0 +1,7 @@
+runtime! syntax/mail.vim
+
+syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!'
+syntax match nmComposeHelpLine /Notmuch-Help:/ contained
+
+highlight link nmComposeHelp Include
+highlight link nmComposeHelpLine Error
diff --git a/vim/syntax/notmuch-folders.vim b/vim/syntax/notmuch-folders.vim
new file mode 100644
index 0000000..9477f86
--- /dev/null
+++ b/vim/syntax/notmuch-folders.vim
@@ -0,0 +1,12 @@
+" notmuch folders mode syntax file
+
+syntax region nmFoldersCount start='^' end='\%10v'
+syntax region nmFoldersName start='\%11v' end='\%31v'
+syntax match nmFoldersSearch /([^()]\+)$/
+
+highlight link nmFoldersCount Statement
+highlight link nmFoldersName Type
+highlight link nmFoldersSearch String
+
+highlight CursorLine term=reverse cterm=reverse gui=reverse
+
diff --git a/vim/syntax/notmuch-search.vim b/vim/syntax/notmuch-search.vim
new file mode 100644
index 0000000..71839fd
--- /dev/null
+++ b/vim/syntax/notmuch-search.vim
@@ -0,0 +1,24 @@
+" notmuch search mode syntax file
+
+" TODO: I cannot figure out why nmSearchTags is not matching anything :(
+
+syntax region nmSearchDate start='^' end='\%13v' oneline
+syntax region nmSearchCountAndFrom start='\%14v\[' end='|' oneline contains=nmSearchCount,nmSearchFrom
+syntax region nmSearchCount start='\[' end='\]' oneline contained contains=nmSearchCountZero,nmSearchCountSome,nmSearchCountAll
+syntax region nmSearchFrom start='\]\@<=' end='|' oneline contained
+syntax match nmSearchCountZero '0/\(\d\+\)' contained
+syntax match nmSearchCountSome '\([1-9]\d*\)/\(\d\+\)' contained
+syntax match nmSearchCountAll '\(\d\+\)/\1' contained
+syntax match nmSearchSquareBracketText '\(\[\w\+\]\)'
+syntax match nmSearchTags /([^)]\+)$/
+
+highlight link nmSearchDate Statement
+"highlight link nmSearchCount Comment
+highlight link nmSearchCountZero Function
+highlight link nmSearchCountSome Special
+highlight link nmSearchCountAll Type
+highlight link nmSearchFrom Include
+highlight link nmSearchSquareBracketText Special
+highlight link nmSearchTags String
+
+highlight CursorLine term=reverse cterm=reverse gui=reverse
diff --git a/vim/syntax/notmuch-show.vim b/vim/syntax/notmuch-show.vim
new file mode 100644
index 0000000..20bcc39
--- /dev/null
+++ b/vim/syntax/notmuch-show.vim
@@ -0,0 +1,25 @@
+" notmuch show mode syntax file
+
+syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags
+syntax match nmShowMsgDescWho /[^)]\+)/ contained
+syntax match nmShowMsgDescDate / ([^)]\+[0-9]) / contained
+syntax match nmShowMsgDescTags /([^)]\+)$/ contained
+
+syntax cluster nmShowMsgHead contains=nmShowMsgHeadKey,nmShowMsgHeadVal
+syntax match nmShowMsgHeadKey /^[^:]\+: / contained
+syntax match nmShowMsgHeadVal /^\([^:]\+: \)\@<=.*/ contained
+
+syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit
+syntax include @nmShowMsgBodyMail syntax/mail.vim
+
+" git-diff.vim marks up diffs in emails, see README for details
+silent! syntax include @nmShowMsgBodyGit syntax/git-diff.vim
+
+highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse
+highlight link nmShowMsgDescDate Type
+highlight link nmShowMsgDescTags String
+
+highlight link nmShowMsgHeadKey Macro
+"highlight link nmShowMsgHeadVal NONE
+
+highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black