From: Carl Worth Date: Thu, 22 Apr 2010 00:29:48 +0000 (-0700) Subject: Merge Sebastian Spaeth's python bindings into bindings/python X-Git-Tag: 0.3~121 X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=commitdiff_plain;h=8cbb5114a20c1217f23977fd5edca99a0b7a2955;hp=3b558de7811a765c3295a58bd53e2156eca0e32e Merge Sebastian Spaeth's python bindings into bindings/python Sebastian offered to maintain these bindings within the notmuch repository and offered them in the following repository: git://github.com/spaetz/python-notmuch.git These are the bindings formerly known as "cnotmuch" and now known simply as "notmuch" from within python. The bindings are not yet integrated into the build system and packaging of the primary ntomuch repository. --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..217440d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +.first-build-message +Makefile.config +TAGS +tags +*cscope* +.deps +notmuch +notmuch-shared +notmuch.1.gz +libnotmuch.so* +*.[ao] +*~ +.*.swp +*.elc +releases diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..6d0f2de8 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,30 @@ +Carl Worth 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 ) + Implementation of SHA-1 (nice and small) (libsha1.c) + +Please see the various files in the Notmuch distribution for +individual copyright statements. + +And of course, though their code isn't distributed here, Notmuch would +be not much of anything without the contributors to Xapian, the search +engine that does the really heavy lifting, as well as the various +system libraries, compilers, and the kernel that make it all work +(thanks GNU, thanks Linux). Thanks to everyone who has played a part! + +Here is an incomplete list of other people that have made +contributions to Notmuch (whether by code, bug reporting/fixes, +ideas, inspiration, testing or feedback): + +Martin Krafft +Keith Packard +Jamey Sharp diff --git a/COPYING b/COPYING new file mode 100644 index 00000000..6dea6939 --- /dev/null +++ b/COPYING @@ -0,0 +1,15 @@ +Notmuch is free software. + +You can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, +either version 3 of the License, or (at your option) any later +version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program, (in the COPYING-GPL-3 file in this +directory). If not, see http://www.gnu.org/licenses/ diff --git a/COPYING-GPL-3 b/COPYING-GPL-3 new file mode 100644 index 00000000..44325404 --- /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. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. + diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..bc7bc677 --- /dev/null +++ b/INSTALL @@ -0,0 +1,86 @@ +Build and install instructions for Notmuch. + +Compilation commands +-------------------- +The process for compiling and installing Notmuch is the very standard +sequence of: + + ./configure + make + sudo make install + +In fact, if you don't plan to pass any arguments to the configure +script, then you can skip that step and just start with "make", (which +will call configure for you). See this command: + + ./configure --help + +for detailed documentation of the things you can control at the +configure stage. + +notmuch.el installation +----------------------- +Installing the notmuch.el emacs lisp function systemwide: + + sudo make install-emacs + +Each user needs to add (require 'notmuch) in his ~/.emacs to make it +available and then start emacs running notmuch with "emacs -f notmuch" +or start notmuch from within emacs with "M-x notmuch". + +Dependencies +------------ +Notmuch depends on three libraries: Xapian, GMime 2.4, and Talloc +which are each described below: + + Xapian + ------ + Xapian is the search-engine library underlying Notmuch. + + It provides all the real machinery of indexing and searching, + (including the very nice parsing of the query string). + + Xapian is available from http://xapian.org + + After installing Xapian, please ensure that you have a command + named "xapian-config" on your $PATH as notmuch expects. (At + least one notmuch user found that Xapian installed the config + program to /usr/local/bin/xapian-config-1.1 ). + + GMime 2.4 + --------- + GMime 2.4 provides decoding of MIME email messages for Notmuch. + + Without GMime, Notmuch would not be able to extract and index + the actual text from email message encoded as BASE64, etc. + + GMime 2.4 is available from http://spruce.sourceforge.net/gmime/ + + Talloc + ------ + Talloc is a memory-pool allocator used by Notmuch. + + Talloc is an extremely lightweight and easy-to-use tool for + allocating memory in a hierarchical fashion and then freeing + it with a single call of the top-level handle. Using it has + made development of Notmuch much easier and much less prone to + memory leaks. + + Talloc is available from http://talloc.samba.org/ + +On a modern, package-based operating system you can install all of the +dependencies with a simple simple command line. For example: + + For Debian and similar: + + sudo apt-get install libxapian-dev libgmime-2.4-dev libtalloc-dev + + For Fedora and similar: + + sudo yum install xapian-core-devel gmime-devel libtalloc-devel + +On other systems, a similar command can be used, but the details of +the package names may be different. + + + diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..619392d3 --- /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 00000000..5bb570b1 --- /dev/null +++ b/Makefile.local @@ -0,0 +1,289 @@ +# -*- makefile -*- + +# Here's the (hopefully simple) versioning scheme. +# +# Releases of notmuch have a two-digit version (0.1, 0.2, etc.). We +# increment the second digit for each release and increment the first +# digit when we reach particularly major milestones of usability. +# +# Between releases, (such as when compiling notmuch from the git +# repository), we let git append identification of the actual commit. +PACKAGE=notmuch + +IS_GIT=$(shell if [ -d .git ] ; then echo yes ; else echo no; fi) + +VERSION:=$(shell cat version) +ifneq ($(MAKECMDGOALS),release) +ifneq ($(MAKECMDGOALS),release-message) +ifeq ($(IS_GIT),yes) +VERSION:=$(shell git describe --match '[0-9.]*') +endif +endif +endif + +RELEASE_HOST=notmuchmail.org +RELEASE_DIR=/srv/notmuchmail.org/www/releases +RELEASE_URL=http://notmuchmail.org/releases +TAR_FILE=$(PACKAGE)-$(VERSION).tar.gz +SHA1_FILE=$(TAR_FILE).sha1 +GPG_FILE=$(SHA1_FILE).asc + +# Smash together user's values with our extra values +FINAL_CFLAGS = -DNOTMUCH_VERSION=$(VERSION) $(CFLAGS) $(WARN_CFLAGS) $(CONFIGURE_CFLAGS) $(extra_cflags) +FINAL_CXXFLAGS = $(CXXFLAGS) $(WARN_CXXFLAGS) $(CONFIGURE_CXXFLAGS) $(extra_cflags) $(extra_cxxflags) +FINAL_NOTMUCH_LDFLAGS = $(LDFLAGS) -Llib -lnotmuch +FINAL_NOTMUCH_LINKER = CC +ifneq ($(LINKER_RESOLVES_LIBRARY_DEPENDENCIES),1) +FINAL_NOTMUCH_LDFLAGS += $(CONFIGURE_LDFLAGS) +FINAL_NOTMUCH_LINKER = CXX +endif +FINAL_LIBNOTMUCH_LDFLAGS = $(LDFLAGS) $(CONFIGURE_LDFLAGS) + +.PHONY: all +all: notmuch notmuch-shared notmuch.1.gz +ifeq ($(MAKECMDGOALS),) +ifeq ($(shell cat .first-build-message 2>/dev/null),) + @NOTMUCH_FIRST_BUILD=1 $(MAKE) --no-print-directory all + @echo "" + @echo "Compilation of notmuch is now complete. You can install notmuch with:" + @echo "" + @echo " make install" + @echo "" + @echo "Note that depending on the prefix to which you are installing" + @echo "you may need root permission (such as \"sudo make install\")." + @echo "See \"./configure --help\" for help on setting an alternate prefix." + @echo Printed > .first-build-message +endif +endif + +.PHONY: test +test: all + @./test/notmuch-test + +$(TAR_FILE): + git archive --format=tar --prefix=$(PACKAGE)-$(VERSION)/ HEAD > $(TAR_FILE).tmp + echo $(VERSION) > version.tmp + tar --append -f $(TAR_FILE).tmp --transform s_^_$(PACKAGE)-$(VERSION)/_ --transform 's_.tmp$$__' version.tmp + rm version.tmp + gzip < $(TAR_FILE).tmp > $(TAR_FILE) + @echo "Source is ready for release in $(TAR_FILE)" + +$(SHA1_FILE): $(TAR_FILE) + sha1sum $^ > $@ + +$(GPG_FILE): $(SHA1_FILE) + @echo "Please enter your GPG password to sign the checksum." + gpg --armor --sign $^ + +.PHONY: dist +dist: $(TAR_FILE) + +# We invoke make recursively only to force ordering of our phony +# targets in the case of parallel invocation of make (-j). +# +# We carefully ensure that our VERSION variable is passed down to any +# sub-ordinate make invocations (which won't otherwhise know that they +# are part of the release and need to take the version from the +# version file). +.PHONY: release +release: verify-source-tree-and-version + $(MAKE) VERSION=$(VERSION) verify-newer + $(MAKE) VERSION=$(VERSION) test + rm -rf ./debian-build + git-buildpackage + cp debian-build/notmuch_$(VERSION).tar.gz notmuch-$(VERSION).tar.gz + $(MAKE) VERSION=$(VERSION) $(GPG_FILE) + scp $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) $(RELEASE_HOST):$(RELEASE_DIR) + ssh $(RELEASE_HOST) "rm -f $(RELEASE_DIR)/LATEST-$(PACKAGE)-[0-9]* && ln -s $(TAR_FILE) $(RELEASE_DIR)/LATEST-$(PACKAGE)-$(VERSION)" + mkdir -p releases + mv $(TAR_FILE) $(SHA1_FILE) $(GPG_FILE) releases + (cd debian-build; dput *.changes) + mv debian-build/* releases + rmdir debian-build + git tag -s -m "$(PACKAGE) $(VERSION) release" $(VERSION) + git push origin $(VERSION) + $(MAKE) VERSION=$(VERSION) release-message > $(PACKAGE)-$(VERSION).announce + @echo "Please send a release announcement using $(PACKAGE)-$(VERSION).announce as a template." + +.PHONY: release-message +release-message: + @echo "To: notmuch@notmuchmail.org" + @echo "Subject: $(PACKAGE) release $(VERSION) now available" + @echo "" + @echo "Where to obtain notmuch $(VERSION)" + @echo "===========================" + @echo " $(RELEASE_URL)/$(TAR_FILE)" + @echo "" + @echo "Which can be verified with:" + @echo "" + @echo " $(RELEASE_URL)/$(SHA1_FILE)" + @echo -n " " + @cat releases/$(SHA1_FILE) + @echo "" + @echo " $(RELEASE_URL)/$(GPG_FILE)" + @echo " (signed by `getent passwd "$$USER" | cut -d: -f 5 | cut -d, -f 1`)" + @echo "" + @echo "What's new in notmuch $(VERSION)" + @echo "=========================" + @sed -ne '/^[Nn]otmuch $(VERSION)/{n;n;b NEWS}; d; :NEWS /^===/q; {p;n;b NEWS}' < NEWS | head -n -2 + @echo "" + @echo "What is notmuch" + @echo "===============" + @echo "Notmuch is a system for indexing, searching, reading, and tagging" + @echo "large collections of email messages in maildir or mh format. It uses" + @echo "the Xapian library to provide fast, full-text search with a convenient" + @echo "search syntax." + @echo "" + @echo "For more about notmuch, see http://notmuchmail.org" + +# This is a chain of dependencies rather than a simple list simply to +# avoid the messages getting interleaved in the case of a parallel +# make invocation. +.PHONY: verify-source-tree-and-version +verify-source-tree-and-version: verify-no-dirty-code + +.PHONY: verify-no-dirty-code +verify-no-dirty-code: verify-version-debian +ifeq ($(IS_GIT),yes) + @printf "Checking that source tree is clean..." +ifneq ($(shell git ls-files -m),) + @echo "No" + @echo "The following files have been modified since the most recent git commit:" + @echo "" + @git ls-files -m + @echo "" + @echo "The release will be made from the committed state, but perhaps you meant" + @echo "to commit this code first? Please clean this up to make it more clear." + @false +else + @echo "Good" +endif +endif + +.PHONY: verify-version-debian +verify-version-debian: verify-version-components + @echo -n "Checking that Debian package version is $(VERSION)..." + @if [ "$(VERSION)" != $$(dpkg-parsechangelog | grep ^Version | awk '{print $$2}') ] ; then \ + (echo "No." && \ + echo "Please edit version and debian/changelog to have consistent versions." && false) \ + fi + @echo "Good." + +.PHONY: verify-version-components +verify-version-components: + @echo -n "Checking that $(VERSION) consists only of digits and periods..." + @if echo $(VERSION) | grep -q -v -x '[0-9.]*'; then \ + (echo "No." && \ + echo "Please follow the instructions in RELEASING to choose a version" && false) \ + else :; fi + @echo "Good." + +.PHONY: verify-newer +verify-newer: + @echo -n "Checking that no $(VERSION) release already exists..." + @ssh $(RELEASE_HOST) test ! -e $(RELEASE_DIR)/$(TAR_FILE) \ + || (echo "Ouch." && echo "Found: $(RELEASE_HOST):$(RELEASE_DIR)/$(TAR_FILE)" \ + && echo "Refusing to replace an existing release." \ + && echo "Don't forget to update \"version\" as described in RELEASING before release." && false) + @echo "Good." + +# The user has not set any verbosity, default to quiet mode and inform the +# user how to enable verbose compiles. +ifeq ($(V),) +quiet_DOC := "Use \"$(MAKE) V=1\" to see the verbose compile lines.\n" +quiet = @printf $(quiet_DOC)$(eval quiet_DOC:=)"$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//')) +endif +# The user has explicitly enabled quiet compilation. +ifeq ($(V),0) +quiet = @printf "$1 $@\n"; $($(shell echo $1 | sed -e s'/ .*//')) +endif +# Otherwise, print the full command line. +quiet ?= $($(shell echo $1 | sed -e s'/ .*//')) + +%.o: %.cc $(global_deps) + $(call quiet,CXX $(CXXFLAGS)) -c $(FINAL_CXXFLAGS) $< -o $@ + +%.o: %.c $(global_deps) + $(call quiet,CC $(CFLAGS)) -c $(FINAL_CFLAGS) $< -o $@ + +.deps/%.d: %.c $(global_deps) + @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \ + $(CC) -M $(CPPFLAGS) $(FINAL_CFLAGS) $< > $@.$$$$ 2>/dev/null ; \ + sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \ + rm -f $@.$$$$ + +.deps/%.d: %.cc $(global_deps) + @set -e; rm -f $@; mkdir -p $$(dirname $@) ; \ + $(CXX) -M $(CPPFLAGS) $(FINAL_CXXFLAGS) $< > $@.$$$$ 2>/dev/null ; \ + sed 's,'$$(basename $*)'\.o[ :]*,$*.o $@ : ,g' < $@.$$$$ > $@; \ + rm -f $@.$$$$ + +DEPS := $(SRCS:%.c=.deps/%.d) +DEPS := $(DEPS:%.cc=.deps/%.d) +-include $(DEPS) + +.PHONY : clean +clean: + rm -f $(CLEAN); rm -rf .deps + +# We don't (yet) have any distributed files not in the upstream repository. +# So distclean is currently identical to clean. +.PHONY: distclean +distclean: clean + +notmuch_client_srcs = \ + debugger.c \ + gmime-filter-reply.c \ + gmime-filter-headers.c \ + notmuch.c \ + notmuch-config.c \ + notmuch-count.c \ + notmuch-dump.c \ + notmuch-new.c \ + notmuch-reply.c \ + notmuch-restore.c \ + notmuch-search.c \ + notmuch-search-tags.c \ + notmuch-setup.c \ + notmuch-show.c \ + notmuch-tag.c \ + notmuch-time.c \ + query-string.c \ + show-message.c \ + json.c + +notmuch_client_modules = $(notmuch_client_srcs:.c=.o) + +notmuch: $(notmuch_client_modules) lib/libnotmuch.a + $(call quiet,CXX $(CFLAGS)) $^ $(FINAL_LIBNOTMUCH_LDFLAGS) -o $@ + +notmuch-shared: $(notmuch_client_modules) lib/$(LINKER_NAME) + $(call quiet,$(FINAL_NOTMUCH_LINKER) $(CFLAGS)) $(notmuch_client_modules) $(FINAL_NOTMUCH_LDFLAGS) -o $@ + +notmuch.1.gz: notmuch.1 + gzip --stdout $^ > $@ + +.PHONY: install +install: all notmuch.1.gz + mkdir -p $(DESTDIR)$(mandir)/man1 + install -m0644 notmuch.1.gz $(DESTDIR)$(mandir)/man1/ + mkdir -p $(DESTDIR)$(prefix)/bin/ + install notmuch-shared $(DESTDIR)$(prefix)/bin/notmuch +ifeq ($(MAKECMDGOALS), install) + @echo "" + @echo "Notmuch is now installed to $(DESTDIR)$(prefix)" + @echo "" + @echo "To run notmuch from emacs, each user should add the following line to ~/.emacs:" + @echo "" + @echo " (require 'notmuch)" + @echo "" + @echo "And should then run \"M-x notmuch\" from within emacs or run \"emacs -f notmuch\"" +endif + +.PHONY: install-desktop +install-desktop: + mkdir -p $(DESTDIR)$(desktop_dir) + desktop-file-install --mode 0644 --dir $(DESTDIR)$(desktop_dir) notmuch.desktop + +SRCS := $(SRCS) $(notmuch_client_srcs) +CLEAN := $(CLEAN) notmuch notmuch-shared $(notmuch_client_modules) notmuch.elc notmuch.1.gz diff --git a/NEWS b/NEWS new file mode 100644 index 00000000..eba0fd5a --- /dev/null +++ b/NEWS @@ -0,0 +1,150 @@ +Notmuch 0.2 (2010-04-16) +======================== +This is the second release of the notmuch mail system, with actual +detailed release notes this time! + +This release consists of a number of minor new features that make +notmuch more pleasant to use, and a few fairly major bug fixes. + +We didn't quite hit our release target of "about a week" from the 0.1 +release, (0.2 is happening 11 days after 0.1), but we hope to do +better for next week. Look forward to some major features coming to +notmuch in subsequent releases. + +-Carl + +General features +---------------- +Better guessing of From: header. + + Notmuch now tries harder to guess which configured address should be + used as the From: line in a "notmuch reply". It will examine the + Received: headers if it fails to find any configured address in To: + or Cc:. This allows it to often choose the correct address even when + replying to a message sent to a mailing list, and not directly to a + configured address. + +Make "notmuch count" with no arguments count all messages + + Previously, it was hard to construct a search term that was + guaranteed to match all messages. + +Provide a new special-case search term of "*" to match all messages. + + This can be used in any command accepting a search term, such as + "notmuch search '*'". Note that you'll want to take care that the + shell doesn't expand * against the current files. And note that the + support for "*" is a special case. It's only meaningful as a single + search term and loses its special meaning when combined with any + other search terms. + +Automatically detect thread connections even when a parent message is +missing. + + Previously, if two or more message were received with a common + parent, but that parent was not received, then these messages would + not be recognized as belonging to the same thread. This is now fixed + so that such messages are properly connected in a thread. + +General bug fixes +----------------- +Fix potential data loss in "notmuch new" with SIGINT + + One code path in "notmuch new" was not properly handling + SIGINT. Previously, this could lead to messages being removed from + the database (and their tags being lost) if the user pressed + Control-C while "notmuch new" was working. + +Fix segfault when a message includes a MIME part that is empty. + +Fix handling of non-ASCII characters with --format=json + + Previously, characters outside the range of 7-bit ASCII were + silently dropped from the JSON output. This led to corrupted display + of utf-8 content in the upcoming notmuch web-based frontends. + +Fix headers to be properly decoded in "notmuch reply" + + Previously, the user might see: + + Subject: Re: =?iso-8859-2?q?Rozlu=E8ka?= + + rather than: + + Subject: Re: Rozlučka + + The former text is properly encoded to be RFC-compliant SMTP, will + be sent correctly, and will be properly decoded by the + recipient. But the user trying to edit the reply would likely be + unable to read or edit that field in its encoded form. + +Emacs client features +--------------------- +Show the last few lines of citations as well as the first few lines. + + It's often the case that the last sentence of a citation is what is + being replied to directly, so the last few lines are often much more + important. The number of lines shown at the beginning and end of any + citation can be configured, (notmuch-show-citation-lines-prefix and + notmuch-show-citation-lines-suffix). + +The '+' and '-' commands in the search view can now add and remove +tags by region. + + Selective bulk tagging is now possible by selecting a region of + threads and then using either the '+' or '-' keybindings. Bulk + tagging is still available for all threads matching the current + search with th '*' binding. + +More meaningful buffer names for thread-view buffers. + + Notmuch now uses the Subject of the thread as the buffer + name. Previously it was using the thread ID, which is a meaningless + number to the user. + +Provide for customized colors of threads in search view based on tags. + + See the documentation of notmuch-search-line-faces, (or us "M-x + customize" and browse to the "notmuch" group within "Applications" + and "Mail"), for details on how to configure this colorization. + +Build-system features +--------------------- +Add support to properly build libnotmuch on Darwin systems (OS X). + +Add support to configure for many standard options. + + We include actual support for: + + --includedir --mandir --sysconfdir + + And accept and silently ignore several more: + + --build --infodir --libexecdir --localstatedir + --disable-maintainer-mode --disable-dependency-tracking + +Install emacs client in "make install" rather than requiring a +separate "make install-emacs". + +Automatically compute versions numbers between releases. + + This support uses the git-describe notation, so a version such as + 0.1-144-g43cbbfc indicates a version that is 144 commits since the + 0.1 release and is available as git commit "43cbbfc". + +Add a new "make test" target to run the test suite and actually verify +its results. + +Notmuch 0.1 (2010-04-05) +======================== +This is the first release of the notmuch mail system. + +It includes the libnotmuch library, the notmuch command-line +interface, and an emacs-based interface to notmuch. + +Note: Notmuch will work best with Xapian 1.0.18 (or later) or Xapian +1.1.4 (or later). Previous versions of Xapian (whether 1.0 or 1.1) had +a performance bug that made notmuch very slow when modifying +tags. This would cause distracting pauses when reading mail while +notmuch would wait for Xapian when removing the "inbox" and "unread" +tags from messages in a thread. diff --git a/README b/README new file mode 100644 index 00000000..5f029c82 --- /dev/null +++ b/README @@ -0,0 +1,71 @@ +Notmuch - thread-based email index, search and tagging. + +Notmuch is a system for indexing, searching, reading, and tagging +large collections of email messages in maildir or mh format. It uses +the Xapian library to provide fast, full-text search with a convenient +search syntax. + +Notmuch is free software, released under the GNU General Public +License version 3 (or later). + +Building notmuch +---------------- +See the INSTALL file for notes on compiling and installing notmuch. + +Contacting users and developers +------------------------------- +The website for Notmuch is: + + http://notmuchmail.org + +The mailing list address for the notmuch community is: + + notmuch@notmuchmail.org + +We welcome any sort of questions, comments, kudos, or code there. + +Subscription is not required, (but if you do subscribe you'll avoid +any delay due to moderation). See the website for subscription +information. + +There is also an IRC channel dedicated to talk about using and +developing notmuch: + + IRC server: irc.freenode.net + Channel: #notmuch + +Interface options +----------------- +Notmuch includes a "notmuch" command-line interface to the system. +This is not a very convenient interface and it is not expected that +users will find it satisfying. Instead, there are two option for +obtaining a more sophisticated interface: + + 1. Build on top of the "notmuch" command-line interface. + + This might be a reasonable option for a very text-oriented + client environment. For example, an emacs-based interface for + notmuch is already under development and is available in the + notmuch.el file in this distribution. + + If someone were to write a curses-based interface, or similar, + it might also be reasonable to build on the "notmuch" + command-line interface. + + 2. Build on top of the notmuch library interface. + + This is a better choice for developing an interface that has + full control of the presentation of email threads and + messages. It is expected that anyone integrating Notmuch into + an existing, graphical email program use the notmuch library + interface. + + The public interface to the notmuch library is contained in + the notmuch.h header file. The "notmuch" command-line program + in notmuch.c can be used as good example code, since it is a + simple program that is written on top of the library + interface. + +As can be seen, alternate interfaces to the Notmuch mail system are +still in development. We would appreciate any contributions to these +efforts. diff --git a/RELEASING b/RELEASING new file mode 100644 index 00000000..d314a6c6 --- /dev/null +++ b/RELEASING @@ -0,0 +1,105 @@ +Here are the steps to follow to create a new notmuch release. + +These steps assume that a process (not described here) has already +been followed to determine the features and bug fixes to be included +in a release, and that adequate testing by the community has already +been performed. The little bit of testing performed here is a safety +check, and not a substitute for wider testing. + +OK, so the code to be released is present and committed to your git +repository. From here, there are just a few steps to release: + +1) Verify that the NEWS file is up to date. + + Read through the entry at the top of the NEWS file and see if + you are aware of any major features recently added that are + not mentioned there. If so, pleas add them, (and ask the + authors of the commits to update NEWS in the future). + +2) Verify that the library version in lib/Makefile.local is correct + + See the instructions there for how to increment it. + + The version should have been updated with any commits that + added API, but do check that that is the case. The command + below can be useful for inspecting header-file changes since + the last release X.Y: + + git diff X.Y..HEAD -- lib/notmuch.h + + Note: We currently don't plan to increment + LIBNOTMUCH_VERSION_MAJOR beyond 1, so if there *are* + incompatible changes to the library interface, then + stop. Don't release. Figure out the plan on the notmuch + mailing list. + + Commit this change, if any. + +3) Upgrade the version in the file "version" + + The scheme for the release number is as follows: + + A major milestone in usability causes an increase in the major + number, yielding a two-component version with a minor number + of 0, (such as "1.0" or "2.0"). + + Otherwise, releases with changes in features cause an increase + in the minor number, yielding a two-component version, (such + as "1.1" or "1.2"). + + Finally, releases that do not change "features" but are merely + bug fixes either add increase the micro number or add it + (starting at ".1" if not present). So a bug-fix release from + "1.0" would be "1.0.1" and a subsequent bug-fix release would + be "1.0.2" etc. + + Commit this change. + +4) Create an entry for the new release in debian/changelog + + The syntax of this file is tightly restricted, but the + available emacs mode (see the dpkg-dev-el package helps). The + entries here will be the Debian-relevant single-line + description of changes from the NEWS entry. And the version + must match the version in the next step. + + Commit this change. + + XXX: It would be great if this step were automated as part of + release, (taking entries from NEWS and the version from the + version file, and creating a new commit, etc.) + +5) Run "make release" which will perform the following steps. + + Note: If any problem occurs during the process, (such as a lintian + warning that you decide should be fixed), you can abort at the + prompt for your GPG passphrase and nothing will have been uploaded + yet. + + * Ensure that the version consists only of digits and periods + * Ensure that version and debian/changelog have the same version + * Verify that the source tree is clean + * Compile the current notmuch code (aborting release if it fails) + * Run the notmuch test suite (aborting release if it fails) + * Compile a Debian package + * Copy the tar file from what was made for Debian package + * Generate a .sha1 sum file for the tar file + * Sign the sha1sum using your GPG setup (asks for your GPG password) + * Check that no release exists with the current version + * scp the three files to appear on http://notmuchmail.org/releases + * Create a LATEST-notmuch-version file (after deleting any old one) + * Place local copies of the tar, sha1, and gpg files into releases + * Upload the Debian package + * Place a local copy of the Debian package files in releases + * Tag the entire source tree with a tag of the form X.Y.Z, and sign + the tag with your GPG key (asks for your GPG password, and you + may need to set GIT_COMMITTER_NAME and GIT_COMMITTER_EMAIL to match + your public-key's setting or this fails.) + * Push that tag + * Provide some text for the release announcement (see below). + +6) Send a message to notmuch@notmuchmail.org to announce the release. + + Use the text provided from "make release" above, (if for some + reason you lose this message, "make release-message" prints + it again for you. diff --git a/TODO b/TODO new file mode 100644 index 00000000..8312db10 --- /dev/null +++ b/TODO @@ -0,0 +1,293 @@ +Fix the things that are causing the most pain to new users +---------------------------------------------------------- +1. A new import is tagging all messages as "inbox" -- total pain + +2. Allow an easy way to get tags from directory names (if the user has them) + +Emacs interface (notmuch.el) +---------------------------- +Enhance '+' and '-' in the search view to operate on an entire region +if set. + +Fix '*' to work by simply calling '+' or '-' on a region consisting of +the entire buffer. + +Add a global keybinding table for notmuch, and then view-specific +tables that add to it. + +Add a '|' binding from the search view. + +Add support for choosing from one of the user's configured email +addresses for the From line. + +Make 'notmuch-show-pipe-message have a private history. + +Add support for a delete keybinding that adds a "deleted" tag to the +current message/thread and make searches not return deleted messages +by default, (unless the user asks explicitly for deleted messags in +the search query). + +Add support to "mute" a thread (add a "muted" tag and then don't +display threads in searches by default where any message of the thread +has the "muted" tag). + +Fix i-search to open up invisible citations as necessary. + +Make '=' count from the end rather than from the beginning if more +than half-way through the buffer. + +Fix to automatically wrap long headers (for RFC compliance) before +sending. This should probably just be fixed in message-mode itself, +(but perhaps we can have a notmuch-message-mode that layers this on +top). + +Implement Fcc and use it for all messages, (whether a new composition, +a reply, or a forward). This again may require a notmuch-message-mode +that extends message-mode. + +Stop hiding the headers so much in the thread-view mode. + +Allow opening a message in thread-view mode by clicking on either +line. + +Automatically open a message when navigating to it with N or P. + +Change 'a' command in thread-view mode to only archive open messages. + +Add a binding to open all closed messages. + +Emacs saved-search interface +---------------------------- +Here's a proposal Carl wrote (id:87einafy4u.fsf@yoom.home.cworth.org): + + So what I'm imagining for the default notmuch view is something like + this: + + Welcome to notmuch. + + Notmuch search: _________________________________________ + + Saved searches: + + 55,342 All messages + 22 Inbox + + Recent searches: + + 1 from:"someone special" and tag:unread + 34 tag:notmuch and tag:todo + + Click (or press Enter) on any search to see the results. + Right-click (or press Space) on any recent search to save it. + + So the "saved searches" portion of the view is basically just what + notmuch-folder displays now. Above that there's an obvious place to + start a new search, (in a slightly more "web-browser-like" way than the + typical mini-buffer approach). + + All recent searches appear in the list at the bottom automatically, and + there's the documented mechanism for saving a search, (giving it a name + and having it appear above). + +Portability +----------- +Fix configure script to test each compiler warning we want to use. + +Completion +---------- +Fix bash completion to complete multiple search options (both --first +and *then* --max-threads), and also complete value for --sort= +(oldest-first or newest-first). + +notmuch command-line tool +------------------------- +Fix the --format=json option to not imply --entire-thread. + +Implement "notmuch search --exclude-threads=" 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= +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 " 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: + + 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). + So then I might have a series that looks like this: + notmuch search --output=address_from tag:address_book_alias + notmuch search --output=address_to tag:sent + notmuch search --output=address_from + I think I might like that quite a bit. + 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 " 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 00000000..fa25832e --- /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 00000000..653fc710 --- /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 00000000..cd32c56f --- /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 00000000..173ef680 --- /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 + */ + +/* 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 +#include + +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 00000000..407f3d07 --- /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 + +#include +#include +#include +#include + +#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 00000000..222e0f6c --- /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 + +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 00000000..a8bcd17e --- /dev/null +++ b/compat/have_getline.c @@ -0,0 +1,13 @@ +#define _GNU_SOURCE +#include +#include + +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 00000000..c0fb7629 --- /dev/null +++ b/compat/have_strcasestr.c @@ -0,0 +1,10 @@ +#define _GNU_SOURCE +#include + +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 00000000..62a3a545 --- /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 + +#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 00000000..b6859eac --- /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 00000000..6a6012d4 --- /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 00000000..40a30e5f --- /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 00000000..8665268c --- /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 +# +# Based on "notmuch help" as follows: +# +# Usage: notmuch [args...] +# +# Where and [args...] are as follows: +# +# setup +# +# new +# +# search [options] [...] +# +# show +# +# reply +# +# tag +|- [...] [--] [...] +# +# dump [] +# +# restore +# +# help [] + +_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 00000000..c0d3a446 --- /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 00000000..67a9aba8 --- /dev/null +++ b/completion/notmuch-completion.zsh @@ -0,0 +1,74 @@ +#compdef notmuch + +# ZSH completion for `notmuch` +# Copyright © 2009 Ingmar Vanhassel + +_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 00000000..ec8c3fd3 --- /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 <-- 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: --" + 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 < /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 < /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 < Makefile.config < Thu, 21 Jan 2010 14:50:19 +1300 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..42a9f6d3 --- /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 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 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 Thu, 21 Jan 2010 14:00:54 +1300 + +notmuch (0.0-1) unstable; urgency=low + + * New Debian package. + + -- Jameson Graef Rollins Fri, 27 Nov 2009 13:39:09 -0500 diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000..7f8f011e --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..e4c61ab3 --- /dev/null +++ b/debian/control @@ -0,0 +1,51 @@ +Source: notmuch +Section: mail +Priority: extra +Maintainer: Carl Worth +Uploaders: Jameson Graef Rollins , martin f. krafft +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 00000000..1cc7a155 --- /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 +Debianized-Date: Fri Nov 28 18:00:00 EDT 2009 +Original-Source: git://notmuchmail.org/git/notmuch + +Files: * +Copyright: Copyright 2009 Carl Worth + Bart Trojanowski + Keith Packard + Alexander Botero-Lowry + Ingmar Vanhassel + Jed Brown + Jan Janak + Chris Wilson + Keith Amidon + Aneesh Kumar K.V + Mikhail Gusarov + Jeffrey C. Ollie + Jameson Graef Rollins + Stewart Smith + Adrian Perez + Kan-Ru Chen + James Rowe + Eric Anholt + Alec Berryman + Tassilo Horn + Stefan Schmidt + Rolland Santimano + Peter Wang + Lars Kellogg-Stedman + Holger Freyther + David Bremner + Alexander Botero-Lowry + +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 + martin f. krafft + +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 00000000..dba526f6 --- /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 00000000..185dba4e --- /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 00000000..da4fc25b --- /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 00000000..ce7f2393 --- /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 00000000..98ec1282 --- /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 , borrowing heavily +# from the install scripts for gettext by Santiago Vila +# and octave by Dirk Eddelbuettel . + +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 00000000..c35e8a42 --- /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 00000000..25a527f5 --- /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 00000000..dd310fad --- /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 00000000..a4b526c1 --- /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 00000000..89ae9db8 --- /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 00000000..e8b9378e --- /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 + */ + +#include "notmuch-client.h" + +#include + +#if HAVE_VALGRIND +#include +#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 00000000..b6859eac --- /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 00000000..f759c0dc --- /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 00000000..cb9be301 --- /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 . +;; +;; Authors: Carl Worth + +;; 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 00000000..0d6e7759 --- /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 . +;; +;; Authors: David Bremner + +(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 00000000..916b39e3 --- /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 . +;; +;; Authors: Carl Worth +;; David Edmondson + +(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 00000000..54a380a1 --- /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 . +;; +;; Authors: Carl Worth + +(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 00000000..57b7fcf4 --- /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 . +; +; Authors: Carl Worth + +; 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 (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 "") '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 00000000..2f3df801 --- /dev/null +++ b/gmime-filter-headers.c @@ -0,0 +1,263 @@ +/* + * Copyright © 2009 Keith Packard + * Copyright © 2010 Michal Sojka + * + * 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 +#include +#include +#include +#include + +/** + * 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 00000000..47d1d456 --- /dev/null +++ b/gmime-filter-headers.h @@ -0,0 +1,69 @@ +/* + * Copyright © 2009 Keith Packard + * Copyright © 2010 Michal Sojka + * + * 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 + +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 00000000..b269db4e --- /dev/null +++ b/gmime-filter-reply.c @@ -0,0 +1,209 @@ +/* + * 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, 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 00000000..b7cbc6b1 --- /dev/null +++ b/gmime-filter-reply.h @@ -0,0 +1,66 @@ +/* + * 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, 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 + +G_BEGIN_DECLS + +#define GMIME_TYPE_FILTER_REPLY (g_mime_filter_reply_get_type ()) +#define GMIME_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReply)) +#define GMIME_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass)) +#define GMIME_IS_FILTER_REPLY(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GMIME_TYPE_FILTER_REPLY)) +#define GMIME_IS_FILTER_REPLY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GMIME_TYPE_FILTER_REPLY)) +#define GMIME_FILTER_REPLY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GMIME_TYPE_FILTER_REPLY, GMimeFilterReplyClass)) + +typedef struct _GMimeFilterReply GMimeFilterReply; +typedef struct _GMimeFilterReplyClass GMimeFilterReplyClass; + +/** + * GMimeFilterReply: + * @parent_object: parent #GMimeFilter + * @encode: encoding vs decoding reply markers + * @saw_nl: previous char was a \n + * @saw_angle: previous char was a > + * + * A filter to insert/remove reply markers (lines beginning with >) + **/ +struct _GMimeFilterReply { + GMimeFilter parent_object; + + gboolean encode; + gboolean saw_nl; + gboolean saw_angle; +}; + +struct _GMimeFilterReplyClass { + GMimeFilterClass parent_class; + +}; + + +GType g_mime_filter_reply_get_type (void); + +GMimeFilter *g_mime_filter_reply_new (gboolean encode); + +G_END_DECLS + + +#endif /* _GMIME_FILTER_REPLY_H_ */ diff --git a/json.c b/json.c new file mode 100644 index 00000000..817fc83a --- /dev/null +++ b/json.c @@ -0,0 +1,109 @@ +/* notmuch - Not much of an email program, (just index and search) + * + * Copyright © 2009 Dave Gamble + * Copyright © 2009 Scott Robinson + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/ . + * + * Authors: Dave Gamble + * Scott Robinson + * + */ + +#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 00000000..b6859eac --- /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 00000000..a0920905 --- /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 00000000..41918d76 --- /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 + */ + +#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 + +#include "notmuch-private.h" + +#include + +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 00000000..6842fafa --- /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 + */ + +#include "database-private.h" + +#include + +#include +#include +#include + +#include /* 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-. + * + * 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 (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 (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 (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 (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 (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 (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 00000000..5e75b73e --- /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 + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include + +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 (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 (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 00000000..0d6640bc --- /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 + */ + +#include "notmuch-private.h" + +#include +#include + +#include + +/* 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 00000000..c39a5a17 --- /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 /* 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 00000000..b4dca93b --- /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 + +/* 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 00000000..0c152a39 --- /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 + */ + +#include + +#include "notmuch-private.h" + +#include + +#include /* 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 00000000..721c9a67 --- /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 + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include + +#include + +#include + +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 (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 (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 00000000..db2b7a16 --- /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 + */ + +#include "notmuch-private.h" + +#include + +/* 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 00000000..94cce1bc --- /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 + */ + +#ifndef NOTMUCH_PRIVATE_H +#define NOTMUCH_PRIVATE_H + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* For getline and asprintf */ +#endif +#include + +#include "compat.h" + +#include "notmuch.h" + +NOTMUCH_BEGIN_DECLS + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 00000000..bae48a65 --- /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 + */ + +#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 + +#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__t see the various + * notmuch_ 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 00000000..7e1b6b52 --- /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 + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include /* GHashTable, GPtrArray */ + +#include + +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 00000000..cc481086 --- /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 + */ + +#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 00000000..8fe4a3f0 --- /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 + */ + +#include "notmuch-private.h" + +#include /* 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 00000000..e514bf82 --- /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 + */ + +#include "notmuch-private.h" +#include "database-private.h" + +#include + +#include +#include /* 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:". + **/ + 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 00000000..268225b8 --- /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 + */ + +#include "notmuch-private.h" + +#include + +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 00000000..b973f7dc --- /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 + */ + +#ifndef NOTMUCH_XUTIL_H +#define NOTMUCH_XUTIL_H + +#include +#include +#include + +/* 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 00000000..d36b9ec1 --- /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 + */ + +#ifndef NOTMUCH_CLIENT_H +#define NOTMUCH_CLIENT_H + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE /* for getline */ +#endif +#include + +#include "compat.h" + +#include + +#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 +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 00000000..cc05f6c4 --- /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 + */ + +#include "notmuch-client.h" + +#include +#include + +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 00000000..39f08c6c --- /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 + */ + +#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 00000000..7e7bc177 --- /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 + */ + +#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 00000000..484d9462 --- /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 + */ + +#include "notmuch-client.h" + +#include + +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 00000000..230cacc3 --- /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 + * Keith Packard + */ + +#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 00000000..b0a4e1ce --- /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 + */ + +#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 00000000..6f3cfccb --- /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 + */ + +#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 00000000..4e3514b6 --- /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 + */ + +#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 00000000..622bbaa6 --- /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 + */ + +#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 00000000..76873a1d --- /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 + */ + +#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 00000000..fd54bc78 --- /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 + */ + +#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 00000000..e250c3d5 --- /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 + */ + +#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 00000000..86830f4b --- /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 +.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 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 . +.RE +.TP +.BR show " [options...] ..." + +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:" 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 . +.RE +.TP +.BR count " ..." + +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...] ..." + +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 . + +Note: It is most common to use +.B "notmuch reply" +with a search string matching a single message, (such as +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 " +|\- [...] [\-\-] ..." + +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 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 . +.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 " []" + +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 " " + +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= ..." + +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 . +.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 + indicate user-supplied values): + + from: + + to: + + subject: + + attachment: + + tag: (or is:) + + id: + + thread: + +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: + + .. + +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 . 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 00000000..0eea5e11 --- /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 + * Keith Packard + */ + +#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 indicate user-supplied values):\n" + "\n" + "\t\tfrom:\n" + "\t\tto:\n" + "\t\tsubject:\n" + "\t\tattachment:\n" + "\t\ttag: (or is:)\n" + "\t\tid:\n" + "\t\tthread:\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..\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 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, + " [...]", + "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:\" 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, + " [...]", + "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...] [...]", + "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, + "+|- [...] [--] [...]", + "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 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, + "[]", + "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, + "", + "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, + "[ [...] ]", + "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= ", + "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, + "[]", + "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 [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 \" 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 00000000..f1600473 --- /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 00000000..a5866601 --- /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 00000000..cca3a1bb --- /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 - 0.0-0.3.306635c2 +- First version + diff --git a/query-string.c b/query-string.c new file mode 100644 index 00000000..65365123 --- /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 + */ + +#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 00000000..b1b61be4 --- /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 + * Keith Packard + */ + +#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 00000000..f0c0aad6 --- /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 "' +# '[to]="Some User "' +# '[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 "' +# [reply-to]=some-address +# [in-reply-to]= +# +# Additional values for email headers. If these are not provided +# then the relevant headers will simply not appear in the +# 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 " + fi + + if [ -z "${template[to]}" ]; then + template[to]="Notmuch Test Suite " + 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 <$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 < ${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 "' +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 "' +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 "' \ + [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 +Subject: Re: notmuch-reply-test +To: Sender +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 wrote: +> basic reply test" + +printf " Multiple recipients...\t\t\t\t" +add_message '[from]="Sender "' \ + '[to]="test_suite@notmuchmail.org, Someone Else "' \ + [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 +Subject: Re: notmuch-reply-test +To: Sender , Someone Else +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 wrote: +> Multiple recipients" + +printf " Reply with CC...\t\t\t\t" +add_message '[from]="Sender "' \ + [to]=test_suite@notmuchmail.org \ + '[cc]="Other Parties "' \ + [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 +Subject: Re: notmuch-reply-test +To: Sender +Cc: Other Parties +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 wrote: +> reply with CC" + +printf " Reply from alternate address...\t\t" +add_message '[from]="Sender "' \ + [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 +Subject: Re: notmuch-reply-test +To: Sender +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 wrote: +> reply from alternate address" + +printf " Support for Reply-To...\t\t\t" +add_message '[from]="Sender "' \ + [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 "' + +output=$($NOTMUCH reply id:${gen_msg_id}) +pass_if_equal "$output" "From: Notmuch Test Suite +Subject: Re: notmuch-reply-test +To: Sender +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 wrote: +> support for reply-to" + +printf " Un-munging Reply-To...\t\t\t\t" +add_message '[from]="Sender "' \ + '[to]="Some List "' \ + [subject]=notmuch-reply-test \ + '[date]="Tue, 05 Jan 2010 15:43:56 -0800"' \ + '[body]="Un-munging Reply-To"' \ + '[reply-to]="Evil Munging List "' + +output=$($NOTMUCH reply id:${gen_msg_id}) +pass_if_equal "$output" "From: Notmuch Test Suite +Subject: Re: notmuch-reply-test +To: Sender , Some List +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 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]=\' [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]=\' [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(NOTMUCH_NEW) +pass_if_equal "$output" "Added 1 new message to the database." +printf " Searching returns both messages in one thread..." +output=$($NOTMUCH search foo | notmuch_search_sanitize) +pass_if_equal "$output" "thread:XXX 2000-01-01 [2/2] Notmuch Test Suite; brokenthreadtest (inbox unread)" +printf " Adding parent message...\t\t\t" +generate_message [body]=foo [id]=parent-id [subject]=brokenthreadtest '[date]="Sat, 01 Jan 2000 12:00:00 -0000"' +output=$(NOTMUCH_NEW) +pass_if_equal "$output" "Added 1 new message to the database." +printf " Searching returns all three messages in one thread..." +output=$($NOTMUCH search foo | notmuch_search_sanitize) +pass_if_equal "$output" "thread:XXX 2000-01-01 [3/3] Notmuch Test Suite; brokenthreadtest (inbox unread)" + +echo "" +echo "Notmuch test suite complete." + +if [ "$test_failures" = "0" ]; then + echo "All $tests tests passed." + rm -rf ${TEST_DIR} +else + echo "$test_failures/$tests tests failed. The failures can be investigated in:" + echo "${TEST_DIR}" +fi + +echo "" + +exit $test_failures diff --git a/version b/version new file mode 100644 index 00000000..3b04cfb6 --- /dev/null +++ b/version @@ -0,0 +1 @@ +0.2 diff --git a/vim/Makefile b/vim/Makefile new file mode 100644 index 00000000..89e18be1 --- /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 00000000..8cd3b1a2 --- /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. + +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: + - 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: + - show the selected thread colapsing unmatched items + - 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: + - 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: + - go to the next header line + + Normal-mode keybindings: + - 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 00000000..3d8422c8 --- /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 00000000..a9754f2b --- /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 . +" +" Authors: Bart Trojanowski + +" --- 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 NM_new_mail()', + \ 's': ':call NM_search_prompt()', + \ 'q': ':call NM_kill_this_buffer()', + \ '=': ':call NM_folders_refresh_view()', + \ '': ':call NM_folders_show_search()', + \ } + +" --- --- bindings for search screen {{{2 +let g:notmuch_search_maps = { + \ '': ':call NM_search_show_thread(0)', + \ '': ':call NM_search_show_thread(1)', + \ '': ':call NM_search_expand('''')', + \ 'a': ':call NM_search_archive_thread()', + \ 'f': ':call NM_search_filter()', + \ 'm': ':call NM_new_mail()', + \ 'o': ':call NM_search_toggle_order()', + \ 'r': ':call NM_search_reply_to_thread()', + \ 's': ':call NM_search_prompt()', + \ ',s': ':call NM_search_edit()', + \ 't': ':call NM_search_filter_by_tag()', + \ 'q': ':call NM_kill_this_buffer()', + \ '+': ':call NM_search_add_tags([])', + \ '-': ':call NM_search_remove_tags([])', + \ '=': ':call NM_search_refresh_view()', + \ '?': ':echo NM_search_thread_id() . '' @ '' . join(NM_get_search_words())', + \ } + +" --- --- bindings for show screen {{{2 +let g:notmuch_show_maps = { + \ '': ':call NM_show_previous(1, 0)', + \ '': ':call NM_show_next(1, 0)', + \ '': ':call NM_search_expand('''')', + \ 'q': ':call NM_kill_this_buffer()', + \ 's': ':call NM_search_prompt()', + \ + \ 'b': ':call NM_show_fold_toggle(''b'', ''bdy'', !g:notmuch_show_fold_bodies)', + \ 'c': ':call NM_show_fold_toggle(''c'', ''cit'', !g:notmuch_show_fold_citations)', + \ 'h': ':call NM_show_fold_toggle(''h'', ''hdr'', !g:notmuch_show_fold_headers)', + \ 'i': ':call NM_show_fold_toggle(''s'', ''sig'', !g:notmuch_show_fold_signatures)', + \ + \ 'a': ':call NM_show_archive_thread()', + \ 'A': ':call NM_show_mark_read_then_archive_thread()', + \ 'N': ':call NM_show_mark_read_then_next_open_message()', + \ 'v': ':call NM_show_view_all_mime_parts()', + \ '+': ':call NM_show_add_tag()', + \ '-': ':call NM_show_remove_tag()', + \ '': ':call NM_show_advance_marking_read_and_archiving()', + \ '\|': ':call NM_show_pipe_message()', + \ + \ '': ':call NM_show_previous_fold()', + \ '': ':call NM_show_next_fold()', + \ '': ':call NM_show_toggle_fold()', + \ + \ 'r': ':call NM_show_reply()', + \ 'm': ':call NM_new_mail()', + \ '?': ':echo NM_show_message_id() . '' @ '' . join(NM_get_search_words())', + \ } + +" --- --- bindings for compose screen {{{2 +let g:notmuch_compose_nmaps = { + \ ',s': ':call NM_compose_send()', + \ ',a': ':call NM_compose_attach()', + \ ',q': ':call NM_kill_this_buffer()', + \ '': ':call NM_compose_next_entry_area()', + \ } +let g:notmuch_compose_imaps = { + \ '': '=NM_compose_next_entry_area()', + \ } + +" --- 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 NM_newBuffer('', 'folders', join(disp, "\n")) + let b:nm_searches = searches + let b:nm_timestamp = reltime() + + call NM_cmd_folders_mksyntax() + call 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 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 NM_newBuffer('', 'search', join(disp, "\n")) + let b:nm_raw_lines = lines + let b:nm_search_words = a:words + + call NM_cmd_search_mksyntax() + call 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 = [ 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 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 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 NM_cmd_search(split(text)) + endif +endfunction + +function! s:NM_search_archive_thread() + call NM_add_remove_tags_on_screen('', '-', ['inbox']) + call NM_add_remove_tags([], '-', ['inbox']) + norm j +endfunction + +function! s:NM_search_filter() + call NM_search_filter_helper('Filter: ', '', '') +endfunction + +function! s:NM_search_filter_by_tag() + call 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'] + \ + NM_combine_tags(a:prefix, split(text), a:joiner, '()') + + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call 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 NM_search_refresh_view() +endfunction + +function! s:NM_search_reply_to_thread() + let cmd = ['reply'] + call add(cmd, NM_search_thread_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call NM_newComposeBuffer(lines, 0) +endfunction + +function! s:NM_search_add_tags(tags) + call NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags) +endfunction + +function! s:NM_search_remove_tags(tags) + call 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 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 NM_add_remove_tags([], a:prefix, tags) + call 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 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 NM_cmd_show_mkfolds() + call NM_cmd_show_mksyntax() + call 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 NM_kill_this_buffer() + if line('.') > 1 + norm k + call NM_search_show_thread(everything) + norm G + call 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 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 NM_kill_this_buffer() + if line('.') != line('$') + norm j + call 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, NM_show_message_id()) + call add(cmd, 'AND') + call extend(cmd, NM_get_search_words()) + + let data = NM_run(cmd) + let lines = split(data, "\n") + call 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 = 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 = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND'] + \ + NM_combine_tags('', ids, 'OR', '()') + call NM_add_remove_tags(filter, '-', advance_tags) + call NM_show_next(1, 1) + return + endif + + let msg_bot = 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 NM_add_remove_tags_on_screen(msg_top['start'], '-', advance_tags) + exec printf('norm %dG', vis_top) + call 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 = NM_combine_tags('tag:', advance_tags, 'OR', '()') + \ + ['AND', msg_top['id']] + call 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 = 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 %s :call NM_show_fold_toggle(''%s'', ''%s'', %d)', 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 = 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 NM_newComposeBuffer(lines, start_on_line) +endfunction + +function! s:NM_compose_send() + call 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 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 = 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 = 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: - 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 NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mail', + \ 'compose', lines) + setlocal bufhidden=hide + let b:nm_prev_bufnr = prev_bufnr + + call NM_set_map('n', g:notmuch_compose_nmaps) + call 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 NM_compose_next_entry_area() + endif + + if g:notmuch_compose_insert_mode_start + startinsert! + endif + echo 'Type your message, use 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 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 to quit." + endif +endfunction + +function! s:NM_search_expand(arg) + let word = expand(a:arg) + let prev_bufnr = bufnr('%') + setlocal bufhidden=hide + call 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 : [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 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 %s %s', a:type, key, code) + endfor + " --- this is a hack for development :) + nnoremap ,nmr :runtime! plugin/notmuch.vim +endfunction + +" --- command handler {{{1 + +function! NotMuch(args) + let args = a:args + if !strlen(args) + let args = 'folders' + endif + + let words = NM_shell_split(args) + if words[0] == 'folders' || words[0] == 'f' + let words = words[1:] + call 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 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 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() +cabbrev notmuch =(getcmdtype()==':' && getcmdpos()==1 ? 'NotMuch' : 'notmuch') + +" 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 00000000..19adb756 --- /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 00000000..9477f86f --- /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 00000000..71839fd9 --- /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 00000000..20bcc399 --- /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