diff options
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 @@ -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 @@ -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>. + @@ -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 @@ -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. @@ -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. @@ -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_ */ @@ -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 (¬much_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 (®ex, + "^([^ ]+) \\(([^)]*)\\)$", + 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 (®ex, 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 (®ex); + + 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 @@ -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 |