├── .gitignore ├── AUTHORS ├── COPYING ├── COPYING-GPL-3 ├── INSTALL ├── ISSUES ├── Makefile ├── README.MUTT.md ├── README.md ├── mutt ├── .muttrc ├── README └── bin │ ├── notmuch_tag │ └── prompt_mkdir ├── notmuchfs.c ├── tests ├── include └── main.sh └── version /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.dep 3 | notmuchfs 4 | tags 5 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Notmuchfs is completely the work of Tim Stoakes . 2 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Notmuchfs is free software. 2 | 3 | You can redistribute it and/or modify it under the terms of the GNU 4 | General Public License as published by the Free Software Foundation, 5 | either version 3 of the License, or (at your option) any later 6 | version. 7 | 8 | This program is distributed in the hope that it will be useful, but 9 | WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program, (in the COPYING-GPL-3 file in this 15 | directory). If not, see http://www.gnu.org/licenses/ 16 | -------------------------------------------------------------------------------- /COPYING-GPL-3: -------------------------------------------------------------------------------- 1 | 2 | GNU GENERAL PUBLIC LICENSE 3 | Version 3, 29 June 2007 4 | 5 | Copyright (C) 2007 Free Software Foundation, Inc. 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The GNU General Public License is a free, copyleft license for 12 | software and other kinds of works. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | the GNU General Public License is intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. We, the Free Software Foundation, use the 19 | GNU General Public License for most of our software; it applies also to 20 | any other work released this way by its authors. You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not 24 | price. Our General Public Licenses are designed to make sure that you 25 | have the freedom to distribute copies of free software (and charge for 26 | them if you wish), that you receive source code or can get it if you 27 | want it, that you can change the software or use pieces of it in new 28 | free programs, and that you know you can do these things. 29 | 30 | To protect your rights, we need to prevent others from denying you 31 | these rights or asking you to surrender the rights. Therefore, you have 32 | certain responsibilities if you distribute copies of the software, or if 33 | you modify it: responsibilities to respect the freedom of others. 34 | 35 | For example, if you distribute copies of such a program, whether 36 | gratis or for a fee, you must pass on to the recipients the same 37 | freedoms that you received. You must make sure that they, too, receive 38 | or can get the source code. And you must show them these terms so they 39 | know their rights. 40 | 41 | Developers that use the GNU GPL protect your rights with two steps: 42 | (1) assert copyright on the software, and (2) offer you this License 43 | giving you legal permission to copy, distribute and/or modify it. 44 | 45 | For the developers' and authors' protection, the GPL clearly explains 46 | that there is no warranty for this free software. For both users' and 47 | authors' sake, the GPL requires that modified versions be marked as 48 | changed, so that their problems will not be attributed erroneously to 49 | authors of previous versions. 50 | 51 | Some devices are designed to deny users access to install or run 52 | modified versions of the software inside them, although the manufacturer 53 | can do so. This is fundamentally incompatible with the aim of 54 | protecting users' freedom to change the software. The systematic 55 | pattern of such abuse occurs in the area of products for individuals to 56 | use, which is precisely where it is most unacceptable. Therefore, we 57 | have designed this version of the GPL to prohibit the practice for those 58 | products. If such problems arise substantially in other domains, we 59 | stand ready to extend this provision to those domains in future versions 60 | of the GPL, as needed to protect the freedom of users. 61 | 62 | Finally, every program is threatened constantly by software patents. 63 | States should not allow patents to restrict development and use of 64 | software on general-purpose computers, but in those that do, we wish to 65 | avoid the special danger that patents applied to a free program could 66 | make it effectively proprietary. To prevent this, the GPL assures that 67 | patents cannot be used to render the program non-free. 68 | 69 | The precise terms and conditions for copying, distribution and 70 | modification follow. 71 | 72 | TERMS AND CONDITIONS 73 | 74 | 0. Definitions. 75 | 76 | "This License" refers to version 3 of the GNU General Public License. 77 | 78 | "Copyright" also means copyright-like laws that apply to other kinds of 79 | works, such as semiconductor masks. 80 | 81 | "The Program" refers to any copyrightable work licensed under this 82 | License. Each licensee is addressed as "you". "Licensees" and 83 | "recipients" may be individuals or organizations. 84 | 85 | To "modify" a work means to copy from or adapt all or part of the work 86 | in a fashion requiring copyright permission, other than the making of an 87 | exact copy. The resulting work is called a "modified version" of the 88 | earlier work or a work "based on" the earlier work. 89 | 90 | A "covered work" means either the unmodified Program or a work based 91 | on the Program. 92 | 93 | To "propagate" a work means to do anything with it that, without 94 | permission, would make you directly or secondarily liable for 95 | infringement under applicable copyright law, except executing it on a 96 | computer or modifying a private copy. Propagation includes copying, 97 | distribution (with or without modification), making available to the 98 | public, and in some countries other activities as well. 99 | 100 | To "convey" a work means any kind of propagation that enables other 101 | parties to make or receive copies. Mere interaction with a user through 102 | a computer network, with no transfer of a copy, is not conveying. 103 | 104 | An interactive user interface displays "Appropriate Legal Notices" 105 | to the extent that it includes a convenient and prominently visible 106 | feature that (1) displays an appropriate copyright notice, and (2) 107 | tells the user that there is no warranty for the work (except to the 108 | extent that warranties are provided), that licensees may convey the 109 | work under this License, and how to view a copy of this License. If 110 | the interface presents a list of user commands or options, such as a 111 | menu, a prominent item in the list meets this criterion. 112 | 113 | 1. Source Code. 114 | 115 | The "source code" for a work means the preferred form of the work 116 | for making modifications to it. "Object code" means any non-source 117 | form of a work. 118 | 119 | A "Standard Interface" means an interface that either is an official 120 | standard defined by a recognized standards body, or, in the case of 121 | interfaces specified for a particular programming language, one that 122 | is widely used among developers working in that language. 123 | 124 | The "System Libraries" of an executable work include anything, other 125 | than the work as a whole, that (a) is included in the normal form of 126 | packaging a Major Component, but which is not part of that Major 127 | Component, and (b) serves only to enable use of the work with that 128 | Major Component, or to implement a Standard Interface for which an 129 | implementation is available to the public in source code form. A 130 | "Major Component", in this context, means a major essential component 131 | (kernel, window system, and so on) of the specific operating system 132 | (if any) on which the executable work runs, or a compiler used to 133 | produce the work, or an object code interpreter used to run it. 134 | 135 | The "Corresponding Source" for a work in object code form means all 136 | the source code needed to generate, install, and (for an executable 137 | work) run the object code and to modify the work, including scripts to 138 | control those activities. However, it does not include the work's 139 | System Libraries, or general-purpose tools or generally available free 140 | programs which are used unmodified in performing those activities but 141 | which are not part of the work. For example, Corresponding Source 142 | includes interface definition files associated with source files for 143 | the work, and the source code for shared libraries and dynamically 144 | linked subprograms that the work is specifically designed to require, 145 | such as by intimate data communication or control flow between those 146 | subprograms and other parts of the work. 147 | 148 | The Corresponding Source need not include anything that users 149 | can regenerate automatically from other parts of the Corresponding 150 | Source. 151 | 152 | The Corresponding Source for a work in source code form is that 153 | same work. 154 | 155 | 2. Basic Permissions. 156 | 157 | All rights granted under this License are granted for the term of 158 | copyright on the Program, and are irrevocable provided the stated 159 | conditions are met. This License explicitly affirms your unlimited 160 | permission to run the unmodified Program. The output from running a 161 | covered work is covered by this License only if the output, given its 162 | content, constitutes a covered work. This License acknowledges your 163 | rights of fair use or other equivalent, as provided by copyright law. 164 | 165 | You may make, run and propagate covered works that you do not 166 | convey, without conditions so long as your license otherwise remains 167 | in force. You may convey covered works to others for the sole purpose 168 | of having them make modifications exclusively for you, or provide you 169 | with facilities for running those works, provided that you comply with 170 | the terms of this License in conveying all material for which you do 171 | not control copyright. Those thus making or running the covered works 172 | for you must do so exclusively on your behalf, under your direction 173 | and control, on terms that prohibit them from making any copies of 174 | your copyrighted material outside their relationship with you. 175 | 176 | Conveying under any other circumstances is permitted solely under 177 | the conditions stated below. Sublicensing is not allowed; section 10 178 | makes it unnecessary. 179 | 180 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 181 | 182 | No covered work shall be deemed part of an effective technological 183 | measure under any applicable law fulfilling obligations under article 184 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 185 | similar laws prohibiting or restricting circumvention of such 186 | measures. 187 | 188 | When you convey a covered work, you waive any legal power to forbid 189 | circumvention of technological measures to the extent such circumvention 190 | is effected by exercising rights under this License with respect to 191 | the covered work, and you disclaim any intention to limit operation or 192 | modification of the work as a means of enforcing, against the work's 193 | users, your or third parties' legal rights to forbid circumvention of 194 | technological measures. 195 | 196 | 4. Conveying Verbatim Copies. 197 | 198 | You may convey verbatim copies of the Program's source code as you 199 | receive it, in any medium, provided that you conspicuously and 200 | appropriately publish on each copy an appropriate copyright notice; 201 | keep intact all notices stating that this License and any 202 | non-permissive terms added in accord with section 7 apply to the code; 203 | keep intact all notices of the absence of any warranty; and give all 204 | recipients a copy of this License along with the Program. 205 | 206 | You may charge any price or no price for each copy that you convey, 207 | and you may offer support or warranty protection for a fee. 208 | 209 | 5. Conveying Modified Source Versions. 210 | 211 | You may convey a work based on the Program, or the modifications to 212 | produce it from the Program, in the form of source code under the 213 | terms of section 4, provided that you also meet all of these conditions: 214 | 215 | a) The work must carry prominent notices stating that you modified 216 | it, and giving a relevant date. 217 | 218 | b) The work must carry prominent notices stating that it is 219 | released under this License and any conditions added under section 220 | 7. This requirement modifies the requirement in section 4 to 221 | "keep intact all notices". 222 | 223 | c) You must license the entire work, as a whole, under this 224 | License to anyone who comes into possession of a copy. This 225 | License will therefore apply, along with any applicable section 7 226 | additional terms, to the whole of the work, and all its parts, 227 | regardless of how they are packaged. This License gives no 228 | permission to license the work in any other way, but it does not 229 | invalidate such permission if you have separately received it. 230 | 231 | d) If the work has interactive user interfaces, each must display 232 | Appropriate Legal Notices; however, if the Program has interactive 233 | interfaces that do not display Appropriate Legal Notices, your 234 | work need not make them do so. 235 | 236 | A compilation of a covered work with other separate and independent 237 | works, which are not by their nature extensions of the covered work, 238 | and which are not combined with it such as to form a larger program, 239 | in or on a volume of a storage or distribution medium, is called an 240 | "aggregate" if the compilation and its resulting copyright are not 241 | used to limit the access or legal rights of the compilation's users 242 | beyond what the individual works permit. Inclusion of a covered work 243 | in an aggregate does not cause this License to apply to the other 244 | parts of the aggregate. 245 | 246 | 6. Conveying Non-Source Forms. 247 | 248 | You may convey a covered work in object code form under the terms 249 | of sections 4 and 5, provided that you also convey the 250 | machine-readable Corresponding Source under the terms of this License, 251 | in one of these ways: 252 | 253 | a) Convey the object code in, or embodied in, a physical product 254 | (including a physical distribution medium), accompanied by the 255 | Corresponding Source fixed on a durable physical medium 256 | customarily used for software interchange. 257 | 258 | b) Convey the object code in, or embodied in, a physical product 259 | (including a physical distribution medium), accompanied by a 260 | written offer, valid for at least three years and valid for as 261 | long as you offer spare parts or customer support for that product 262 | model, to give anyone who possesses the object code either (1) a 263 | copy of the Corresponding Source for all the software in the 264 | product that is covered by this License, on a durable physical 265 | medium customarily used for software interchange, for a price no 266 | more than your reasonable cost of physically performing this 267 | conveying of source, or (2) access to copy the 268 | Corresponding Source from a network server at no charge. 269 | 270 | c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | 276 | d) Convey the object code by offering access from a designated 277 | place (gratis or for a charge), and offer equivalent access to the 278 | Corresponding Source in the same way through the same place at no 279 | further charge. You need not require recipients to copy the 280 | Corresponding Source along with the object code. If the place to 281 | copy the object code is a network server, the Corresponding Source 282 | may be on a different server (operated by you or a third party) 283 | that supports equivalent copying facilities, provided you maintain 284 | clear directions next to the object code saying where to find the 285 | Corresponding Source. Regardless of what server hosts the 286 | Corresponding Source, you remain obligated to ensure that it is 287 | available for as long as needed to satisfy these requirements. 288 | 289 | e) Convey the object code using peer-to-peer transmission, provided 290 | you inform other peers where the object code and Corresponding 291 | Source of the work are being offered to the general public at no 292 | charge under subsection 6d. 293 | 294 | A separable portion of the object code, whose source code is excluded 295 | from the Corresponding Source as a System Library, need not be 296 | included in conveying the object code work. 297 | 298 | A "User Product" is either (1) a "consumer product", which means any 299 | tangible personal property which is normally used for personal, family, 300 | or household purposes, or (2) anything designed or sold for incorporation 301 | into a dwelling. In determining whether a product is a consumer product, 302 | doubtful cases shall be resolved in favor of coverage. For a particular 303 | product received by a particular user, "normally used" refers to a 304 | typical or common use of that class of product, regardless of the status 305 | of the particular user or of the way in which the particular user 306 | actually uses, or expects or is expected to use, the product. A product 307 | is a consumer product regardless of whether the product has substantial 308 | commercial, industrial or non-consumer uses, unless such uses represent 309 | the only significant mode of use of the product. 310 | 311 | "Installation Information" for a User Product means any methods, 312 | procedures, authorization keys, or other information required to install 313 | and execute modified versions of a covered work in that User Product from 314 | a modified version of its Corresponding Source. The information must 315 | suffice to ensure that the continued functioning of the modified object 316 | code is in no case prevented or interfered with solely because 317 | modification has been made. 318 | 319 | If you convey an object code work under this section in, or with, or 320 | specifically for use in, a User Product, and the conveying occurs as 321 | part of a transaction in which the right of possession and use of the 322 | User Product is transferred to the recipient in perpetuity or for a 323 | fixed term (regardless of how the transaction is characterized), the 324 | Corresponding Source conveyed under this section must be accompanied 325 | by the Installation Information. But this requirement does not apply 326 | if neither you nor any third party retains the ability to install 327 | modified object code on the User Product (for example, the work has 328 | been installed in ROM). 329 | 330 | The requirement to provide Installation Information does not include a 331 | requirement to continue to provide support service, warranty, or updates 332 | for a work that has been modified or installed by the recipient, or for 333 | the User Product in which it has been modified or installed. Access to a 334 | network may be denied when the modification itself materially and 335 | adversely affects the operation of the network or violates the rules and 336 | protocols for communication across the network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders of 364 | that material) supplement the terms of this License with terms: 365 | 366 | a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | 369 | b) Requiring preservation of specified reasonable legal notices or 370 | author attributions in that material or in the Appropriate Legal 371 | Notices displayed by works containing it; or 372 | 373 | c) Prohibiting misrepresentation of the origin of that material, or 374 | requiring that modified versions of such material be marked in 375 | reasonable ways as different from the original version; or 376 | 377 | d) Limiting the use for publicity purposes of names of licensors or 378 | authors of the material; or 379 | 380 | e) Declining to grant rights under trademark law for use of some 381 | trade names, trademarks, or service marks; or 382 | 383 | f) Requiring indemnification of licensors and authors of that 384 | material by anyone who conveys the material (or modified versions of 385 | it) with contractual assumptions of liability to the recipient, for 386 | any liability that these contractual assumptions directly impose on 387 | those licensors and authors. 388 | 389 | All other non-permissive additional terms are considered "further 390 | restrictions" within the meaning of section 10. If the Program as you 391 | received it, or any part of it, contains a notice stating that it is 392 | governed by this License along with a term that is a further 393 | restriction, you may remove that term. If a license document contains 394 | a further restriction but permits relicensing or conveying under this 395 | License, you may add to a covered work material governed by the terms 396 | of that license document, provided that the further restriction does 397 | not survive such relicensing or conveying. 398 | 399 | If you add terms to a covered work in accord with this section, you 400 | must place, in the relevant source files, a statement of the 401 | additional terms that apply to those files, or a notice indicating 402 | where to find the applicable terms. 403 | 404 | Additional terms, permissive or non-permissive, may be stated in the 405 | form of a separately written license, or stated as exceptions; 406 | the above requirements apply either way. 407 | 408 | 8. Termination. 409 | 410 | You may not propagate or modify a covered work except as expressly 411 | provided under this License. Any attempt otherwise to propagate or 412 | modify it is void, and will automatically terminate your rights under 413 | this License (including any patent licenses granted under the third 414 | paragraph of section 11). 415 | 416 | However, if you cease all violation of this License, then your 417 | license from a particular copyright holder is reinstated (a) 418 | provisionally, unless and until the copyright holder explicitly and 419 | finally terminates your license, and (b) permanently, if the copyright 420 | holder fails to notify you of the violation by some reasonable means 421 | prior to 60 days after the cessation. 422 | 423 | Moreover, your license from a particular copyright holder is 424 | reinstated permanently if the copyright holder notifies you of the 425 | violation by some reasonable means, this is the first time you have 426 | received notice of violation of this License (for any work) from that 427 | copyright holder, and you cure the violation prior to 30 days after 428 | your receipt of the notice. 429 | 430 | Termination of your rights under this section does not terminate the 431 | licenses of parties who have received copies or rights from you under 432 | this License. If your rights have been terminated and not permanently 433 | reinstated, you do not qualify to receive new licenses for the same 434 | material under section 10. 435 | 436 | 9. Acceptance Not Required for Having Copies. 437 | 438 | You are not required to accept this License in order to receive or 439 | run a copy of the Program. Ancillary propagation of a covered work 440 | occurring solely as a consequence of using peer-to-peer transmission 441 | to receive a copy likewise does not require acceptance. However, 442 | nothing other than this License grants you permission to propagate or 443 | modify any covered work. These actions infringe copyright if you do 444 | not accept this License. Therefore, by modifying or propagating a 445 | covered work, you indicate your acceptance of this License to do so. 446 | 447 | 10. Automatic Licensing of Downstream Recipients. 448 | 449 | Each time you convey a covered work, the recipient automatically 450 | receives a license from the original licensors, to run, modify and 451 | propagate that work, subject to this License. You are not responsible 452 | for enforcing compliance by third parties with this License. 453 | 454 | An "entity transaction" is a transaction transferring control of an 455 | organization, or substantially all assets of one, or subdividing an 456 | organization, or merging organizations. If propagation of a covered 457 | work results from an entity transaction, each party to that 458 | transaction who receives a copy of the work also receives whatever 459 | licenses to the work the party's predecessor in interest had or could 460 | give under the previous paragraph, plus a right to possession of the 461 | Corresponding Source of the work from the predecessor in interest, if 462 | the predecessor has it or can get it with reasonable efforts. 463 | 464 | You may not impose any further restrictions on the exercise of the 465 | rights granted or affirmed under this License. For example, you may 466 | not impose a license fee, royalty, or other charge for exercise of 467 | rights granted under this License, and you may not initiate litigation 468 | (including a cross-claim or counterclaim in a lawsuit) alleging that 469 | any patent claim is infringed by making, using, selling, offering for 470 | sale, or importing the Program or any portion of it. 471 | 472 | 11. Patents. 473 | 474 | A "contributor" is a copyright holder who authorizes use under this 475 | License of the Program or a work on which the Program is based. The 476 | work thus licensed is called the contributor's "contributor version". 477 | 478 | A contributor's "essential patent claims" are all patent claims 479 | owned or controlled by the contributor, whether already acquired or 480 | hereafter acquired, that would be infringed by some manner, permitted 481 | by this License, of making, using, or selling its contributor version, 482 | but do not include claims that would be infringed only as a 483 | consequence of further modification of the contributor version. For 484 | purposes of this definition, "control" includes the right to grant 485 | patent sublicenses in a manner consistent with the requirements of 486 | this License. 487 | 488 | Each contributor grants you a non-exclusive, worldwide, royalty-free 489 | patent license under the contributor's essential patent claims, to 490 | make, use, sell, offer for sale, import and otherwise run, modify and 491 | propagate the contents of its contributor version. 492 | 493 | In the following three paragraphs, a "patent license" is any express 494 | agreement or commitment, however denominated, not to enforce a patent 495 | (such as an express permission to practice a patent or covenant not to 496 | sue for patent infringement). To "grant" such a patent license to a 497 | party means to make such an agreement or commitment not to enforce a 498 | patent against the party. 499 | 500 | If you convey a covered work, knowingly relying on a patent license, 501 | and the Corresponding Source of the work is not available for anyone 502 | to copy, free of charge and under the terms of this License, through a 503 | publicly available network server or other readily accessible means, 504 | then you must either (1) cause the Corresponding Source to be so 505 | available, or (2) arrange to deprive yourself of the benefit of the 506 | patent license for this particular work, or (3) arrange, in a manner 507 | consistent with the requirements of this License, to extend the patent 508 | license to downstream recipients. "Knowingly relying" means you have 509 | actual knowledge that, but for the patent license, your conveying the 510 | covered work in a country, or your recipient's use of the covered work 511 | in a country, would infringe one or more identifiable patents in that 512 | country that you have reason to believe are valid. 513 | 514 | If, pursuant to or in connection with a single transaction or 515 | arrangement, you convey, or propagate by procuring conveyance of, a 516 | covered work, and grant a patent license to some of the parties 517 | receiving the covered work authorizing them to use, propagate, modify 518 | or convey a specific copy of the covered work, then the patent license 519 | you grant is automatically extended to all recipients of the covered 520 | work and works based on it. 521 | 522 | A patent license is "discriminatory" if it does not include within 523 | the scope of its coverage, prohibits the exercise of, or is 524 | conditioned on the non-exercise of one or more of the rights that are 525 | specifically granted under this License. You may not convey a covered 526 | work if you are a party to an arrangement with a third party that is 527 | in the business of distributing software, under which you make payment 528 | to the third party based on the extent of your activity of conveying 529 | the work, and under which the third party grants, to any of the 530 | parties who would receive the covered work from you, a discriminatory 531 | patent license (a) in connection with copies of the covered work 532 | conveyed by you (or copies made from those copies), or (b) primarily 533 | for and in connection with specific products or compilations that 534 | contain the covered work, unless you entered into that arrangement, 535 | or that patent license was granted, prior to 28 March 2007. 536 | 537 | Nothing in this License shall be construed as excluding or limiting 538 | any implied license or other defenses to infringement that may 539 | otherwise be available to you under applicable patent law. 540 | 541 | 12. No Surrender of Others' Freedom. 542 | 543 | If conditions are imposed on you (whether by court order, agreement or 544 | otherwise) that contradict the conditions of this License, they do not 545 | excuse you from the conditions of this License. If you cannot convey a 546 | covered work so as to satisfy simultaneously your obligations under this 547 | License and any other pertinent obligations, then as a consequence you may 548 | not convey it at all. For example, if you agree to terms that obligate you 549 | to collect a royalty for further conveying from those to whom you convey 550 | the Program, the only way you could satisfy both those terms and this 551 | License would be to refrain entirely from conveying the Program. 552 | 553 | 13. Use with the GNU Affero General Public License. 554 | 555 | Notwithstanding any other provision of this License, you have 556 | permission to link or combine any covered work with a work licensed 557 | under version 3 of the GNU Affero General Public License into a single 558 | combined work, and to convey the resulting work. The terms of this 559 | License will continue to apply to the part which is the covered work, 560 | but the special requirements of the GNU Affero General Public License, 561 | section 13, concerning interaction through a network will apply to the 562 | combination as such. 563 | 564 | 14. Revised Versions of this License. 565 | 566 | The Free Software Foundation may publish revised and/or new versions of 567 | the GNU General Public License from time to time. Such new versions will 568 | be similar in spirit to the present version, but may differ in detail to 569 | address new problems or concerns. 570 | 571 | Each version is given a distinguishing version number. If the 572 | Program specifies that a certain numbered version of the GNU General 573 | Public License "or any later version" applies to it, you have the 574 | option of following the terms and conditions either of that numbered 575 | version or of any later version published by the Free Software 576 | Foundation. If the Program does not specify a version number of the 577 | GNU General Public License, you may choose any version ever published 578 | by the Free Software Foundation. 579 | 580 | If the Program specifies that a proxy can decide which future 581 | versions of the GNU General Public License can be used, that proxy's 582 | public statement of acceptance of a version permanently authorizes you 583 | to choose that version for the Program. 584 | 585 | Later license versions may give you additional or different 586 | permissions. However, no additional obligations are imposed on any 587 | author or copyright holder as a result of your choosing to follow a 588 | later version. 589 | 590 | 15. Disclaimer of Warranty. 591 | 592 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 593 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 594 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 595 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 596 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 597 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 598 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 599 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 600 | 601 | 16. Limitation of Liability. 602 | 603 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 604 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 605 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 606 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 607 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 608 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 609 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 610 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 611 | SUCH DAMAGES. 612 | 613 | 17. Interpretation of Sections 15 and 16. 614 | 615 | If the disclaimer of warranty and limitation of liability provided 616 | above cannot be given local legal effect according to their terms, 617 | reviewing courts shall apply local law that most closely approximates 618 | an absolute waiver of all civil liability in connection with the 619 | Program, unless a warranty or assumption of liability accompanies a 620 | copy of the Program in return for a fee. 621 | 622 | END OF TERMS AND CONDITIONS 623 | 624 | How to Apply These Terms to Your New Programs 625 | 626 | If you develop a new program, and you want it to be of the greatest 627 | possible use to the public, the best way to achieve this is to make it 628 | free software which everyone can redistribute and change under these terms. 629 | 630 | To do so, attach the following notices to the program. It is safest 631 | to attach them to the start of each source file to most effectively 632 | state the exclusion of warranty; and each file should have at least 633 | the "copyright" line and a pointer to where the full notice is found. 634 | 635 | 636 | Copyright (C) 637 | 638 | This program is free software: you can redistribute it and/or modify 639 | it under the terms of the GNU General Public License as published by 640 | the Free Software Foundation, either version 3 of the License, or 641 | (at your option) any later version. 642 | 643 | This program is distributed in the hope that it will be useful, 644 | but WITHOUT ANY WARRANTY; without even the implied warranty of 645 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 646 | GNU General Public License for more details. 647 | 648 | You should have received a copy of the GNU General Public License 649 | along with this program. If not, see . 650 | 651 | Also add information on how to contact you by electronic and paper mail. 652 | 653 | If the program does terminal interaction, make it output a short 654 | notice like this when it starts in an interactive mode: 655 | 656 | Copyright (C) 657 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 658 | This is free software, and you are welcome to redistribute it 659 | under certain conditions; type `show c' for details. 660 | 661 | The hypothetical commands `show w' and `show c' should show the appropriate 662 | parts of the General Public License. Of course, your program's commands 663 | might be different; for a GUI interface, you would use an "about box". 664 | 665 | You should also get your employer (if you work as a programmer) or school, 666 | if any, to sign a "copyright disclaimer" for the program, if necessary. 667 | For more information on this, and how to apply and follow the GNU GPL, see 668 | . 669 | 670 | The GNU General Public License does not permit incorporating your program 671 | into proprietary programs. If your program is a subroutine library, you 672 | may consider it more useful to permit linking proprietary applications with 673 | the library. If this is what you want to do, use the GNU Lesser General 674 | Public License instead of this License. But first, please read 675 | . 676 | 677 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Build and install instructions for Notmuchfs. 2 | 3 | Compilation commands 4 | -------------------- 5 | Simply: 6 | $ make 7 | 8 | Platforms 9 | --------- 10 | Any platform that supports FUSE and Notmuch should be supported, but only 11 | 64-bit Arch-linux has been tested extensively. 12 | 13 | Arch Linux 14 | ---------- 15 | 16 | See 'notmuchfs-git' in AUR. 17 | 18 | Dependencies 19 | ------------ 20 | 21 | Notmuch 22 | ------- 23 | Required >= 0.26 24 | 25 | FUSE 26 | ---- 27 | Required >= 2.6, tested with 2.8.7, 2.9.1, 2.9.7 28 | -------------------------------------------------------------------------------- /ISSUES: -------------------------------------------------------------------------------- 1 | notmuchfs - A virtual maildir file system for notmuch queries 2 | ============================================================= 3 | 4 | Notmuchfs is free software, released under the GNU General Public 5 | License version 3 (or later). 6 | 7 | Copyright © 2012-2016 Tim Stoakes 8 | 9 | 10 | 11 | This is early version code, so there are many unimplemented features and rough 12 | edges. Some of them are listed here. 13 | 14 | 15 | Bugs: 16 | ----- 17 | 18 | - Sometimes see 'rename: No such file or directory (errno = 2)' from mutt. Why? 19 | 20 | - Anything that creates new messages or modifies the content of existing ones 21 | will not work. This doesn't seem to break anything with mutt except 22 | break-thread/join-thread (which want to mknod() new message files and write 23 | altered content into them). 24 | 25 | - Sometimes see segfault in Xapian::Document::Internal::open_term_list() called 26 | from notmuchfs_open() (with notmuch 0.11 at least, have not seen it since). 27 | Is it gone? 28 | 29 | - Sometimes see abort in ChertTable::set_overwritten() called from 30 | Xapian::Database::get_document() (with xapian 1.2.12, notmuch 0.14), only 31 | seen it once. 32 | 33 | 34 | Mis(sing)-features: 35 | ------------------- 36 | 37 | - Currently notmuchfs searches for individual messages, not threads. Is a 38 | threads search useful? How to fit it into the 'directory name is query' 39 | model? 40 | 41 | - Currently if the notmuch database contains duplicate message IDs, notmuchfs 42 | only shows one message. Perhaps display them all somehow? Probably not worth 43 | the effort. 44 | 45 | - The X-label header is forwarded within the mail verbatim, if MIME forwarding 46 | is used. Can the MUA be changed to ignore them? It doesn't look like mutt 47 | can, without a patch. 48 | 49 | - Various buffy-esque ways of checking for new mail inside a virtual maildir, 50 | perhaps involving mtime of cur/ dir. Can notmuchfs change this to indicate 51 | new mail in a sensible way? 52 | 53 | - Current munging of virtual maildir file names is not strictly to maildir 54 | spec. Since # is allowed in maildir file names, s,/,#, is not OK, although it 55 | doesn't seem to fail for anyone yet. 56 | 57 | - Notmuchfs doesn't show up in 'df' output. Use the '-a' option to 'df' if you 58 | must. 59 | 60 | 61 | 62 | Ideas: 63 | ------ 64 | 65 | - An IMAP or POP server could run on top of notmuchfs easily. What would make 66 | this more useful? 67 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # notmuchfs - A virtual maildir file system for notmuch queries 2 | # 3 | # Copyright © 2012 Tim Stoakes 4 | # 5 | # This file is part of notmuchfs. 6 | # 7 | # Notmuchfs is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with notmuchfs. If not, see http://www.gnu.org/licenses/ . 19 | # 20 | # Authors: Tim Stoakes 21 | 22 | CFLAGS = -g -O2 -std=c99 -Wall -Wextra -Werror -D_FILE_OFFSET_BITS=64 23 | 24 | OBJS = notmuchfs.o 25 | 26 | LIBS = -lnotmuch -lfuse 27 | 28 | all: notmuchfs 29 | 30 | notmuchfs: $(OBJS) 31 | $(CC) -o $@ $+ $(LIBS) 32 | 33 | clean: 34 | rm -f *.o *.dep notmuchfs 35 | 36 | %.o : %.c 37 | $(COMPILE.c) -MD -o $@ $< 38 | @cp $*.d $*.dep; \ 39 | sed -e 's/#.*//' -e 's/^[^:]*: *//' -e 's/ *\\$$//' \ 40 | -e '/^$$/ d' -e 's/$$/ :/' < $*.d >> $*.dep; \ 41 | rm -f $*.d 42 | 43 | -include $(OBJS:.o=.dep) 44 | -------------------------------------------------------------------------------- /README.MUTT.md: -------------------------------------------------------------------------------- 1 | notmuchfs - A virtual maildir file system for notmuch queries 2 | ============================================================= 3 | 4 | Notmuchfs is free software, released under the GNU General Public 5 | License version 3 (or later). 6 | 7 | Copyright © 2012 Tim Stoakes 8 | 9 | 10 | Quick start - using notmuchfs with mutt 11 | --------------------------------------- 12 | Super quick start: 13 | * Add 'notmuchfs/mutt/bin' to $PATH. 14 | * Add 'source /path/to/notmuchfs/mutt/.muttrc' to the top of your '.muttrc', and 15 | edit that file to taste, etc. 16 | * Mount notmuchfs, being sure to pass the option '-o mutt_2476_workaround'. 17 | * Run mutt! 18 | 19 | 20 | Longer - using notmuchfs with mutt 21 | ---------------------------------- 22 | Notmuchfs was developed because I wanted to use mutt with notmuch, but the mutt 23 | codebase was... difficult. 24 | 25 | Mutt understands maildirs, so the simplest thing I could think of was to 26 | emulate maildirs on top of notmuch, and point mutt at them. See mutt/.muttrc 27 | for a complete example. 28 | 29 | It is assumed that notmuchfs is up and running already. If not, get started 30 | by reading README.md, for example: 31 | 32 | ~~~ sh 33 | $ mkdir ~/my_notmuchfs_backing 34 | $ cd ~/my_notmuchfs_mountpoint 35 | $ mkdir "tag:unread and not tag:spam" 36 | $ ln -s "tag:unread and not tag:spam" inbox 37 | $ notmuchfs ~/my_notmuchfs_mountpoint \ 38 | -o backing_dir=~/my_notmuchfs_backing \ 39 | -o mail_dir=~/.maildir \ 40 | -o mutt_2476_workaround 41 | ~~~ 42 | 43 | Now, just like any other maildir, simply point the 'folder' variable at 44 | notmuchfs and our new notmuchfs inbox: 45 | 46 | ~~~ 47 | set folder=~/my_notmuchfs_mountpoint/ 48 | set spoolfile=+inbox/ 49 | ~~~ 50 | 51 | When the search results have changed e.g. delivery of new mail, mutt will not 52 | automatically notice. So we need to reload the inbox - with a handy macro such 53 | as: 54 | ~~~ 55 | macro index "#" '^' "Reload mailbox" 56 | ~~~ 57 | 58 | It may be nice to colorise messages with particular flags in the index, 59 | perhaps something like: 60 | ~~~ 61 | color index red default "~h '^X-Label: .*interesting_tag.*$'" 62 | ~~~ 63 | 64 | 65 | This is however, essentially read-only - not so useful. 66 | 67 | 68 | (The following section assumes that notmuchfs/mutt/bin/ is in your $PATH, and 69 | 'formail' is installed on the system.) 70 | 71 | Remembering that notmuchfs queries are just directories, making a new search is 72 | simple. With a macro like this, use the included 'mutt/bin/prompt_mkdir' script 73 | to create new query directories from within mutt: 74 | 75 | ~~~ 76 | macro index "S" "prompt_mkdir $folder ?" "Create a new notmuchfs query mailbox" 77 | ~~~ 78 | 79 | 80 | 81 | Now we need a way to modify the tags on messages. Again, with a macro like 82 | this, use the included 'notmuch_tag' script to alter the tags on a single 83 | message, or a (mutt) tagged set of messages interactively: 84 | 85 | ~~~ 86 | macro index,pager ",T" "formail -d -xMessage-id: -s | tr -d \"<>\" | notmuch_tag" "Manage notmuch tags" 87 | ~~~ 88 | 89 | Find yourself entering the same interactive tag modifications over and over? 90 | Perhaps add new macros to taste: 91 | 92 | ~~~ 93 | macro index,pager ",tw" "formail -d -xMessage-id: -s | tr -d \"<>\" | notmuch_tag +watch" "Add 'watch' tag" 94 | ~~~ 95 | 96 | 97 | 98 | Mutt bug 2476 99 | ------------- 100 | Mutt is not compliant with the maildir specification, see: 101 | * http://dev.mutt.org/trac/ticket/2476 102 | * http://notmuchmail.org/pipermail/notmuch/2011/004833.html 103 | 104 | Notmuchfs can work around this issue, if mounted with the 105 | ~~~ sh 106 | '-o mutt_2476_workaround' 107 | ~~~ 108 | mount option. If you use mutt, you want to use this option. Mutt is basically 109 | unusable without it. 110 | 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | notmuchfs - A virtual maildir file system for notmuch queries 2 | ============================================================= 3 | 4 | Notmuchfs is free software, released under the GNU General Public 5 | License version 3 (or later). 6 | 7 | Copyright © 2012 Tim Stoakes 8 | 9 | 10 | 11 | What is notmuch? 12 | ---------------- 13 | Notmuch is an awesome email indexing system: 14 | http://notmuchmail.org/ 15 | 16 | 17 | 18 | What is notmuchfs? 19 | ------------------ 20 | Notmuchfs implements a virtual file system which creates maildirs from notmuch 21 | mail query results. This is useful for using notmuch with tools which are not 22 | aware of notmuch, only maildirs - such as mutt. 23 | 24 | 25 | 26 | How does it work? 27 | ----------------- 28 | A notmuchfs virtual file system is mounted referencing a particular existing 29 | (real) directory called the 'backing store'. 30 | 31 | Directories created within the backing store, when read, appear to have the 32 | standard maildir format (e.g. cur/, new/, tmp/ sub-directories). 33 | 34 | Notmuchfs interprets the names of these backing store directories as notmuch 35 | queries, and fills the cur/ maildir sub-directory in the directory in the 36 | virtual file system with corresponding name, to appear to contain messages 37 | which are the result of executing that query (at the instant in time that the 38 | directory is read). 39 | 40 | Each virtual maildir message file, when read, appears to have the exact content 41 | of the message referenced by the notmuch query, augmented with an 'X-Label' 42 | header generated automatically by notmuchfs, containing the notmuch tags of 43 | that message. 44 | 45 | The name of each virtual maildir message is derived from the real name of 46 | the maildir message, causing maildir flags to be 'passed through' from the 47 | real maildir message to notmuchfs. 48 | 49 | The renaming of virtual maildir messages within the same maildir 50 | sub-directory e.g. cur/, is supported. This allows the modification of 51 | maildir flags to be passed back through notmuchfs to the real message, by 52 | renaming the real message. 53 | 54 | Renaming virtual messages from new/ to cur/ is allowed, to support standard 55 | maildir behavior. Renaming virtual messages from cur/ to new/ is optionally 56 | also allowed, to support non-maildir-compliant MUAs such as mutt (see mount 57 | option '-o mutt_2476_workaround'). 58 | 59 | In all rename cases, the notmuch database is informed of the rename, to keep 60 | the database in sync with the real maildir message. 61 | 62 | Symbolic links to directories have their targets interpreted as notmuch 63 | queries, providing query 'aliases'. 64 | 65 | The unlinking of virtual maildir messages is supported - the real message 66 | file is unlinked. 67 | 68 | In general, non-maildir operations such as mkdir() at the root level, rename of 69 | non-maildir files, etc. which are executed within the virtual file system are 70 | passed to the backing store. 71 | 72 | 73 | 74 | Building notmuchfs 75 | ------------------ 76 | See the INSTALL file for notes on compiling and installing notmuchfs. 77 | 78 | 79 | 80 | Quick start 81 | ----------- 82 | First you need a backing store - this is just a directory that contains other 83 | directories which are interpreted as queries. 84 | 85 | ~~~ sh 86 | $ mkdir ~/my_notmuchfs_backing 87 | ~~~ 88 | 89 | Start notmuchfs, assuming your notmuch database is in ~/.maildir/.notmuch/. 90 | 91 | ~~~ sh 92 | $ notmuchfs ~/my_notmuchfs_mountpoint \ 93 | -o backing_dir=~/my_notmuchfs_backing \ 94 | -o mail_dir=~/.maildir 95 | ~~~ 96 | 97 | Notmuchfs is not too interesting unless you create at least one query. Queries 98 | are directories within the notmuchfs mount point (equally, within the backing 99 | store). 100 | 101 | ~~~ sh 102 | $ cd ~/my_notmuchfs_mountpoint 103 | $ mkdir "tag:unread and not tag:spam" 104 | ~~~ 105 | 106 | That name is not too meaningful, so also create a handy alias to that 107 | query directory. 108 | 109 | ~~~ sh 110 | $ ln -s "tag:unread and not tag:spam" inbox 111 | ~~~ 112 | 113 | See that a virtual maildir is created. 114 | 115 | ~~~ sh 116 | $ ls ~/my_notmuchfs_backing/inbox 117 | 118 | ~~~ 119 | But... 120 | 121 | ~~~ sh 122 | $ ls ~/my_notmuchfs_mountpoint/inbox 123 | cur/ new/ tmp/ 124 | $ ls ~/my_notmuchfs_mountpoint/inbox/cur/ 125 | #maildir#cur#1324430382_4.20193.somehost.net,U=4101:2,S 126 | #maildir#cur#1324427822_1.5704.somehost.net,U=4101:2,S 127 | ... 128 | ~~~ 129 | 130 | These are the results of executing the notmuch query "tag:unread and not 131 | tag:spam". 132 | 133 | 134 | Since 'cat' is not a good MUA, now tell your preferred one to find its maildirs 135 | at ~/my_notmuchfs_mountpoint/. E.g. with mutt, use: 136 | ~~~ 137 | set folder=~/my_notmuchfs_mountpoint/ 138 | ~~~ 139 | 140 | To unmount: 141 | 142 | ~~~ sh 143 | $ fusermount -u ~/my_notmuchfs_mountpoint 144 | ~~~ 145 | 146 | 147 | Using notmuchfs with mutt 148 | ------------------------- 149 | See the README.MUTT.md file for notes on using notmuchfs with mutt. 150 | 151 | 152 | Debugging 153 | --------- 154 | Mount notmuchfs with the '-d' option. Optionally, rebuild notmuchfs with 155 | NOTMUCHFS_DEBUG set to 1 in notmuchfs.c. 156 | 157 | 158 | Contact 159 | ------- 160 | Tim Stoakes 161 | http://tim.stoakes.net/ 162 | -------------------------------------------------------------------------------- /mutt/.muttrc: -------------------------------------------------------------------------------- 1 | #---------------------------------------------------------------------------- 2 | # A demonstration muttrc for use with notmuchfs. 3 | # 4 | # This file is part of notmuchfs 5 | # 6 | # Notmuchfs is free software, released under the GNU General Public 7 | # License version 3 (or later). 8 | # 9 | # Copyright © 2012 Tim Stoakes 10 | #---------------------------------------------------------------------------- 11 | set mbox_type=Maildir 12 | # This is where notmuchfs is mounted. Trailing slash required. 13 | set folder=~/my_notmuchfs_mountpoint/ 14 | 15 | # The directory inside the above folder to open by default. 16 | set spoolfile=+inbox/ 17 | 18 | # Standard mutt, not notmuchfs specific. 19 | set postponed=~/.maildir/.queue/ 20 | set record=~/.maildir/.sent/ 21 | mailboxes + $record $postponed 22 | 23 | # Trim some fields from the folder viewer that don't make much sense. Tune to 24 | # taste. 25 | set folder_format="%2C %N %8s %d %f" 26 | 27 | # Display the X-Label header, so you can see the notmuch tags of a message. 28 | # Perhaps think about 'hdr_order' too. 29 | set weed=yes 30 | unignore X-Label 31 | 32 | # Header caching is a fine addition to any mutt. 33 | set header_cache=~/.mutt/hcache/ 34 | # But body caching doesn't make so much sense with local mail. 35 | # The X-Label header changing dynamically also messes with this, so disable it. 36 | unset message_cachedir 37 | 38 | #---------------------------------------------------------------------------- 39 | 40 | # A macro to reload the contents of the mailbox. Use this when the results of a 41 | # query have changed, and you want to see them. 42 | macro index "#" '^' "Reload mailbox" 43 | 44 | # List or alter notmuch tags of a message or set of tagged messages, by passing 45 | # through a script to the notmuch CLI. 46 | macro index,pager ",T" "formail -d -xMessage-id: -s | tr -d \"<>\" | notmuch_tag" "Manage notmuch tags" 47 | 48 | # Shortcuts for managing specific tags. 49 | # e.g.: 50 | #macro index,pager ",ti" "formail -d -xMessage-id: -s | tr -d \"<>\" | notmuch_tag +interesting_tag" "Add 'interesting_tag' tag" 51 | #macro index,pager ",tI" "formail -d -xMessage-id: -s | tr -d \"<>\" | notmuch_tag -interesting_tag" "Remove 'interesting_tag' tag" 52 | 53 | 54 | # Creating a new notmuch search is as simple as creating a new directory inside 55 | # notmuchfs. This macro does just that. 56 | # 57 | # The default search stem to use. May be empty. 58 | set my_default_search="tag:interesting_tag" 59 | macro index "S" "prompt_mkdir $folder$my_default_search ?" "Create a new notmuchfs query mailbox" 60 | 61 | # Colorise messages with particular flags. 62 | # e.g.: 63 | #color index red default "~h '^X-Label: .*interesting_tag.*$'" 64 | 65 | #---------------------------------------------------------------------------- 66 | -------------------------------------------------------------------------------- /mutt/README: -------------------------------------------------------------------------------- 1 | See ../README.MUTT.md 2 | -------------------------------------------------------------------------------- /mutt/bin/notmuch_tag: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to prompt the user for the tag string to use (e.g. '+tag1 -tag2'), 4 | # then apply it to the given message(s). 5 | # 6 | # Usage: 7 | # Interactive: 8 | # $ notmuch_tag 9 | # Enter "?" to see a full tag list. 10 | # 11 | # Pipe in Message-ID value(s), select tags interactively: 12 | # $ echo id | notmuch_tag 13 | # 14 | # All automatic: 15 | # $ echo id | notmuch_tag +tag 16 | # 17 | # This file is part of notmuchfs. 18 | # 19 | # Notmuchfs is free software, released under the GNU General Public 20 | # License version 3 (or later). 21 | # 22 | # Copyright © 2012 Tim Stoakes 23 | ################################################################################ 24 | 25 | # Tag string, if any. 26 | TAGS=$1 27 | 28 | # Fetch the list of message IDs. 29 | while true; do 30 | read -p "Message-ID: " ID 31 | if [ -z "$ID" ]; then 32 | break 33 | fi 34 | IDS+=($ID) 35 | done 36 | 37 | if [ ${#IDS[*]} -eq 0 ]; then 38 | echo "Message-ID required" 39 | exit 1; 40 | fi 41 | 42 | ID_ORED=$(printf "id:%s or " "${IDS[@]}") 43 | ID_ORED=${ID_ORED:0:${#ID_ORED}-4} 44 | 45 | if [ -z "$TAGS" ]; then 46 | # Display the current tags of the given messages, but only if no tags were 47 | # specified on the command line (which probably means we are 48 | # non-interactive). 49 | echo 50 | echo "Current tags:" 51 | notmuch search-tags $ID_ORED 52 | echo 53 | fi 54 | 55 | while [ -z "$TAGS" ]; do 56 | # Prompt for tag string. Hack /dev/tty for mutt. 57 | read -p "Tags: " -e TAGS < /dev/tty 58 | if [ -z "$TAGS" ]; then 59 | exit 60 | fi 61 | 62 | if [ "$TAGS" == "?" ]; then 63 | echo 64 | echo "All tags:" 65 | notmuch search-tags 66 | echo 67 | TAGS= 68 | fi 69 | done 70 | 71 | notmuch tag $TAGS $ID_ORED 72 | exit $? 73 | -------------------------------------------------------------------------------- /mutt/bin/prompt_mkdir: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Prompt for a directory name, then mkdir it. 4 | # 5 | # This file is part of notmuchfs. 6 | # 7 | # Notmuchfs is free software, released under the GNU General Public 8 | # License version 3 (or later). 9 | # 10 | # Copyright © 2012-2014 Tim Stoakes 11 | ################################################################################ 12 | 13 | read -e -p "Directory: " -i "$1" DIR 14 | if [ -z "$DIR" ]; then 15 | exit 1 16 | fi 17 | 18 | mkdir "$DIR" 19 | exit $? 20 | -------------------------------------------------------------------------------- /notmuchfs.c: -------------------------------------------------------------------------------- 1 | /*============================================================================*/ 2 | /* 3 | * notmuchfs - A virtual maildir file system for notmuch queries 4 | * 5 | * Copyright © 2012-2017 Tim Stoakes 6 | * 7 | * This file is part of notmuchfs. 8 | * 9 | * Notmuchfs is free software: you can redistribute it and/or modify 10 | * it under the terms of the GNU General Public License as published by 11 | * the Free Software Foundation, either version 3 of the License, or 12 | * (at your option) any later version. 13 | * 14 | * This program is distributed in the hope that it will be useful, 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | * GNU General Public License for more details. 18 | * 19 | * You should have received a copy of the GNU General Public License 20 | * along with notmuchfs. If not, see http://www.gnu.org/licenses/ . 21 | * 22 | * Authors: Tim Stoakes 23 | */ 24 | /*============================================================================*/ 25 | 26 | /** 27 | * @section message_names Message Names 28 | * 29 | * Messages in a notmuchfs virtual maildir are named by taking the full path 30 | * to the real message, and replacing all / with #. This whole string is now 31 | * the message name. 32 | * 33 | * 34 | * @section x_label X-Label Header 35 | * 36 | * Each message read from a virtual maildir has an X-Label header inserted 37 | * on-the-fly, containing the concatenation of the notmuch tags of this 38 | * message (comma separated), up to #MAX_XLABEL_LENGTH characters long. 39 | */ 40 | 41 | /*============================================================================*/ 42 | 43 | #define _GNU_SOURCE 44 | 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | 63 | #define FUSE_USE_VERSION 26 64 | #include 65 | 66 | #include "notmuch.h" 67 | 68 | /*============================================================================*/ 69 | 70 | #define NOTMUCHFS_VERSION "0.4" 71 | 72 | /*============================================================================*/ 73 | 74 | /** 75 | * Global configuration information, from CLI. 76 | * 77 | * @{ 78 | */ 79 | struct notmuchfs_config { 80 | /** 81 | * The backing directory path. 82 | */ 83 | char *backing_dir; 84 | 85 | /** 86 | * The notmuch database directory path. This is actually the directory that 87 | * contains the .notmuch/ database directory, since that's what notmuch 88 | * requires. 89 | */ 90 | char *mail_dir; 91 | 92 | 93 | /** 94 | * Tag to apply when a mail is deleted instead of unlinking the 95 | * underlying maildir file. 96 | */ 97 | char *delete_tag; 98 | 99 | /** 100 | * Mutt is not compliant with the maildir spec, see: 101 | * - http://dev.mutt.org/trac/ticket/2476 102 | * - http://notmuchmail.org/pipermail/notmuch/2011/004833.html 103 | * Notmuchfs can workaround this issue if this field is set. 104 | */ 105 | bool mutt_2476_workaround_allowed; 106 | }; 107 | 108 | static struct notmuchfs_config global_config; 109 | 110 | /** @} */ 111 | 112 | /*============================================================================*/ 113 | 114 | /** 115 | * Whether to enable debug tracing. 116 | */ 117 | #define NOTMUCHFS_DEBUG 0 118 | 119 | /** 120 | * The maximum length of the X-Label header that notmuchfs will synthesize. 121 | * 122 | * @todo this limit is arbitrary. 123 | */ 124 | #define MAX_XLABEL_LENGTH 1024 125 | 126 | /** 127 | * The text of the X-Label header. 128 | */ 129 | #define XLABEL "X-Label: " 130 | 131 | /*============================================================================*/ 132 | 133 | /** Lock a pthread mutex with error checking. */ 134 | #define PTHREAD_LOCK(LOCK) \ 135 | do { \ 136 | int _ret = pthread_mutex_lock(LOCK); \ 137 | assert(_ret == 0); \ 138 | } while (0); 139 | 140 | /** Unlock a pthread mutex with error checking. */ 141 | #define PTHREAD_UNLOCK(LOCK) \ 142 | do { \ 143 | int _ret = pthread_mutex_unlock(LOCK); \ 144 | assert(_ret == 0); \ 145 | } while (0); 146 | 147 | /*============================================================================*/ 148 | 149 | /** 150 | * Logging. 151 | */ 152 | #if NOTMUCHFS_DEBUG 153 | #define LOG_TRACE(...) \ 154 | printf(__VA_ARGS__) 155 | #else 156 | #define LOG_TRACE(...) 157 | #endif 158 | 159 | /*============================================================================*/ 160 | 161 | /** 162 | * The context required to deal with the notmuch database. 163 | */ 164 | typedef struct 165 | { 166 | /** Mutex to protect the database object from concurrent access. */ 167 | pthread_mutex_t mutex; 168 | 169 | /** The notmuch database handle. May be NULL if the database is not opened. */ 170 | notmuch_database_t *db; 171 | 172 | /** Newline-delimited string list of tags to exclude from results. */ 173 | char *excluded_tags; 174 | } notmuch_context_t; 175 | 176 | /*============================================================================*/ 177 | 178 | /** 179 | * Replace every instance of character 'old' with 'new' in 'str_in'. 180 | * 181 | * @param[in,out] str_in The string to replace within. 182 | * @param[in] old The character to replace. 183 | * @param[in] new The character to replace with. 184 | */ 185 | static void string_replace (char *str_in, char old, char new) 186 | { 187 | char *str = str_in; 188 | 189 | while (*str != '\0') { 190 | if (*str == old) 191 | *str = new; 192 | str++; 193 | } 194 | } 195 | 196 | /*============================================================================*/ 197 | 198 | /** 199 | * Open the notmuch database inside this context. Continue trying forever 200 | * if the open fails (e.g. the database was locked). 201 | * 202 | * @param[in,out] p_ctx The notmuch context. 203 | * @param[in] need_write Whether to open the database in read-only or 204 | * read-write mode. 205 | * @pre Database must not already be open. 206 | */ 207 | static void database_open (notmuch_context_t *p_ctx, bool need_write) 208 | { 209 | LOG_TRACE("notmuch database_open\n"); 210 | PTHREAD_LOCK(&p_ctx->mutex); 211 | assert(p_ctx->db == NULL); 212 | 213 | while (TRUE) { 214 | notmuch_status_t status = 215 | notmuch_database_open(global_config.mail_dir, 216 | need_write ? 217 | NOTMUCH_DATABASE_MODE_READ_WRITE: 218 | NOTMUCH_DATABASE_MODE_READ_ONLY, 219 | &p_ctx->db); 220 | 221 | if (status == NOTMUCH_STATUS_SUCCESS) { 222 | break; 223 | } 224 | else if (status == NOTMUCH_STATUS_XAPIAN_EXCEPTION) { 225 | /* Try again. */ 226 | sleep(1); 227 | } 228 | else { 229 | fprintf(stderr, "ERROR: Database open error.\n"); 230 | exit(1); 231 | } 232 | } 233 | 234 | if (notmuch_database_needs_upgrade(p_ctx->db)) { 235 | fprintf(stderr, "ERROR: Database needs upgrade.\n"); 236 | exit(1); 237 | } 238 | } 239 | 240 | /*============================================================================*/ 241 | 242 | /** 243 | * Close the notmuch database inside this context. 244 | * 245 | * @param[in,out] p_ctx The notmuch context. 246 | * @pre Database must be open. 247 | */ 248 | static void database_close (notmuch_context_t *p_ctx) 249 | { 250 | LOG_TRACE("notmuch database_close\n"); 251 | assert(p_ctx->db != NULL); 252 | notmuch_database_close(p_ctx->db); 253 | notmuch_database_destroy(p_ctx->db); 254 | p_ctx->db = NULL; 255 | PTHREAD_UNLOCK(&p_ctx->mutex); 256 | } 257 | 258 | /*============================================================================*/ 259 | 260 | /* FUSE operations. */ 261 | 262 | /** The maximum length of the tag exclusion string. Arbitrarily chosen. */ 263 | #define EXCLUDED_TAGS_MAX_LENGTH 128 264 | 265 | static void *notmuchfs_init (struct fuse_conn_info *conn) 266 | { 267 | (void)conn; 268 | 269 | int res = chdir(global_config.backing_dir); 270 | if (res == -1) 271 | return NULL; 272 | 273 | notmuch_context_t *p_ctx = malloc(sizeof(notmuch_context_t)); 274 | memset(p_ctx, 0, sizeof(notmuch_context_t)); 275 | res = pthread_mutex_init(&p_ctx->mutex, NULL); 276 | if (res != 0) { 277 | free(p_ctx); 278 | return NULL; 279 | } 280 | 281 | /* Fetch the list of excluded tags from notmuch config. 282 | * If only there was an API for this... 283 | */ 284 | p_ctx->excluded_tags = malloc(EXCLUDED_TAGS_MAX_LENGTH); 285 | p_ctx->excluded_tags[0] = '\0'; 286 | FILE *fp = popen("notmuch config get search.exclude_tags", "r"); 287 | if (fp != NULL) { 288 | size_t bytes_read = fread(p_ctx->excluded_tags, 1, EXCLUDED_TAGS_MAX_LENGTH, 289 | fp); 290 | if (bytes_read > 0) { 291 | p_ctx->excluded_tags[bytes_read - 1] = '\0'; 292 | } 293 | (void)pclose(fp); 294 | } 295 | 296 | return p_ctx; 297 | } 298 | 299 | /*============================================================================*/ 300 | 301 | static void notmuchfs_destroy (void *p_ctx_in) 302 | { 303 | notmuch_context_t *p_ctx = (notmuch_context_t *)p_ctx_in; 304 | 305 | free(p_ctx->excluded_tags); 306 | int res = pthread_mutex_destroy(&p_ctx->mutex); 307 | /* Any failure here is a problem that we caused. */ 308 | assert(res == 0); 309 | 310 | free(p_ctx); 311 | } 312 | 313 | /*============================================================================*/ 314 | 315 | static int notmuchfs_getattr (const char *path, struct stat *stbuf) 316 | { 317 | int res = 0; 318 | 319 | memset(stbuf, 0, sizeof(struct stat)); 320 | 321 | if (strcmp(path, "/") == 0) { 322 | /* Querying the base directory, pass to backing store. */ 323 | if (stat(".", stbuf) != 0) 324 | res = -errno; 325 | return res; 326 | } 327 | 328 | char *last_slash = strrchr(path + 1, '/'); 329 | if (last_slash == NULL) { 330 | /* Querying '/', pass to backing store. */ 331 | LOG_TRACE("getattr stat1: %s\n", path + 1); 332 | if (lstat(path + 1, stbuf) != 0) 333 | res = -errno; 334 | } 335 | else if (strcmp(last_slash + 1, "new") == 0 || 336 | strcmp(last_slash + 1, "tmp") == 0 || 337 | strcmp(last_slash + 1, "cur") == 0) { 338 | /* Querying a maildir directory, so copy the parent directory. */ 339 | char trans_name[PATH_MAX]; 340 | strncpy(trans_name, path + 1, last_slash - path); 341 | trans_name[last_slash - path] = '\0'; 342 | LOG_TRACE("getattr stat2: %s\n", trans_name); 343 | if (stat(trans_name, stbuf) != 0) 344 | res = -errno; 345 | } 346 | else { 347 | /* '//cur/translated#msg#name' */ 348 | char *first_slash = strchr(path + 1, '/'); 349 | bool mutt_2476_workaround = FALSE; 350 | 351 | if (global_config.mutt_2476_workaround_allowed) { 352 | /* The workaround here is to intercept all getattr()s of a path like: 353 | * /real/path/new/fake#maildir#cur#foofile 354 | * and treat it as if 'new' was 'cur' thus: 355 | * /real/path/cur/fake#maildir#cur#foofile 356 | */ 357 | 358 | if ((last_slash - path) >= 3 && 359 | memcmp(last_slash - 3, "new", 3) == 0) { 360 | LOG_TRACE("Activating mutt_bug_2476 workaround for getattr(%s)\n", path); 361 | mutt_2476_workaround = TRUE; 362 | } 363 | } 364 | 365 | if (mutt_2476_workaround || 366 | (last_slash - first_slash > 3 && 367 | strncmp(first_slash, "/cur/", 5) == 0)) { 368 | char trans_name[PATH_MAX]; 369 | strncpy(trans_name, last_slash + 1, PATH_MAX - 1); 370 | trans_name[PATH_MAX - 1] = '\0'; 371 | string_replace(trans_name, '#', '/'); 372 | 373 | LOG_TRACE("getattr stat3: %s\n", trans_name); 374 | if (stat(trans_name, stbuf) != 0) 375 | res = -errno; 376 | 377 | /* Inflate the size of the file by the maximum length of a synthetic 378 | * X-Label header. 379 | */ 380 | stbuf->st_size += MAX_XLABEL_LENGTH; 381 | } 382 | else { 383 | res = -ENOENT; 384 | } 385 | } 386 | 387 | return res; 388 | } 389 | 390 | /*============================================================================*/ 391 | 392 | /** 393 | * Which type of directory read is being done? 394 | */ 395 | typedef enum 396 | { 397 | /** A directory containing no files. */ 398 | OPENDIR_TYPE_EMPTY_DIR, 399 | /** A maildir root (e.g. containing 'cur/'). */ 400 | OPENDIR_TYPE_MAIL_DIR, 401 | /** A real directory in the backing store. */ 402 | OPENDIR_TYPE_BACKING_DIR, 403 | /** A maildir with message files taken from a notmuch query. */ 404 | OPENDIR_TYPE_NOTMUCH_QUERY 405 | } opendir_type_t; 406 | 407 | 408 | /** 409 | * Context for opendir(), readdir(), releasedir(). 410 | */ 411 | typedef struct 412 | { 413 | opendir_type_t type; 414 | 415 | /** 416 | * These are for type == OPENDIR_TYPE_NOTMUCH_QUERY. 417 | * @{ 418 | */ 419 | notmuch_query_t *p_query; 420 | notmuch_messages_t *p_messages; 421 | /** @} */ 422 | 423 | /** This is for type == OPENDIR_TYPE_BACKING_DIR. */ 424 | DIR *fd; 425 | 426 | off_t next_offset; 427 | } opendir_t; 428 | 429 | /*============================================================================*/ 430 | 431 | static int notmuchfs_opendir (const char* path, struct fuse_file_info* fi) 432 | { 433 | int res = 0; 434 | opendir_t *dir_fd = (opendir_t *) malloc(sizeof(opendir_t)); 435 | 436 | if (strcmp(path, "/") == 0) { 437 | /* Listing '/', so show the backing directory. */ 438 | dir_fd->type = OPENDIR_TYPE_BACKING_DIR; 439 | char trans_name[PATH_MAX]; 440 | strncpy(trans_name, global_config.backing_dir, PATH_MAX - 1); 441 | strncpy(trans_name + strlen(global_config.backing_dir), path + 1, 442 | PATH_MAX - 1 - strlen(global_config.backing_dir)); 443 | trans_name[PATH_MAX - 1] = '\0'; 444 | 445 | LOG_TRACE("opendir list backing dir: %s\n", trans_name); 446 | dir_fd->fd = opendir(trans_name); 447 | } 448 | else { 449 | char *last_slash = strrchr(path + 1, '/'); 450 | if (last_slash == NULL) { 451 | /* Listing '/', so return the 3 maildir dirs. */ 452 | LOG_TRACE("opendir fake maildir: %s\n", path); 453 | dir_fd->type = OPENDIR_TYPE_MAIL_DIR; 454 | } 455 | else if (strcmp(last_slash + 1, "new") == 0 || 456 | strcmp(last_slash + 1, "tmp") == 0) { 457 | /* Listing '//new' or '//tmp', so return nothing. */ 458 | LOG_TRACE("opendir fake empty new/, tmp/ maildir: %s\n", path); 459 | dir_fd->type = OPENDIR_TYPE_EMPTY_DIR; 460 | } 461 | else if (strcmp(last_slash + 1, "cur") == 0) { 462 | /* Listing '//cur', so parse the query from the pathname, and 463 | * execute it to get the iterator, and remember it. 464 | */ 465 | dir_fd->type = OPENDIR_TYPE_NOTMUCH_QUERY; 466 | char trans_name[PATH_MAX]; 467 | strncpy(trans_name, path + 1, last_slash - path - 1); 468 | trans_name[last_slash - path - 1] = '\0'; 469 | 470 | /* If it's a symlink, dereference it. */ 471 | struct stat stbuf; 472 | while (res == 0) { 473 | LOG_TRACE("opendir stat(%s)\n", trans_name); 474 | if (lstat(trans_name, &stbuf) == 0 && 475 | S_ISLNK(stbuf.st_mode)) { 476 | char link_name[PATH_MAX + 1]; 477 | LOG_TRACE("opendir dereference symlink %s for query\n", trans_name); 478 | res = readlink(trans_name, link_name, PATH_MAX); 479 | if (res >= 0) { 480 | link_name[res] = '\0'; 481 | memcpy(trans_name, link_name, res + 1); 482 | res = 0; 483 | } 484 | else 485 | res = -errno; 486 | } 487 | else 488 | break; 489 | } 490 | 491 | LOG_TRACE("opendir notmuch query: '%s'\n", trans_name); 492 | 493 | struct fuse_context *p_fuse_ctx = fuse_get_context(); 494 | notmuch_context_t *p_ctx = (notmuch_context_t *)p_fuse_ctx->private_data; 495 | database_open(p_ctx, FALSE); 496 | 497 | dir_fd->next_offset = 1; 498 | dir_fd->p_query = notmuch_query_create(p_ctx->db, trans_name); 499 | if (dir_fd->p_query != NULL) { 500 | /* Exclude messages that match the 'excluded' tags. */ 501 | char *exclude_tag = strtok(p_ctx->excluded_tags, "\n"); 502 | while (exclude_tag != NULL) { 503 | notmuch_query_add_tag_exclude(dir_fd->p_query, exclude_tag); 504 | exclude_tag = strtok(NULL, "\n"); 505 | } 506 | notmuch_query_set_omit_excluded(dir_fd->p_query, NOTMUCH_EXCLUDE_ALL); 507 | 508 | /* Run the query. */ 509 | notmuch_status_t status = 510 | notmuch_query_search_messages(dir_fd->p_query, &dir_fd->p_messages); 511 | if (status != NOTMUCH_STATUS_SUCCESS) { 512 | notmuch_query_destroy(dir_fd->p_query); 513 | dir_fd->p_query = NULL; 514 | database_close(p_ctx); 515 | res = -EIO; 516 | } 517 | else { 518 | /* On success, the database is left open here. */ 519 | } 520 | } 521 | else { 522 | database_close(p_ctx); 523 | res = -EIO; 524 | } 525 | } 526 | else { 527 | /* Trying to open an unrecognized directory, that we did not put there. 528 | * Error it, since this is not supported behavior. 529 | */ 530 | res = -ENOENT; 531 | } 532 | } 533 | 534 | if (res == 0) { 535 | fi->fh = (uint64_t)(uintptr_t)dir_fd; 536 | } 537 | else { 538 | free(dir_fd); 539 | } 540 | return res; 541 | } 542 | 543 | /*============================================================================*/ 544 | 545 | static int notmuchfs_releasedir (const char *path, struct fuse_file_info *fi) 546 | { 547 | (void)path; 548 | 549 | opendir_t *dir_fd = (opendir_t *)(uintptr_t)fi->fh; 550 | if (dir_fd != NULL) { 551 | if (dir_fd->type == OPENDIR_TYPE_NOTMUCH_QUERY) { 552 | if (dir_fd->p_messages != NULL) 553 | notmuch_messages_destroy(dir_fd->p_messages); 554 | if (dir_fd->p_query != NULL) 555 | notmuch_query_destroy(dir_fd->p_query); 556 | 557 | struct fuse_context *p_fuse_ctx = fuse_get_context(); 558 | notmuch_context_t *p_ctx = (notmuch_context_t *)p_fuse_ctx->private_data; 559 | database_close(p_ctx); 560 | } 561 | else if (dir_fd->type == OPENDIR_TYPE_BACKING_DIR) { 562 | int ret = closedir(dir_fd->fd); 563 | /* The only possible error value is EBADF, which would be a programming 564 | * error. 565 | */ 566 | assert(ret == 0); 567 | } 568 | free(dir_fd); 569 | dir_fd = NULL; 570 | } 571 | return 0; 572 | } 573 | 574 | /*============================================================================*/ 575 | 576 | /** 577 | * Adds an entry to a readdir() buffer with a maildir file, representing the 578 | * given notmuch messages. 579 | * 580 | * @param[in,out] dir_fd The opendir context. 581 | * @param[in] p_message The notmuch message to add to the directory 582 | * listing. 583 | * @param[in,out] buf The readdir() buffer. 584 | * @param[in] filler The filler function. 585 | * 586 | * @return A negative errno on error, 0 on success, #INT_MAX if the 587 | * directory is too full to add this message. 588 | */ 589 | static int fill_dir_with_message (opendir_t *dir_fd, 590 | notmuch_message_t *p_message, 591 | void *buf, 592 | fuse_fill_dir_t filler) 593 | { 594 | assert(p_message != NULL); 595 | 596 | int res = 0; 597 | 598 | const char *fname = notmuch_message_get_filename(p_message); 599 | if (fname != NULL) { 600 | struct stat stbuf; 601 | if (stat(fname, &stbuf) == 0) { 602 | char trans_name[PATH_MAX]; 603 | strncpy(trans_name, fname, PATH_MAX - 1); 604 | trans_name[PATH_MAX - 1] = '\0'; 605 | string_replace(trans_name, '/', '#'); 606 | 607 | /* Perpetuate the file size inflation lie told in getattr(). */ 608 | stbuf.st_size += MAX_XLABEL_LENGTH; 609 | LOG_TRACE("readdir filling dir %s at %ld\n", 610 | trans_name, dir_fd->next_offset); 611 | if (filler(buf, trans_name, &stbuf, dir_fd->next_offset++) != 0) { 612 | LOG_TRACE("readdir filler full \"%s\".\n", trans_name); 613 | dir_fd->next_offset--; 614 | res = INT_MAX; 615 | } 616 | } 617 | else if (errno == ENOENT) { 618 | /* If a message is gone, don't stop the whole readdir(). */ 619 | fprintf(stderr, "WARNING: Skipping missing file \"%s\".\n", fname); 620 | } 621 | else { 622 | fprintf(stderr, "ERROR: notmuch message stat error \"%s\" %s.\n", fname, 623 | strerror(errno)); 624 | res = -errno; 625 | } 626 | } 627 | else { 628 | /* There's nothing we can do about this case, which I doubt can ever 629 | * happen. Just ignore it. 630 | */ 631 | } 632 | 633 | return res; 634 | } 635 | 636 | /*============================================================================*/ 637 | 638 | static int notmuchfs_readdir (const char *path, 639 | void *buf, 640 | fuse_fill_dir_t filler, 641 | off_t offset_in, 642 | struct fuse_file_info *fi) 643 | { 644 | (void)path; 645 | int res = 0; 646 | 647 | opendir_t *dir_fd = (opendir_t *)(uintptr_t)fi->fh; 648 | 649 | switch (dir_fd->type) { 650 | case OPENDIR_TYPE_NOTMUCH_QUERY: 651 | { 652 | if (offset_in == 0) { 653 | filler(buf, ".", NULL, dir_fd->next_offset++); 654 | filler(buf, "..", NULL, dir_fd->next_offset++); 655 | } 656 | else if (offset_in + 1 != dir_fd->next_offset) { 657 | fprintf(stderr, "ERROR: discontiguous dir offsets %ld %ld.\n", 658 | (long int)offset_in, (long int)dir_fd->next_offset); 659 | res = -EDOM; 660 | break; 661 | } 662 | 663 | notmuch_message_t *p_message = NULL; 664 | while (res == 0 && 665 | (p_message = notmuch_messages_get(dir_fd->p_messages)) != NULL) { 666 | 667 | res = fill_dir_with_message(dir_fd, p_message, buf, filler); 668 | 669 | notmuch_message_destroy(p_message); 670 | if (res == INT_MAX) { 671 | res = 0; 672 | break; 673 | } 674 | notmuch_messages_move_to_next(dir_fd->p_messages); 675 | } 676 | break; 677 | } 678 | 679 | case OPENDIR_TYPE_BACKING_DIR: 680 | { 681 | LOG_TRACE("readdir read from backing directory:\n"); 682 | seekdir(dir_fd->fd, offset_in); 683 | 684 | struct dirent *de; 685 | while ((de = readdir(dir_fd->fd)) != NULL) { 686 | struct stat st; 687 | memset(&st, 0, sizeof(st)); 688 | st.st_ino = de->d_ino; 689 | st.st_mode = de->d_type << 12; 690 | 691 | if (filler(buf, de->d_name, &st, telldir(dir_fd->fd)) != 0) { 692 | res = 0; 693 | break; 694 | } 695 | } 696 | break; 697 | } 698 | 699 | case OPENDIR_TYPE_EMPTY_DIR: 700 | { 701 | filler(buf, ".", NULL, 0); 702 | filler(buf, "..", NULL, 0); 703 | break; 704 | } 705 | 706 | case OPENDIR_TYPE_MAIL_DIR: 707 | { 708 | filler(buf, ".", NULL, 0); 709 | filler(buf, "..", NULL, 0); 710 | filler(buf, "cur", NULL, 0); 711 | filler(buf, "new", NULL, 0); 712 | filler(buf, "tmp", NULL, 0); 713 | break; 714 | } 715 | } 716 | 717 | return res; 718 | } 719 | 720 | /*============================================================================*/ 721 | 722 | /** 723 | * The string to replace the list of message tags with in the X-Label header, 724 | * if the header will not fit in #MAX_XLABEL_LENGTH. 725 | */ 726 | #define TAG_ERROR_STRING "ERROR" 727 | 728 | /** 729 | * Fill the provided buffer with all the tags of the given message, comma 730 | * separated. If they don't all fit, replace the whole string with 731 | * #TAG_ERROR_STRING. No NULL termination. 732 | * 733 | * @param[in,out] buf_in The buffer to fill. 734 | * @param[in] length The length of 'buf_in'. 735 | * @param[in] p_message The message to read tags from. 736 | * @return The number of bytes written to the buffer. 737 | */ 738 | static size_t fill_string_with_tags (char *buf_in, 739 | size_t length, 740 | notmuch_message_t *p_message) 741 | { 742 | char *buf = buf_in; 743 | notmuch_tags_t *tags = notmuch_message_get_tags(p_message); 744 | const char *tag_str = NULL; 745 | bool error = FALSE; 746 | 747 | while ((tag_str = notmuch_tags_get(tags)) != NULL) { 748 | LOG_TRACE("Adding tag \"%s\" to X-label\n", tag_str); 749 | 750 | /* If this tag can fit in the buffer, append it. Otherwise, error out. */ 751 | if (strlen(tag_str) >= length - (buf - buf_in)) { 752 | error = TRUE; 753 | break; 754 | } 755 | memcpy(buf, tag_str, strlen(tag_str)); 756 | buf += strlen(tag_str); 757 | 758 | notmuch_tags_move_to_next(tags); 759 | 760 | if (notmuch_tags_valid(tags)) { 761 | /* There's another one coming, add separator. */ 762 | if (length - (buf - buf_in) < 1) { 763 | error = TRUE; 764 | break; 765 | } 766 | buf[0] = ','; 767 | buf++; 768 | } 769 | } 770 | if (error) { 771 | LOG_TRACE("X-Label buffer overflow\n"); 772 | buf = buf_in; 773 | memcpy(buf, TAG_ERROR_STRING, strlen(TAG_ERROR_STRING)); 774 | buf += strlen(TAG_ERROR_STRING); 775 | } 776 | notmuch_tags_destroy(tags); 777 | 778 | return buf - buf_in; 779 | } 780 | 781 | /*============================================================================*/ 782 | 783 | /** 784 | * A notmuchfs open file handle type, created by notmuchfs_open(). 785 | */ 786 | typedef struct 787 | { 788 | /** The actual file handle. */ 789 | int fh; 790 | /** The X-Label header - filled by open(), used later. */ 791 | char x_label[MAX_XLABEL_LENGTH]; 792 | } open_t; 793 | 794 | 795 | static int notmuchfs_open (const char *path, struct fuse_file_info *fi) 796 | { 797 | if ((fi->flags & 3) != O_RDONLY) 798 | return -EACCES; 799 | 800 | open_t *p_open = malloc(sizeof(open_t)); 801 | memset(p_open, 0, sizeof(open_t)); 802 | 803 | char *last_slash = strrchr(path + 1, '/'); 804 | if (last_slash == NULL) { 805 | p_open->fh = open(path + 1, O_RDONLY); 806 | if (p_open->fh == -1) { 807 | int err = errno; 808 | free(p_open); 809 | return -err; 810 | } 811 | } 812 | else { 813 | char trans_name[PATH_MAX]; 814 | strncpy(trans_name, last_slash + 1, PATH_MAX - 1); 815 | trans_name[PATH_MAX - 1] = '\0'; 816 | 817 | char *first_pslash = strchr(trans_name, '#'); 818 | if (first_pslash != NULL) { 819 | string_replace(trans_name, '#', '/'); 820 | 821 | struct fuse_context *p_fuse_ctx = fuse_get_context(); 822 | notmuch_context_t *p_ctx = 823 | (notmuch_context_t *)p_fuse_ctx->private_data; 824 | 825 | /** 826 | * @todo shouldn't need writeable database here, but otherwise get: 827 | * "Internal error: Failure to ensure database is writable" 828 | * why? 829 | */ 830 | database_open(p_ctx, TRUE); 831 | 832 | LOG_TRACE("open notmuch lookup by name: %s\n", trans_name); 833 | notmuch_message_t *p_message; 834 | if (notmuch_database_find_message_by_filename(p_ctx->db, trans_name, 835 | &p_message) == 836 | NOTMUCH_STATUS_SUCCESS) { 837 | if (p_message == NULL) { 838 | LOG_TRACE("WARNING: Message not found in DB - ignoring."); 839 | } 840 | else { 841 | char *buf = p_open->x_label; 842 | /* Make sure the buffer is big enough to at least take the 843 | * representation of overflow. 844 | */ 845 | assert(MAX_XLABEL_LENGTH > 846 | strlen(XLABEL) + strlen(TAG_ERROR_STRING) + 1); 847 | memcpy(buf, XLABEL, strlen(XLABEL)); 848 | buf += strlen(XLABEL); 849 | buf += fill_string_with_tags(buf, 850 | MAX_XLABEL_LENGTH - strlen(XLABEL) - 1, 851 | p_message); 852 | 853 | /* Pad the header out. RFC5322 doesn't say anything about this that I 854 | * can see. NULs don't work, nor \n's, so spaces are used. 855 | */ 856 | while (buf - p_open->x_label < (MAX_XLABEL_LENGTH - 1)) { 857 | assert(1 <= MAX_XLABEL_LENGTH - (buf - p_open->x_label)); 858 | buf[0] = ' '; 859 | buf++; 860 | } 861 | 862 | assert(1 <= MAX_XLABEL_LENGTH - (buf - p_open->x_label)); 863 | buf[0] = '\n'; 864 | buf++; 865 | 866 | notmuch_message_destroy(p_message); 867 | } 868 | database_close(p_ctx); 869 | } 870 | else { 871 | /* Notmuch somehow failed to do anything successfully, fail the open. */ 872 | database_close(p_ctx); 873 | free(p_open); 874 | return -EIO; 875 | } 876 | } 877 | 878 | LOG_TRACE("open(%s)\n", trans_name); 879 | p_open->fh = open(trans_name, O_RDONLY); 880 | if (p_open->fh == -1) { 881 | int err = errno; 882 | free(p_open); 883 | return -err; 884 | } 885 | } 886 | 887 | fi->fh = (uint64_t)(uintptr_t)p_open; 888 | 889 | return 0; 890 | } 891 | 892 | /*============================================================================*/ 893 | 894 | static int notmuchfs_release (const char *path, struct fuse_file_info *fi) 895 | { 896 | (void)path; 897 | open_t *p_open = (open_t *)(uintptr_t)fi->fh; 898 | assert(p_open != NULL); 899 | 900 | LOG_TRACE("close(%d)\n", p_open->fh); 901 | int res = close(p_open->fh); 902 | assert(res == 0); 903 | 904 | free(p_open); 905 | fi->fh = (uint64_t)(uintptr_t)NULL; 906 | 907 | return 0; /* Documentation says this is ignored. */ 908 | } 909 | 910 | /*============================================================================*/ 911 | 912 | static int notmuchfs_read (const char *path, 913 | char *buf_in, 914 | size_t size, 915 | off_t offset, 916 | struct fuse_file_info *fi) 917 | { 918 | (void)path; 919 | char *buf = buf_in; 920 | size_t offset_adj = MAX_XLABEL_LENGTH; 921 | open_t *p_open = (open_t *)(uintptr_t)fi->fh; 922 | 923 | assert(p_open != NULL); 924 | 925 | if (offset < MAX_XLABEL_LENGTH) { 926 | size_t bytes_to_copy = MIN((size_t)(MAX_XLABEL_LENGTH - offset), size); 927 | memcpy(buf, p_open->x_label + offset, bytes_to_copy); 928 | buf += bytes_to_copy; 929 | offset_adj = offset + bytes_to_copy; 930 | offset += bytes_to_copy; 931 | } 932 | 933 | size_t bytes_to_read = size - (buf - buf_in); 934 | if (bytes_to_read > 0) { 935 | LOG_TRACE("read(%s, %ld, %ld)\n", path, offset - offset_adj, 936 | bytes_to_read); 937 | ssize_t bytes_read = pread(p_open->fh, buf, bytes_to_read, 938 | offset - offset_adj); 939 | if (bytes_read == -1) 940 | return -errno; 941 | buf += bytes_read; 942 | } 943 | 944 | return (int)(buf - buf_in); 945 | } 946 | 947 | /*============================================================================*/ 948 | 949 | static int notmuchfs_mkdir (const char* path, mode_t mode) 950 | { 951 | assert(path[0] == '/'); 952 | 953 | if (mkdir(path + 1, mode) == -1) 954 | return -errno; 955 | return 0; 956 | } 957 | 958 | /*============================================================================*/ 959 | 960 | static int notmuchfs_rmdir (const char* path) 961 | { 962 | assert(path[0] == '/'); 963 | 964 | if (rmdir(path + 1) == -1) 965 | return -errno; 966 | return 0; 967 | } 968 | 969 | /*============================================================================*/ 970 | 971 | static int notmuchfs_rename (const char* from, const char* to) 972 | { 973 | assert(from[0] == '/'); 974 | assert(to[0] == '/'); 975 | 976 | char *last_pslash_from = strrchr(from + 1, '#'); 977 | char *last_pslash_to = strrchr(to + 1, '#'); 978 | char *last_slash_from = strrchr(from + 1, '/'); 979 | char *last_slash_to = strrchr(to + 1, '/'); 980 | /* Values are 0 (no workaround), 1 or 2 (see below). */ 981 | unsigned mutt_2476_workaround = 0; 982 | 983 | if (last_pslash_from == NULL && last_pslash_to == NULL) { 984 | /* Renaming from a non-maildir name to another non-maildir name - just pass 985 | * it through. 986 | */ 987 | LOG_TRACE("rename(%s, %s)\n", from + 1, to + 1); 988 | 989 | if (rename(from + 1, to + 1) == 0) 990 | return 0; 991 | else 992 | return -errno; 993 | } 994 | 995 | if (last_pslash_from == NULL || last_pslash_to == NULL) { 996 | /* Renaming from a non-maildir name to a maildir name, or vice versa. 997 | * Doesn't make much sense - deny it. 998 | */ 999 | LOG_TRACE("ERROR: Rename die 1\n"); 1000 | return -ENOTSUP; 1001 | } 1002 | 1003 | if ((last_pslash_from - from) != (last_pslash_to - to)) { 1004 | /* Renaming from one maildir name to another, in different paths. */ 1005 | LOG_TRACE("ERROR: Rename die 2\n"); 1006 | return -ENOTSUP; 1007 | } 1008 | 1009 | if (strncmp(from, to, last_pslash_from - from) != 0) { 1010 | /* Renaming from one maildir name to another, in different paths, but the 1011 | * paths have the same length. 1012 | */ 1013 | 1014 | if (global_config.mutt_2476_workaround_allowed) { 1015 | /* The workaround here is to intercept all renames of the form: 1016 | * Case 1: 1017 | * rename(/real/path/cur/fake#maildir#cur#foofile, 1018 | * /real/path/new/fake#maildir#cur#barfile) 1019 | * and 1020 | * Case 2: 1021 | * rename(/real/path/new/fake#maildir#cur#foofile, 1022 | * /real/path/cur/fake#maildir#cur#barfile) 1023 | * and ignore the 'new' part - treat it as if the 'new' was 'cur'. 1024 | */ 1025 | 1026 | if ((last_slash_from - from) >= 3 && 1027 | memcmp(from, to, last_slash_from - from - 3) == 0) { 1028 | if (memcmp(last_slash_from - 3, "cur", 3) == 0 && 1029 | memcmp(last_slash_to - 3, "new", 3) == 0) { 1030 | /* Case 1. */ 1031 | mutt_2476_workaround = 1; 1032 | } 1033 | else if (memcmp(last_slash_from - 3, "new", 3) == 0 && 1034 | memcmp(last_slash_to - 3, "cur", 3) == 0) { 1035 | /* Case 2. */ 1036 | mutt_2476_workaround = 2; 1037 | } 1038 | 1039 | if (mutt_2476_workaround != 0) { 1040 | LOG_TRACE("Activating mutt_bug_2476 workaround for rename(%s, %s)\n", 1041 | from, to); 1042 | } 1043 | } 1044 | } 1045 | if (mutt_2476_workaround == 0) { 1046 | fprintf(stderr, "ERROR: Rename die 3 %s %s %ld\n", from, to, 1047 | (long int)(last_pslash_from - from)); 1048 | return -ENOTSUP; 1049 | } 1050 | } 1051 | 1052 | /* Renaming from one file name to another, both in the same (maildir) 1053 | * directory. 1054 | */ 1055 | char trans_name_from[PATH_MAX]; 1056 | strncpy(trans_name_from, last_slash_from + 1, PATH_MAX - 1); 1057 | trans_name_from[PATH_MAX - 1] = '\0'; 1058 | string_replace(trans_name_from, '#', '/'); 1059 | 1060 | char trans_name_to[PATH_MAX]; 1061 | strncpy(trans_name_to, last_slash_to + 1, PATH_MAX - 1); 1062 | trans_name_to[PATH_MAX - 1] = '\0'; 1063 | string_replace(trans_name_to, '#', '/'); 1064 | 1065 | LOG_TRACE("rename(%s, %s)\n", trans_name_from, trans_name_to); 1066 | if (rename(trans_name_from, trans_name_to) == -1) 1067 | return -errno; 1068 | 1069 | 1070 | /* Rename it in the notmuch database too. */ 1071 | int res = 0; 1072 | struct fuse_context *p_fuse_ctx = fuse_get_context(); 1073 | notmuch_context_t *p_ctx = 1074 | (notmuch_context_t *)p_fuse_ctx->private_data; 1075 | 1076 | database_open(p_ctx, TRUE); 1077 | 1078 | if (notmuch_database_begin_atomic(p_ctx->db) != NOTMUCH_STATUS_SUCCESS) { 1079 | res = -EIO; 1080 | } 1081 | else { 1082 | /* If renaming from/to the same name, skip this - it gets confused. The 1083 | * mutt bug 2476 workaround can cause this, but it's also legitimately 1084 | * possible. 1085 | */ 1086 | if (strncmp(trans_name_from, trans_name_to, PATH_MAX) != 0) { 1087 | LOG_TRACE("notmuch_database_add_message(%s)\n", trans_name_to); 1088 | if (notmuch_database_index_file(p_ctx->db, trans_name_to, NULL, NULL) != 1089 | NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) { 1090 | LOG_TRACE("WARNING: Did not find message in database: %s\n", 1091 | trans_name_to); 1092 | } 1093 | else { 1094 | LOG_TRACE("notmuch_database_remove_message(%s)\n", trans_name_from); 1095 | notmuch_status_t status = 1096 | notmuch_database_remove_message(p_ctx->db, trans_name_from); 1097 | if (status != NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID) { 1098 | LOG_TRACE("WARNING: Did not find old message in database: %s\n", 1099 | trans_name_from); 1100 | /* Continue, can't do anything about it anyway. */ 1101 | } 1102 | } 1103 | } 1104 | 1105 | /* Lookup the message again here to sync the maildir flags. Do *not* use 1106 | * the message returned by notmuch_database_add_message(), it seems to 1107 | * refer to the file name that is subsequently removed above. 1108 | */ 1109 | notmuch_message_t *p_message; 1110 | LOG_TRACE("rename notmuch lookup by name: %s\n", trans_name_to); 1111 | if (notmuch_database_find_message_by_filename(p_ctx->db, trans_name_to, 1112 | &p_message) == 1113 | NOTMUCH_STATUS_SUCCESS) { 1114 | /* We just put it there, it should still be there. */ 1115 | assert(p_message != NULL); 1116 | 1117 | LOG_TRACE("notmuch_message_maildir_flags_to_tags(%s)\n", trans_name_to); 1118 | notmuch_message_maildir_flags_to_tags(p_message); 1119 | 1120 | if (global_config.mutt_2476_workaround_allowed) { 1121 | /* If mutt just moved the file to 'new', add the 'unread' flag. 1122 | * notmuch_message_maildir_flags_to_tags() does not do this because it's 1123 | * somewhat against the interpretation of the maildir spec, but it is 1124 | * what mutt means. 1125 | */ 1126 | if (mutt_2476_workaround == 1) { 1127 | LOG_TRACE("notmuch_message_add_tag(%s, unread)\n", trans_name_to); 1128 | if (notmuch_message_add_tag(p_message, "unread") != 1129 | NOTMUCH_STATUS_SUCCESS) { 1130 | /* Ignore all errors. Flags will go slightly out of sync now until 1131 | * 'notmuch new' fixes them. 1132 | */ 1133 | } 1134 | } 1135 | } 1136 | notmuch_message_destroy(p_message); 1137 | } 1138 | else { 1139 | /* Ignore all errors. Flags will go slightly out of sync now until 1140 | * 'notmuch new' fixes them. 1141 | */ 1142 | } 1143 | if (notmuch_database_end_atomic(p_ctx->db) != NOTMUCH_STATUS_SUCCESS) 1144 | res = -EIO; 1145 | } 1146 | 1147 | database_close(p_ctx); 1148 | 1149 | return res; 1150 | } 1151 | 1152 | /*============================================================================*/ 1153 | 1154 | static int notmuchfs_unlink (const char* path) 1155 | { 1156 | char trans_name[PATH_MAX]; 1157 | 1158 | /* Ignore the initial '/' */ 1159 | assert(path[0] == '/'); 1160 | path++; 1161 | 1162 | char *last_pslash = strrchr(path, '#'); 1163 | 1164 | if (last_pslash != NULL) { 1165 | char *last_slash = strrchr(path, '/'); 1166 | 1167 | strncpy(trans_name, last_slash + 1, PATH_MAX - 1); 1168 | trans_name[PATH_MAX - 1] = '\0'; 1169 | string_replace(trans_name, '#', '/'); 1170 | 1171 | path = trans_name; 1172 | } 1173 | 1174 | if (global_config.delete_tag != NULL) { 1175 | struct fuse_context *p_fuse_ctx = fuse_get_context(); 1176 | notmuch_context_t *p_ctx = 1177 | (notmuch_context_t *)p_fuse_ctx->private_data; 1178 | 1179 | database_open(p_ctx, TRUE); 1180 | 1181 | LOG_TRACE("notmuch_database_find_message_by_filename(%s)\n", path); 1182 | notmuch_message_t *message; 1183 | notmuch_status_t status = 1184 | notmuch_database_find_message_by_filename(p_ctx->db, path, &message); 1185 | switch (status) { 1186 | case NOTMUCH_STATUS_SUCCESS: 1187 | break; 1188 | case NOTMUCH_STATUS_OUT_OF_MEMORY: 1189 | database_close(p_ctx); 1190 | return -ENOMEM; 1191 | default: 1192 | database_close(p_ctx); 1193 | return -EIO; 1194 | } 1195 | 1196 | LOG_TRACE("notmuch_message_add_tag(%s, %s)\n", path, 1197 | global_config.delete_tag); 1198 | if (notmuch_message_add_tag(message, global_config.delete_tag) != 1199 | NOTMUCH_STATUS_SUCCESS) { 1200 | /* Ignore all errors, there's nothing we can do about them anyway. This 1201 | * will mean a deleted message is 'resurrected', which the user can 1202 | * clearly see and retry. 1203 | */ 1204 | } 1205 | 1206 | database_close(p_ctx); 1207 | } 1208 | else { 1209 | LOG_TRACE("unlink(%s)\n", path); 1210 | if (unlink(path) != 0) 1211 | return -errno; 1212 | } 1213 | 1214 | return 0; 1215 | } 1216 | 1217 | /*============================================================================*/ 1218 | 1219 | static int notmuchfs_symlink (const char* to, const char* from) 1220 | { 1221 | assert(from[0] == '/'); 1222 | 1223 | if (symlink(to, from + 1) != 0) 1224 | return -errno; 1225 | return 0; 1226 | } 1227 | 1228 | /*============================================================================*/ 1229 | 1230 | static int notmuchfs_readlink (const char* path, char* buf, size_t size) 1231 | { 1232 | assert(path[0] == '/'); 1233 | 1234 | int res = readlink(path + 1, buf, size); 1235 | if (res >=0) { 1236 | buf[res] = '\0'; 1237 | res = 0; 1238 | } 1239 | else 1240 | res = -errno; 1241 | return res; 1242 | } 1243 | 1244 | /*============================================================================*/ 1245 | 1246 | static struct fuse_operations notmuchfs_oper = { 1247 | .init = notmuchfs_init, 1248 | .destroy = notmuchfs_destroy, 1249 | .getattr = notmuchfs_getattr, 1250 | .opendir = notmuchfs_opendir, 1251 | .releasedir = notmuchfs_releasedir, 1252 | .readdir = notmuchfs_readdir, 1253 | .open = notmuchfs_open, 1254 | .release = notmuchfs_release, 1255 | .read = notmuchfs_read, 1256 | .mkdir = notmuchfs_mkdir, 1257 | .rmdir = notmuchfs_rmdir, 1258 | .rename = notmuchfs_rename, 1259 | .unlink = notmuchfs_unlink, 1260 | .symlink = notmuchfs_symlink, 1261 | .readlink = notmuchfs_readlink 1262 | }; 1263 | 1264 | /*============================================================================*/ 1265 | 1266 | /** 1267 | * Option key types used in the CLI parser. 1268 | */ 1269 | 1270 | enum { 1271 | KEY_HELP, 1272 | KEY_VERSION, 1273 | }; 1274 | 1275 | #define NOTMUCHFS_OPT(t, p, v) { t, offsetof(struct notmuchfs_config, p), v } 1276 | 1277 | static struct fuse_opt notmuchfs_opts[] = { 1278 | NOTMUCHFS_OPT("backing_dir=%s", backing_dir, 0), 1279 | NOTMUCHFS_OPT("mail_dir=%s", mail_dir, 0), 1280 | NOTMUCHFS_OPT("delete_tag=%s", delete_tag, 0), 1281 | NOTMUCHFS_OPT("mutt_2476_workaround", mutt_2476_workaround_allowed, 1), 1282 | NOTMUCHFS_OPT("nomutt_2476_workaround", mutt_2476_workaround_allowed, 0), 1283 | NOTMUCHFS_OPT("--mutt_2476_workaround=true", mutt_2476_workaround_allowed, 1), 1284 | NOTMUCHFS_OPT("--mutt_2476_workaround=false", mutt_2476_workaround_allowed, 0), 1285 | 1286 | FUSE_OPT_KEY("-V", KEY_VERSION), 1287 | FUSE_OPT_KEY("--version", KEY_VERSION), 1288 | FUSE_OPT_KEY("-h", KEY_HELP), 1289 | FUSE_OPT_KEY("--help", KEY_HELP), 1290 | FUSE_OPT_END 1291 | }; 1292 | 1293 | static void print_notmuchfs_usage (char *arg0) { 1294 | fprintf(stderr, 1295 | "Usage: %s mountpoint -o backing_dir=PATH -o mail_dir=PATH [options]\n" 1296 | "\n" 1297 | "General options:\n" 1298 | " -o opt,[opt...] mount options\n" 1299 | " -h --help print help\n" 1300 | " -V --version print version\n" 1301 | "\n" 1302 | "Notmuchfs options:\n" 1303 | " -o backing_dir=PATH Path to backing directory (required)\n" 1304 | " -o mail_dir=PATH Path to parent directory of notmuch database (required)\n" 1305 | " -o delete_tag=TAG Tag to apply when a mail is deleted\n" 1306 | " -o mutt_2476_workaround\n" 1307 | " -o nomutt_2476_workaround (default)\n" 1308 | , arg0); 1309 | } 1310 | 1311 | 1312 | static int notmuchfs_opt_proc (void *data, 1313 | const char *arg, 1314 | int key, 1315 | struct fuse_args *outargs) 1316 | { 1317 | (void)data; 1318 | (void)arg; 1319 | switch (key) { 1320 | case KEY_HELP: 1321 | print_notmuchfs_usage(outargs->argv[0]); 1322 | fuse_opt_add_arg(outargs, "-ho"); 1323 | fuse_main(outargs->argc, outargs->argv, ¬muchfs_oper, NULL); 1324 | exit(1); 1325 | 1326 | case KEY_VERSION: 1327 | fprintf(stderr, "Notmuchfs version %s\n", NOTMUCHFS_VERSION); 1328 | fuse_opt_add_arg(outargs, "--version"); 1329 | fuse_main(outargs->argc, outargs->argv, ¬muchfs_oper, NULL); 1330 | exit(0); 1331 | } 1332 | return 1; 1333 | } 1334 | 1335 | /*============================================================================*/ 1336 | 1337 | int main(int argc, char *argv[]) 1338 | { 1339 | struct fuse_args args = FUSE_ARGS_INIT(argc, argv); 1340 | 1341 | fuse_opt_parse(&args, &global_config, notmuchfs_opts, notmuchfs_opt_proc); 1342 | 1343 | if (global_config.backing_dir == NULL || 1344 | global_config.mail_dir == NULL) { 1345 | fprintf(stderr, "Required option(s) missing. See \"%s --help\".\n", 1346 | args.argv[0]); 1347 | exit(1); 1348 | } 1349 | 1350 | struct stat stbuf; 1351 | if (stat(global_config.backing_dir, &stbuf) != 0 || 1352 | !S_ISDIR(stbuf.st_mode)) { 1353 | fprintf(stderr, "Can't find backing dir \"%s\".\n", 1354 | global_config.backing_dir); 1355 | exit(1); 1356 | } 1357 | 1358 | if (stat(global_config.mail_dir, &stbuf) != 0 || 1359 | !S_ISDIR(stbuf.st_mode)) { 1360 | fprintf(stderr, "Can't find mail dir \"%s\".\n", 1361 | global_config.mail_dir); 1362 | exit(1); 1363 | } 1364 | 1365 | 1366 | int ret = fuse_main(args.argc, args.argv, ¬muchfs_oper, 1367 | NULL /* userdata */); 1368 | fuse_opt_free_args(&args); 1369 | return ret; 1370 | } 1371 | 1372 | /*============================================================================*/ 1373 | -------------------------------------------------------------------------------- /tests/include: -------------------------------------------------------------------------------- 1 | NOTMUCHFS=../notmuchfs 2 | TEST_ROOT=`pwd`/testdir/ 3 | 4 | die() { 5 | echo $1 6 | exit 1; 7 | } 8 | -------------------------------------------------------------------------------- /tests/main.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This is a terribly rough little test harness for some basic functionality in 3 | # notmuchfs. 4 | # 5 | # This file is part of notmuchfs 6 | # 7 | # Notmuchfs is free software, released under the GNU General Public 8 | # License version 3 (or later). 9 | # 10 | # Copyright © 2012 Tim Stoakes 11 | ################################################################################ 12 | 13 | . "include" || exit 1 14 | 15 | set -x 16 | 17 | trap "cleanup" SIGINT SIGTERM EXIT 18 | 19 | function cleanup { 20 | fusermount -u "$TEST_ROOT/mount" 21 | rm -Rf "$TEST_ROOT" 22 | } 23 | 24 | mkdir -p "$TEST_ROOT" 25 | mkdir -p "$TEST_ROOT/backing" 26 | mkdir -p "$TEST_ROOT/mount" 27 | 28 | "$NOTMUCHFS" "$TEST_ROOT/mount" \ 29 | -o backing_dir="$TEST_ROOT/backing" \ 30 | -o mail_dir=~/.maildir/ || die "mount notmuchfs" 31 | 32 | 33 | ls -al "$TEST_ROOT" >/dev/null || die "list empty root" 34 | 35 | QUERY="tag:work and from:admin" 36 | 37 | mkdir "$TEST_ROOT/mount/$QUERY" || die "mkdir" 38 | test -d "$TEST_ROOT/mount/$QUERY" || die "dir exists 1" 39 | test -d "$TEST_ROOT/mount/$QUERY/cur" || die "dir exists 2" 40 | test -d "$TEST_ROOT/mount/$QUERY/new" || die "dir exists 3" 41 | test -d "$TEST_ROOT/mount/$QUERY/tmp" || die "dir exists 4" 42 | 43 | # Read all the message IDs of the query results, both from notmuchfs and 44 | # directly from notmuch. Compare them, they should be the same. 45 | # 46 | # Use message IDs here instead of files to avoid being tripped up by multiple 47 | # files with the same message ID. 48 | cat "$TEST_ROOT/mount/$QUERY/cur/"* | formail -d -xMessage-id: -s | tr -d "<>" | sort > out1 49 | notmuch search --output=messages "$QUERY" | sed s/id:/\ / | sort > out2 50 | wc -l out1 51 | wc -l out2 52 | diff out1 out2 || die "diff" 53 | rm -f out1 out2 54 | 55 | 56 | SAVEIFS=$IFS 57 | IFS=$(echo -en "\n\b") 58 | for FILE in `ls -1 "$TEST_ROOT/mount/$QUERY/cur/"*`; do 59 | ID=`cat "$FILE" | formail -d -xMessage-id: -s | tr -d "<> "` 60 | # Check that notmuch tags match X-Label: tags. 61 | TAGS=`notmuch search --output=tags "id:$ID" | tr "\\n" "," |sed s/,\$//` 62 | XLABEL=`head -n 1 "$FILE" | sed s/X-Label:\ // | sed -e "s/\s\+$//" | tr -d "\\r\\n"` 63 | [ "$TAGS" == "$XLABEL" ] || die "tags don't match X-Label: \"$TAGS\" vs. \"$XLABEL\""; 64 | 65 | # Check that the rest of the message matches the original ie. notmuchfs 66 | # didn't alter it. 67 | tail -n +2 "$FILE" > out1 68 | notmuch show --format=raw "id:$ID" > out2 69 | wc -l out1 70 | wc -l out2 71 | diff out1 out2 || die "non-tag file content does not match" 72 | rm -f out1 out2 73 | done 74 | IFS=$SAVEIFS 75 | 76 | rmdir "$TEST_ROOT/mount/$QUERY" || die "rmdir" 77 | 78 | echo "Success!" 79 | exit 0 80 | -------------------------------------------------------------------------------- /version: -------------------------------------------------------------------------------- 1 | 0.3 2 | --------------------------------------------------------------------------------