From: Norbert Preining Date: Sat, 23 Jan 2021 16:01:10 +0000 (+0900) Subject: Import maildir-utils_1.4.15.orig.tar.gz X-Git-Tag: archive/raspbian/1.12.9-1+rpi1~1^2^2^2~8 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=f8da9c83029a5e7c36f6df3eaa0b28871112ec07;p=maildir-utils.git Import maildir-utils_1.4.15.orig.tar.gz [dgit import orig maildir-utils_1.4.15.orig.tar.gz] --- f8da9c83029a5e7c36f6df3eaa0b28871112ec07 diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 0000000..3a491ef --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,2 @@ +((emacs-lisp-mode + (indent-tabs-mode . nil))) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d7b5d39 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,36 @@ +#-*-mode:conf-*- +# editorconfig file (see EditorConfig.org), with some +# lowest-denominator settings that should work for many editors. + +root = true # this is the top-level + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +trim_trailing_whitespace = true + +# The "best" answer is "tabs-for-indentation; spaces for alignment", but in +# practice that's hard to accomplish in many editors. +# +# So we use spaces instead, at least that looks consistent for all + +[*.{cc,cpp,hh,hpp}] +indent_style = space +indent_size = 8 +max_line_length = 100 + +[*.{c,h}] +indent_style = space +indent_size = 8 +max_line_length = 80 + +[configure.ac] +indent_style = space +indent_size = 4 +max_line_length = 100 + +[Makefile.am] +indent_style = tab +indent_size = 8 +max_line_length = 100 diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 0000000..4527979 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,20 @@ +## Expected or desired behavior + +Please describe the behavior you expected / want + +## Actual behavior + +Please describe the behavior you are actually seeing. + +For bug-reports, if applicable, include error messages, emacs stack +traces etc. Try to be as specific as possible - when do you see this +happening? Does it happen always? Sometimes? How often? + +## Steps to reproduce + +For bug-reports, please describe in as much details as possible how +one can reproduce the problem*. + +## Versions of mu, mu4e/emacs, operating system etc. + +## Any other detail diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62e23bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ +www/mu +mug +/mu/mu +/mu/mu-help-strings.h +mug2 +.desktop +*html +.deps +.libs +autom4te* +Makefile +Makefile.in +INSTALL +aclocal.m4 +config.* +configure +install-sh +depcomp +libtool +ltmain.sh + +# Added automatically by `autoreconf` +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 + +missing +nohup.out +vgdump +stamp-h1 +GPATH +GRTAGS +GSYMS +GTAGS +*.lo +*.o +*.la +*.x +*.go +*.gz +*.bz2 +\#* +*.aux +*.cp +*.fn +*.info +*.ky +*.log +*.pg +*.toc +*.tp +*.vr +*.elc +*.gcda +*.gcno +*.trs +*.exe +*.lib +aminclude_static.am +elisp-comp +elc-stamp +dummy.cc +msg2pdf +gmime-test +test-mu-cmd +test-mu-cmd-cfind +test-mu-contacts +test-mu-container +test-mu-date +test-mu-flags +test-mu-maildir +test-mu-msg +test-mu-msg-fields +test-mu-query +test-mu-runtime +test-mu-store +test-mu-str +test-mu-threads +test-mu-util +test-parser +test-tokenizer +test-utils +tokenize +test-command-parser +test-mu-utils +test-sexp-parser +test-scanner +/guile/tests/test-mu-guile + +mu4e-meta.el +mu4e.pdf +texinfo.tex +texi.texi +*.tex +*.pdf +/www/auto/* +configure.lineno +/test.xml +/mu4e/mdate-sh +/mu4e/mu4e-about.el +/mu4e/stamp-vti +/mu4e/version.texi +/lib/doxyfile +/version.texi +/compile +build-aux/ +/TAGS +parse + +*_flymake.* +*_flymake_* +/perf.data +perf.data +perf.data.old +*vgdump +/lib/asan.log* +/man/mu-mfind.1 +/mu/mu-memcheck +mu-*-coverage +mu*tar.xz +compile_commands.json diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..3a54641 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Dirk-Jan C. Binnema diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..063324a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,35 @@ +language: c +sudo: required +compiler: + - gcc +env: + global: + - BUILD_PKGS="libtool autoconf autoconf-archive automake texinfo" + - BUILD_LIBS="libgmime-2.6-dev libxapian-dev guile-2.0-dev libwebkitgtk-dev" + - TEST_PKGS="pmccabe" + matrix: + - EVM_EMACS=emacs-24.1-bin + - EVM_EMACS=emacs-24.2-bin + - EVM_EMACS=emacs-24.3-bin + # - EVM_EMACS=emacs-24.5-travis + # - EVM_EMACS=emacs-25.1-travis +before_install: + - git submodule update --init --recursive + # The Ubuntu version on travis is way too old, need Autoconf 2.69 + - sudo add-apt-repository ppa:dns/gnu -y + - sudo apt-get -qq update + - sudo apt-get install -qq ${BUILD_PKGS} ${BUILD_LIBS} ${TEST_PKGS} +install: + - sudo mkdir /usr/local/evm + - sudo chown $(id -u):$(id -g) /usr/local/evm + - curl -fsSkL https://raw.github.com/rejeep/evm/master/go | bash + - export PATH="$HOME/.evm/bin:$PATH" + - evm install $EVM_EMACS --use +script: + # Need recent version of autoconf-archive + - curl http://nl.mirror.babylon.network/gnu/autoconf-archive/autoconf-archive-2016.09.16.tar.xz -o /tmp/aa.tar.xz && tar xf /tmp/aa.tar.xz + - cp autoconf-archive-2016.09.16/m4/*.m4 m4/ + - ./autogen.sh + - ./configure + - make + - make check diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..3a54641 --- /dev/null +++ b/AUTHORS @@ -0,0 +1 @@ +Dirk-Jan C. Binnema diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..eee38d2 --- /dev/null +++ b/ChangeLog @@ -0,0 +1 @@ +See NEWS.org diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..4d8ce94 --- /dev/null +++ b/HACKING @@ -0,0 +1,144 @@ +* HACKING + + Here are some guidelines for hacking on the 'mu' source code. + + This is a fairly long list -- this is not meant to discourage anyone from + working on mu; I think most of the rules are common sense anyway, and some of + the more stylistic-aesthetic rules are clearly visible in current source code, + so as long as any new code 'fits in', it should go a long way in satisfying + them. + + I should add some notes for the Lisp/Scheme code as well... + +** Coding style + + For consistency and, more important, to keep things understandable, mu + attempts to follow the following rules: + + 1. Basic code layout is like in the Linux kernel coding style. Keep the '{' + on the same line as the statement, except for functions. Tabs for + indentation, space for alignment; use 8-char tabs. + + 2. Lines should not exceed 80 characters (C) or 100 characters (C++) + + 3. Functions should not exceed 35 lines (with few exceptions). You can easily + check if any functions violate this rule with 'make line35', which lists + all functions with more than 35 non-comment lines. + + 4. Source files should not exceed 1000 lines + + 5. A function's cyclomatic complexity should not exceed 10 (there could be + rare exceptions, see the toplevel ~Makefile.am~). You can test the + cyclomatic complexity with the ~pmccabe~ tool; if you installed that, you + can use 'make cc10' to list all functions that violate this rule; there + should be none. + + 6. Filenames have their components separated with dashes (e.g, ~mu-log.h~), and + start with ~mu-~ where appropriate. + + 7. Global functions have the prefix based on their module, e.g., ~mu-foo.h~ + declares a function of 'mu_foo_bar (int a);', mu-foo.c implements this. + + 8. Non-global functions *don't* have the module prefix, and are declared + static. + + 9. Functions have their return type on a separate line before the function + name, so: +#+BEGIN_EXAMPLE + int + foo (const char *bar) + { + .... + } +#+END_EXAMPLE + + There is no non-aesthetic reason for this. + + 10. In C code, variable-declarations are at the beginning of a block; in + principle, C++ follows that same guideline, unless for heavy yet + uncertain initializations following RAII. + + In C code, the declaration does *not* initialize the variable. This will + give the compiler a chance to warn us if the variable is not initialized + in a certain code path. + + 11. Returned strings of type char* must be freed by the caller; if they are + not to be freed, 'const char*' should be used instead + + 12. Functions calls have a space between function name and arguments, unless + there are none, so: + + ~foo (12, 3)~; + + and + + ~bar();~ + + after a comma, a space should follow. + + 13. Functions that do not take arguments are explicitly declared as + f(void) and not f(). Reason: f() means that the arguments are + /unspecified/ (in C) + + 14. C-code should not use ~//~ comments. + + +** Logging + + For logging, mu uses the GLib logging functions/macros as listed below, + except when logging may not have been initialized. + + The logging system redirects most logging to the log file (typically, + ~/.cache/mu/mu.log). g_warning, g_message and g_critical are shown to the user, + except when running with --quiet, in which case g_message is *not* shown. + + - ~g_message~ is for non-error messages the user will see (unless running with + ~--quiet~) + - ~g_warning~ is for problems the user may be able to do something about (and + they are written on ~stderr~) + - ~g_critical~ is for mu bugs, serious, internal problems (~g_return_if_fail~ and + friends use this). (and they are written on ~stderr~) + - don't use ~g_error~ + + If you just want to log something in the log file without writing to screen, + use ~MU_LOG_WRITE~, as defined in ~mu-util.h~. + +** Compiling from git + + For hacking, you're strongly advised to use the latest git version. + Compilation from git should be straightforward, if you have the right tools + installed. + +*** dependencies + + You need to install a few dependencies; e.g. on Debian/Ubuntu: +#+BEGIN_EXAMPLE + sudo apt-get install \ + automake \ + autoconf-archive \ + autotools-dev \ + libglib2.0-dev \ + libxapian-dev \ + libgmime-3.0-dev \ + m4 \ + make \ + libtool \ + pkg-config +#+END_EXAMPLE + + Then, to compile straight from ~git~: + +#+BEGIN_EXAMPLE + $ git clone https://github.com/djcb/mu + $ cd mu + $ ./autogen.sh + $ make +#+END_EXAMPLE + + You only need to run ~./autogen.sh~ the first time and after changes in the + build system; otherwise you can use ~./configure~. + +# Local Variables: +# mode: org; org-startup-folded: nofold +# fill-column: 80 +# End: diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..f62c2e7 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,61 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +if BUILD_GUILE +guile=guile +else +guile= +endif + +if BUILD_MU4E +mu4e=mu4e +else +mu4e= +endif + +SUBDIRS=m4 man lib $(guile) mu $(mu4e) contrib toys + +ACLOCAL_AMFLAGS=-I m4 + +# so we can say 'make test' +check: test cleanupnote + +cleanupnote: + @echo -e "\nNote: you can remove the mu-test- dir in your tempdir" + @echo "after 'make check' has finished." + +tags: + gtags + +EXTRA_DIST= \ + TODO \ + HACKING \ + README \ + gtest.mk \ + NEWS \ + NEWS.org \ + autogen.sh + +doc_DATA = \ + NEWS.org + +include $(top_srcdir)/aminclude_static.am + +CODE_COVERAGE_IGNORE_PATTERN= \ + '/usr/*' \ + '*test-*' diff --git a/NEWS b/NEWS new file mode 100644 index 0000000..23e9add --- /dev/null +++ b/NEWS @@ -0,0 +1,2 @@ +See NEWS.org + diff --git a/NEWS.org b/NEWS.org new file mode 100644 index 0000000..28bb44f --- /dev/null +++ b/NEWS.org @@ -0,0 +1,868 @@ +#+STARTUP:showall +* NEWS (user visible changes) + +* 1.4 (released, as of April 18 2020) + +*** mu + + - mu now defaults to the [[https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html][XDG Base Directory Specification]] for the default + locations for various files. E.g. on Unix the mu database now lives under + ~~/.cache/mu/~ rather than ~~/.mu~. You can still use the old location by + passing ~--muhome=~/.mu~ to various ~mu~ commands, or setting ~(setq + mu4e-mu-home "~/.mu")~ for ~mu4e~. + + If your ~~/.cache~ is volatile (e.g., is cleared on reboot), you may want + use ~--muhome~. Some mailing-list dicussion suggest that's fairly rare + though. + + After upgrading, you may wish to delete the files in the old location to + recover some diskspace. + + - There's a new subcommand ~mu init~ to initialize the mu database, which + takes the ~--maildir~ and ~--my-address~ parameters that ~index~ used to take. + These parameters are persistent so ~index~ does not need (or accept) them + anymore. ~mu4e~ now depends on those parameters. + + ~init~ only needs to be run once or when changing these parameters. That + implies that you need to re-index after changing these parameters. The + ~.noupdate~ files are ignored when indexing the first time after ~mu init~ (or + in general, when the database is empty). + + - There is another new subcommand ~mu info~ to get information about the mu + database, the personal addresses etc. + + - The contacts cache (which is used by ~mu cfind~ and ~mu4e~'s + contact-completion) is now stored as part of the Xapian database rather + than as a separate file. + + - The ~--xbatchsize~ and ~--autoupgrade~ options for indexing are gone; both are + determined implicitly now. + +*** mu4e + + - ~mu4e~ no longer uses the ~mu4e-maildir~ and ~mu4e-user-mail-address-list~ + variables; instead it uses the information it gets from ~mu~ (see the ~mu~ + section above). If you have a non-default ~mu4e-mu-home~, make sure to set + it before ~mu4e~ starts. + + It is strongly recommended that you run ~mu init~ with the appropriate + parameters to (re)initialize the Xapian database, as mentioned in the + mu-section above. + + The main screen shows your address(es), and issues a warning if + ~user-email-address~ is not part of that (and refer you to ~mu init~). You can + avoid the addresses in the main screen and the warning by setting + ~mu4e-main-view-hide-addresses~ to non-nil. + + - In many cases, ~mu4e~ used to receive /all/ contacts after each indexing + operation; this was slow for some users, so we have updated this to /only/ + get the contacts that have changed since the last round. + + We also moved sorting the contacts to the mu-side, which speeds things up + further. However, as a side-effect of this, ~mu4e-contact-rewrite-function~ + and ~mu4e-compose-complete-ignore-address-regexp~ have been obsoleted; users + of those should migrate to ~mu4e-contact-process-function~; see its + docstring for details. + + - Christophe Troestler contributed support for Gnus' calender-invitation + handling in mu4e (i.e., you should be able to accept/reject invitations + etc.). It's very fresh code, and likely it'll be tweaked in the future. + But it's available now for testing. Note that this requires the gnus-based + viewer, as per ~(setq mu4e-view-use-gnus t)~ + + - In addition, he added support for custom headers, so the ones for for the + non-gnus-view should work just as well. + + - ~org-mode~ support is enabled by default now. ~speedbar~ support is disabled + by default. The support org functionality has been moved to ~mu4e-org.el~, + with ~org-mu4e.el~ remaining for older things. + + - ~mu4e~ now adds message-ids to messages when saving drafts, so we can find + them even with ~mu4e-headers-skip-duplicates~. + + - Bookmarks (as in ~mu4e-bookmarks~) are now simple plists (instead of cl + structs). ~make-mu4e-bookmark~ has been updated to produce such plists (for + backward compatibility). A bookmark now looks like a list of e.g. ~(:name + "My bookmark" :query "banana OR pear" :key ?f)~ this format is a bit easier + extensible. + + - ~mu4e~ recognizes an attribute ~:hide t~, which will hide the bookmark item + from the main-screen (and speedbar), but keep it available through the + completion UI. + + - ~mu4e-maildir-shortcuts~ have also become plists. The older format is still + recognized for backward compatibility, but you are encouraged to upgrade. + + - Replying to mailing-lists has been improved, allowing for choosing for + replying to all, sender, list-only. + + - A very visible change, ~mu4e~ now shows unread/all counts for bookmarks in + the main screen that are strings. This is on by default, but can be + disabled by setting ~:hide-unread~ in the bookmark ~plist~ to ~t~. For + speed-reasons, these counts do _not_ filter out duplicates nor messages that + have been removed from the filesystem. + + - ~mu4e-attachment-dir~ now also applies to composing messages; it determines + the default directory for inclusion. + + - The mu4e <-> mu interaction has been rewritten to communicate using + s-expressions, with a repl for testing. + +*** toys + + - Updated the ~mug~ toy UI to use Webkit2/GTK+. Note that this is just a toy + which is not meant for distribution. ~msg2pdf~ is disabled for now. + + +*** How to upgrade mu4e + + - upgrade ~mu~ to the latest stable version (1.4.x) + + - shut down emacs + + - Run ~mu init~ in a terminal + + - Make sure ~mu init~ points to the right Maildir folder and add your email + address(es) the following way: + + ~mu init --maildir=~/Maildir --my-address=jim@example.com --my-address=bob@example.com~ + + - once this is done, run ~mu index~ + + - Don't forget to delete your old mail cache location if necessary (see + release notes for more detail). + + +** 1.2 + + After a bit over a year since version 1.0, here is version 1.2. This is + mostly a bugfix release, but there are also a number of new features. + +*** mu + + - Substantial (algorithmic) speed-up of message-threading; this also (or + especially) affects mu4e, since threading is the default. See commit + eb9bfbb1ca3c for all the details, and thanks to Nicolas Avrutin. + + - The query-parser now generates better queries for wildcard searches, by + using the Xapian machinery for that (when available) rather than + transforming into regexp queries. + + - The perl backend is hardly used and will be removed; for now we just + disable it in the build. + + - Allow outputting messages in json format, closely following the sexp + output. This adds an (optional) dependency on the Json-Glib library. + +*** mu4e + + - Bump the minimal required emacs version to 24.4. This was already de-facto + true, now it is enforced. + + - In mu4e-bookmarks, allow the `:query` element to take a function (or + lambda) to dynamically generate the query string. + + - There is a new message-view for mu4e, based on the Gnus' article-view. + This bring a lot of (but not all) of the very rich Gnus article-mode + feature-set to mu4e, such as S/MIME-support, syntax-highlighting, + + For now this is experimental ("tech preview"), but might replace the + current message-view in a future release. Enable it with: + (setq mu4e-view-use-gnus t) + + Thanks to Christophe Troestler for his work on fixing various encoding + issues. + + - Many bug fixes + +*** guile + + - Now requires guile 2.2. + +*** Contributors for this release: + + Ævar Arnfjörð Bjarmason, Albert Krewinkel, Alberto Luaces, Alex Bennée, Alex + Branham, Alex Murray, Cheong Yiu Fung, Chris Nixon, Christian Egli, + Christophe Troestler, Dirk-Jan C. Binnema, Eric Danan, Evan Klitzke, Ian + Kelling, ibizaman, James P. Ascher, John Whitbeck, Junyeong Jeong, Kevin + Foley, Marcelo Henrique Cerri, Nicolas Avrutin, Oleh Krehel, Peter W. V. + Tran-Jørgensen, Piotr Oleskiewicz, Sebastian Miele, Ulrich Ölmann, + +** 1.0 + + After a decade of development, *mu 1.0*! + + Note: the new release requires a C++14 capable compiler. + +*** mu + + - New, custom query parser which replaces Xapian's 'QueryParser' + both in mu and mu4e. Existing queries should still work, but the new + engine handles non-alphanumeric queries much better. + - Support regular expressions in queries (with the new query engine), + e.g. "subject:/foo.*bar/". See the new `mu-query` and updated `mu-easy` + manpages for examples. + - cfind: ensure nicks are unique + - auxiliary programs invoked from mu/mu4e survive terminating the + shell / emacs + +*** mu4e + + - Allow for rewriting message bodies + - Toggle-menus for header settings + - electric-quote-(local-)mode work when composing emails + - Respect format=flowed and delsp=yes for viewing plain-text + messages + - Added new mu4e-split-view mode: single-window + - Add menu item for `untrash'. + - Unbreak abbrevs in mu4e-compose-mode + - Allow forwarding messages as attachments + (`mu4e-compose-forward-as-attachment') + - New defaults: default to 'skip duplicates' and 'include related' + in headers-view, which should be good defaults for most users. Can be + customized using `mu4e-headers-skip-duplicates' and + `mu4e-headers-include-related', respectively. + - Many bug fixed (see github for all the details). + - Updated documentation + +*** Contributors for this release: + + Ævar Arnfjörð Bjarmason, Alex Bennée, Arne Köhn, Christophe Troestler, + Damien Garaud, Dirk-Jan C. Binnema, galaunay, Hong Xu, Ian Kelling, John + Whitbeck, Josiah Schwab, Jun Hao, Krzysztof Jurewicz, maxime, Mekeor Melire, + Nathaniel Nicandro, Ronald Evers, Sean 'Shaleh' Perry, Sébastien Le + Callonnec, Stig Brautaset, Thierry Volpiatto, Titus von der Malsburg, + Vladimir Sedach, Wataru Ashihara, Yuri D'Elia. + + And all the people on the mailing-list and in github, with bug reports, + questions and suggestions. + + +** 0.9.18 + + New development series which will lead to 0.9.18. + +*** mu + + - Increase the default maximum size for messages to index to 500 + Mb; you can customize this using the --max-msg-size parameter to mu index. + - implement "lazy-checking", which makes mu not descend into + subdirectories when the directory-timestamp is up to date; greatly speeds + up indexing (see --lazy-check) + - prefer gpg2 for crypto + - fix a crash when running on OpenBSD + - fix --clear-links (broken filenames) + - You can now set the MU_HOME environment variable as an + alternative way of setting the mu homedir via the --muhome command-line + parameter. + +*** mu4e + +**** reading messages + + - Add `mu4e-action-view-with-xwidget`, and action for viewing + e-mails inside a Webkit-widget inside emacs (requires emacs 25.x with + xwidget/webkit/gtk3 support) + - Explicitly specify utf8 for external html viewing, so browsers + can handle it correctly. + - Make `shr' the default renderer for rich-text emails (when + available) + - Add a :user-agent field to the message-sexp (in mu4e-view), which + is either the User-Agent or X-Mailer field, when present. + +**** composing messages + + - Cleanly handle early exits from message composition as well as while + composing. + - Allow for resending existing messages, possibly editing them. M-x + mu4e-compose-resend, or use the menu; no shortcut. + - Better handle the closing of separate compose frames + - Improved font-locking for the compose buffers, and more extensive + checks for cited parts. + - automatically sign/encrypt replies to signed/encrypted messages + (subject to `mu4e-compose-crypto-reply-policy') + +**** searching & marking + + - Add a hook `mu4e-mark-execute-pre-hook`, which is run just before + executing marks. + - Just before executing any search, a hook-function + `mu4e-headers-search-hook` is invoked, which receives the search + expression as its parameter. + - In addition, there's a `mu4e-headers-search-bookmark-hook` which + gets called when searches get invoked as a bookmark (note that + `mu4e-headers-search-hook` will also be called just afterwards). This + hook also receives the search expression as its parameter. + - Remove the 'z' keybinding for leaving the headers + view. Keybindings are precious! + - Fix parentheses/precedence in narrowing search terms + +**** indexing + + - Allow for indexing in the background; see + `mu4e-index-update-in-background`. + - Better handle mbsync output in the update buffer + - Add variables mu4e-index-cleanup and mu4e-index-lazy to enable + lazy checking from mu4e; you can sit from mu4e using something like: + #+BEGIN_SRC elisp +(setq mu4e-index-cleanup nil ;; don't do a full cleanup check + mu4e-index-lazy-check t) ;; don't consider up-to-date dirs #+END_SRC + +**** misc + + - don't overwrite global-mode-string, append to it. + - Make org-links (and more general, all users of + mu4e-view-message-with-message-id) use a headers buffer, then view the + message. This way, those linked message are just like any other, and can + be deleted, moved etc. + - Support org-mode 9.x + - Improve file-name escaping, and make it support non-ascii filenames + - Attempt to jump to the same messages after a re-search update operation + - Add action for spam-filter options + - Let `mu4e~read-char-choice' become case-insensitive if there is + no exact match; small convenience that affects most the single-char + option-reading in mu4e. + +*** Perl + + - an experimental Perl binding ("mup") is available now. See + perl/README.md for details. + +** Contributors: + + Aaron LI, Abdo Roig-Maranges, Ævar Arnfjörð Bjarmason, Alex Bennée, Allen, + Anders Johansson, Antoine Levitt, Arthur Lee, attila, Charles-H. Schulz, + Christophe Troestler, Chunyang Xu, Dirk-Jan C. Binnema, Jakub Sitnicki, + Josiah Schwab, jsrjenkins, Jun Hao, Klaus Holst, Lukas Fürmetz, Magnus + Therning, Maximilian Matthe, Nicolas Richard, Piotr Trojanek, Prashant + Sachdeva, Remco van 't Veer, Stephen Eglen, Stig Brautaset, Thierry + Volpiatto, Thomas Moulia, Titus von der Malsburg, Yuri D'Elia, Vladimir + Sedach + +* Old news + :PROPERTIES: + :VISIBILITY: folded + :END: + +** 0.9.16 + +*** Release + + 2016-01-20: Release from the 0.9.15 series + +*** Contributors: + + Adam Sampson, Ævar Arnfjörð Bjarmason, Bar Shirtcliff, Charles-H. Schulz, + Clément Pit--Claudel, Damien Cassou, Declan Qian, Dima Kogan, Dirk-Jan C. + Binnema, Foivos S. Zakkak, Hinrik Örn Sigurðsson, Jeroen Tiebout, JJ Asghar, + Jonas Bernoulli, Jun Hao, Martin Yrjölä, Maximilian Matthé, Piotr Trojanek, + prsarv, Thierry Volpiatto, Titus von der Malsburg + + (and of course all people who reported issues, provided suggestions etc.) + +** 0.9.15 + + - bump version to 0.9.15. From now on, odd minor version numbers + are for development versions; thus, 0.9.16 is to be the next stable + release. + - special case text/calendar attachments to get .vcs + extensions. This makes it easier to process those with external tools. + - change the message file names to better conform to the maildir + spec; this was confusing some tools. + - fix navigation when not running in split-view mode + - add `mu4e-view-body-face', so the body-face for message in the + view can be customized; e.g. (set-face-attribute 'mu4e-view-body-face nil + :font "Liberation Serif-10") + - add `mu4e-action-show-thread`, an action for the headers and view + buffers to search for messages in the same thread as the current one. + - allow for transforming mailing-list names for display, using + `mu4e-mailing-list-patterns'. + - some optimizations in indexing (~30% faster in some cases) + - new variable mu4e-user-agent-string, to customize the User-Agent: + header. + - when removing the "In-reply-to" header from replies, mu4e will + also remove the (hidden) References header, effectively creating a new + message-thread. + - implement 'mu4e-context', for defining and switching between + various contexts, which are groups of settings. This can be used for + instance for switch between e-mail accounts. See the section in the manual + for details. + - correctly decode mailing-list headers + - allow for "fancy" mark-characters; and improve the default set + - by default, the maildirs are no longer cached; please see the + variable ~mu4e-cache-maildir-list~ if you have a lot of maildirs and it + gets slow. + - change the default value for + ~org-mu4e-link-query-in-headers-mode~ to ~nil~, ie. by default link to the + message, not the query, as this is usually more useful behavior. + - overwrite target message files that already exist, rather than + erroring out. + - set mu4e-view-html-plaintext-ratio-heuristic to 5, as 10 was too + high to detect some effectively html-only messages + - add mu4e-view-toggle-html (keybinding: 'h') to toggle between + text and html display. The existing 'mu4e-view-toggle-hide-cited' gets the + new binding '#'. + - add a customization variable `mu4e-view-auto-mark-as-read' + (defaults to t); if set to nil, mu4e won't mark messages as read when you + open them. This can be useful on read-only file-systems, since + marking-as-read implies a file-move operation. + - use smaller chunks for mu server on Cygwin, allowing for better + mu4e support there. + +** 0.9.13 + +*** contributors + + Attila, Daniele Pizzolli, Charles-H.Schulz, David C Sterrat, Dirk-Jan C. + Binnema, Eike Kettner, Florian Lindner, Foivos S. Zakkak, Gour, KOMURA + Takaaki, Pan Jie, Phil Hagelberg, thdox, Tiago Saboga, Titus von der + Malsburg + + (and of course all people who reported issues, provided suggestions etc.) + +*** mu/mu4e/guile + + - NEWS (this file) is now visible from within mu4e – "N" in the main-menu. + + - make `mu4e-headers-sort-field', `mu4e-headers-sort-direction' + public (that, is change the prefix from mu4e~ to mu4e-), so users can + manipulate them + + - make it possible the 'fancy' (unicode) characters separately for + headers and marks (see the variable `mu4e-use-fancy-chars'.) + + - allow for composing in a separate frame (see + `mu4e-compose-in-new-frame') + + - add the `:thread-subject' header field, for showing the subject + for a thread only once. So, instead of (from the manual): + + #+BEGIN_EXAMPLE +06:32 Nu To Edmund Dantès GstDev + Re: Gstreamer-V4L... 15:08 Nu Abbé Busoni +GstDev + Re: Gstreamer-V... 18:20 Nu Pierre Morrel GstDev \ Re: Gstreamer... +2013-03-18 S Jacopo EmacsUsr + emacs server on win... 2013-03-18 S Mercédès +EmacsUsr \ RE: emacs server ... 2013-03-18 S Beachamp EmacsUsr + Re: Copying a +whole... 22:07 Nu Albert de Moncerf EmacsUsr \ Re: Copying a who... 2013-03-18 S +Gaspard Caderousse GstDev | Issue with GESSimpl... 2013-03-18 Ss Baron Danglars +GuileUsr | Guile-SDL 0.4.2 ava... End of search results #+END_EXAMPLE + + the headers list would now look something like: + #+BEGIN_EXAMPLE +10:26 ⭑☐ Nicolas Goaziou Orgmode /bulk ◼ Re: [O] 2 issue with Include function +11:00 ⭑☐ Leonard Randall Orgmode /bulk ┗▶ 10:55 ⭑☐ Guillermo Rodrigu... GstDev +/bulk ◼ Re: stop pipeline into a callback function. 12:04 ⭑☐ Enrique Ocaña Gon... +GstDev /bulk ┗▶ 11:27 ⭑☐ Tim Müller GstDev /bulk ◼ 09:34 ⭑☐ Robert Klein Orgmode +/bulk ◼ Re: [O] Agenda Tag filtering - has the behaviour changed? #+END_EXAMPLE + + This is a feature known from e.g. `mutt' and `gnus` and many other + clients, and can be enabled by customizing `mu4e-headers-fields' + (replacing `:subject' with `:thread-subject') + + It's not the default yet, but may become so in the future. + + - add some spam-handling actions to mu4e-contrib.el + + - mu4e now targets org 8.x, which support for previous versions + relegated to `org-old-mu4e.el`. Some of the new org-features are improved + capture templates. + + - updates to the documentation, in particular about using BBDB. + + - improved URL-handling (use emacs built-in functionality) + + - many bug fixes, including some crash fixes on BSD + +*** guile + + – add --delete option to the find-dups scripts, to automatically delete + them. Use with care! + +** Release 0.9.12 + +*** mu + + - truncate /all/ terms the go beyond xapian's max term length + - lowercase the domain-part of email addresses in mu cfind (and mu4e), if + the domain is in ascii + - give messages without msgids fake-message-ids; this fixes the problem + where such messages were not found in --include-related queries + - cleanup of the query parser + - provide fake message-ids for messages without it; fixes #183 + - allow showing tags in 'mu find' output + - fix CSV quoting + +*** mu4e + + - update the emacs <-> backend protocol; documented in the mu-server man page + - show 'None' as date for messages without it (Headers View) + - add `mu4e-headers-found-hook', `mu4e-update-pre-hook'. + - split org support in org-old-mu4e.el (org <= 7.x) and org-mu4e.el + - org: improve template keywords + - rework URL handling + +** Release 0.9.5 + +*** mu + + - allow 'contact:' as a shortcut in queries for 'from:foo OR to:foo OR + cc:foo OR bcc:foo', and 'recip:' as a shortcut for 'to:foo OR cc:foo OR + bcc:foo' + - support getting related messages (--include-related), which includes + messages that may not match the query, but that are in the same threads as + messages that were + - support "list:"/"v:" for matching mailing list names, and the "v" + format-field to show them. E.g 'mu find list:emacs-orgmode.gnu.org' + +*** mu4e + + - scroll down in message view takes you to next message (but see + `mu4e-view-scroll-to-next') + - support 'human dates', that is, show the time for today's messages, and + the date for older messages in the headers view + - replace `mu4e-user-mail-address-regexp' and `mu4e-my-mail-addresses' with + `mu4e-user-mail-address-list' + - support tags (i.e.., X-Keywords and friends) in the headers-view, and the + message view. Thanks to Abdó Roig-Maranges. New field ":tags". + - automatically update the headers buffer when new messages are found during + indexing; set `mu4e-headers-auto-update' to nil to disable this. + - update mail/index with M-x mu4e-update-mail-and-index; which everywhere in + mu4e is available with key C-S-u. Use prefix argument to run in + background. + - add function `mu4e-update-index' to only update the index + - add 'friendly-names' for mailing lists, so they should up nicely in the + headers view + +*** guile + + - add 'mu script' command to run mu script, for example to do statistics on + your message corpus. See the mu-script man-page. + +*** mug + + - ported to gtk+ 3; remove gtk+ 2.x code + + + +** Release 0.9.9 <2012-10-14> + +*** mu4e + - view: address can be toggled long/short, compose message + - sanitize opening urls (mouse-1, and not too eager) + - tooltips for header labels, flags + - add sort buttons to header-labels + - support signing / decryption of messages + - improve address-autocompletion (e.g., ensure it's case-insensitive) + - much faster when there are many maildirs + - improved line wrapping + - better handle attached messages + - improved URL-matching + - improved messages to user (mu4e-(warn|error|message)) + - add refiling functionality + - support fancy non-ascii in the UI + - dynamic folders (i.e.., allow mu4e-(sent|draft|trash|refile)-folder) to + be a function + - dynamic attachment download folder (can be a function now) + - much improved manual + +*** mu + - remove --summary (use --summary-len instead) + - add --after for mu find, to limit to messages after T + - add new command `mu verify', to verify signatures + - fix iso-2022-jp decoding (and other 7-bit clean non-ascii) + - add support for X-keywords + - performance improvements for threaded display (~ 25% for 23K msgs) + - mu improved user-help (and the 'mu help' command) + - toys/mug2 replaces toys/mug + +*** mu-guile + - automated tests + - add mu:timestamp, mu:count + - handle db reopenings in the background + + +** Release 0.9.8.5 <2012-07-01> + +*** mu4e + + - auto-completion of e-mail addresses + - inline display of images (see `mu4e-view-show-images'), uses imagemagick + if available + - interactively change number of headers / columns for showing headers with + C-+ and C-- in headers, view mode + - support flagging message + - navigate to previous/next queries like a web browser (with , + ) + - narrow search results with '/' + - next/previous take a prefix arg now, to move to the nth previous/next message + - allow for writing rich-text messages with org-mode + - enable marking messages as Flagged + - custom marker functions (see manual) + - better "dwim" handling of buffer switching / killing + - deferred marking of message (i.e.., mark now, decide what to mark for + later) + - enable changing of sort order, display of threads + - clearer marks for marked messages + - fix sorting by subject (disregarding Re:, Fwd: etc.) + - much faster handling when there are many maildirs (speedbar) + - handle mailto: links + - improved, extended documentation + +*** mu + + - support .noupdate files (parallel to .noindex, dir is ignored unless we're + doing a --rebuild). + - append all inline text parts, when getting the text body + - respect custom maildir flags + - correctly handle the case where g_utf8_strdown (str) > len (str) + - make gtk, guile, webkit dependency optional, even if they are installed + + +** Release 0.9.8.4 <2012-05-08> + +*** mu4e + + - much faster header buffers + - split view mode (headers, view); see `mu4e-split-view'. + - add search history for queries + - ability to open attachments with arbitrary programs, pipe through shell + commands or open in the current emacs + - quote names in recipient addresses + - mu4e-get-maildirs works now for recursive maildirs as well + - define arbitrary operations for headers/messages/attachments using the + actions system -- see the chapter 'Actions' in the manual + - allow mu4e to be uses as the default emacs mailer (`mu4e-user-agent') + - mark headers based on a regexp, `mu4e-mark-matches', or '%' + - mark threads, sub-threads (mu4e-hdrs-mark-thread, + mu4e-hdrs-mark-subthread, or 'T', 't') + - add msg2pdf toy + - easy logging (using `mu4e-toggle-logging') + - improve mu4e-speedbar for use in headers/view + - use the message-mode FCC system for saving messages to the sent-messages + folder + - fix: off-by-one in number of matches shown + +*** general + + - fix for opening files with non-ascii names + - much improved support for searching non-Latin (Cyrillic etc.) languages + we can now match 'Тесла' or 'Аркона' without problems + - smarter escaping (fixes issues with finding message ids) + - fixes for queries with brackets + - allow --summary-len for the length of message summaries + - numerous other small fixes + + +** Release 0.9.8.3 <2012-04-06> + + *NOTE*: existing mu/mu4e are recommended to run `mu index --rebuild' after + installation. + +*** mu4e + + - allow for searching by editing bookmarks + (`mu4e-search-bookmark-edit-first') (keybinding 'B') + - make it configurable what to do with sent messages (see + `mu4e-sent-messages-behavior') + - speedbar support (initial patch by Antono V) + - better handling of drafts: + - don't save too early + - more descriptive buffer names (based on Subject, if any) + - don't put "--text-follows-this-line--" markers in files + - automatically include signatures, if set + - add user-settable variables mu4e-view-wrap-lines and mu4e-view-hide-cited, + which determine the initial way a message is displayed + - improved documentation + +*** general + + - much improved searching for GMail folders (i.e. maildir:/ matching); + this requires a 'mu index --rebuild' + - correctly handle utf-8 messages, even if they don't specify this explicitly + - fix compiler warnings for newer/older gcc and clang/clang++ + - fix unit tests (and some code) for Ubuntu 10.04 and FreeBSD9 + - fix warnings for compilation with GTK+ 3.2 and recent glib (g_set_error) + - fix mu_msg_move_to_maildir for top-level messages + - fix in maildir scanning + - plug some memleaks + +** Release 0.9.8.2 <2012-03-11> + +*** mu4e: + + - make mail updating non-blocking + - allow for automatic periodic update ('mu4e-update-interval') + - allow for external triggering of update + - make behavior when leaving the headers buffer customizable, ie. + ask/apply/ignore ('mu4e-headers-leave-behaviour') + +*** general + + - fix output for some non-UTF8 locales + - open ('play') file names with spaces + - don't show unnecessary errors for --format=links + - make build warning-free for clang/clang++ + - allow for slightly older autotools + - fix unit tests for some hidden assumptions (locale, dir structure etc.) + - some documentation updates / clarifications + +** Release 0.9.8.1 <2012-02-18 Sat> + +*** mu + - show only leaf/rfc822 MIME-parts + +*** mu4e + + - allow for shell commands with arguments in `mu4e-get-mail-command'. + - support marking messages as 'read' and 'unread' + - show the current query in the the mode-line (`global-mode-string'). + - don't repeat 'Re:' / 'Fwd:' + - colorize cited message parts + - better handling of text-based, embedded message attachments + - for text-bodies, concatenate all text/plain parts + - make filladapt dep optional + - documentation improvements + +** Release 0.9.8 <2012-01-31> + + - '--descending' has been renamed into '--reverse' + - search for attachment MIME-type using 'mime:' or 'y:' + - search for text in text-attachments using 'embed:' or 'e:' + - searching for attachment file names now uses 'file:' (was: 'attach:') + - experimental emacs-based mail client -- "mu4e" + - added more unit tests + - improved guile binding - no special binary is needed anymore, it's + installable are works with the normal guile system; code has been + substantially improved. still 'experimental' + +** Release 0.9.7 <2011-09-03 Sat> + + - don't enforce UTF-8 output, use locale (fixes issue #11) + - add mail threading to mu-find (using -t/--threads) (sorta fixes issue #13) + - add header line to --format=mutt-ab (mu cfind), (fixes issue #42) + - terminate mu view results with a form-feed marker (use --terminate) (fixes + issue #41) + - search X-Label: tags (fixes issue #40) + - added toys/muile, the mu guile shells, which allows for message stats etc. + - fix date handling (timezones) + +** Release 0.9.6 <2011-05-28 Sat> + + - FreeBSD build fix + - fix matching for mu cfind to be as expected + - fix mu-contacts for broken names/emails + - clear the contacts-cache too when doing a --rebuild + - wildcard searches ('*') for fields (except for path/maildir) + - search for attachment file names (with 'a:'/'attach:') -- also works with + wildcards + - remove --xquery completely; use --output=xquery instead + - fix progress info in 'mu index' + - display the references for a message using the 'r' character (xmu find) + - remove --summary-len/-k, instead use --summary for mu view and mu find, and + - support colorized output for some sub-commands (view, cfind and + extract). Disabled by default, use --color to enable, or set env MU_COLORS + to non-empty + - update documentation, added more examples + +** Release 0.9.5 <2011-04-25 Mon> + + - bug fix for infinite loop in Maildir detection + - minor fixes in tests, small optimizations + +** Release 0.9.4 <2011-04-12 Tue> + + - add the 'cfind' command, to search/export contact information + - add 'flag:unread' as a synonym for 'flag:new OR NOT flag:unseen' + - updated documentation + +** Release 0.9.3 <2011-02-13 Sun> + + - don't warn about missing files with --quiet + +** Release 0.9.2 <2011-02-02 Wed> + + - stricter checking of options; and options must now *follow* the sub-command + (if any); so, something like: 'mu index --maildir=/foo/bar' + - output searches as plain text (default), XML, JSON or s-expressions using + --format=plain|xml|json|sexp. For example: 'mu find foobar --output=json'. + These format options are experimental (except for 'plain') + - the --xquery option should now be used as --format=xquery, for output + symlinks, use --format=links. This is a change in the options. + - search output can include the message size using the 'z' shortcut + - match message size ranges (i.e.. size:500k..2M) + - fix: honor the --overwrite (or lack thereof) parameter + - support folder names with special characters (@, ' ', '.' and so on) + - better check for already-running mu index + - when --maildir= is not provided for mu index, default to the last one + - add --max-msg-size, to specify a new maximum message size + - move the 'mug' UI to toys/mug; no longer installable + - better support for Solaris builds, Gentoo. + +** Release 0.9.1 <2010-12-05 Sun> + + - Add missing icon for mug + - Fix unit tests (Issue #30) + - Fix Fedora 14 build (broken GTK+ 3) (Issue #31) + +** Release 0.9 <2010-12-04 Sat> + + - you can now search for the message priority ('prio:high', 'prio:low', + 'prio:normal') + - you can now search for message flags, e.g. 'flag:attach' for messages with + attachment, or 'flag:encrypted' for encrypted messages + - you can search for time-intervals, e.g. 'date:2010-11-26..2010-11-29' for + messages in that range. See the mu-find(1) and mu-easy(1) man-pages for + details and examples. + - you can store bookmarked queries in ~/.mu/bookmarks + - the 'flags' parameter has been renamed in 'flag' + - add a simple graphical UI for searching, called 'mug' + - fix --clearlinks for file systems without entry->d_type (fixes issue #28) + - make matching case-insensitive and accent-insensitive (accent-insensitive + for characters in Unicode Blocks 'Latin-1 Supplement' and 'Latin + Extended-A') + - more extensive pre-processing is done to make searching for email-addresses + and message-ids less likely to not work (issue #21) + - updated the man-pages + - experimental support for Fedora 14, which uses GMime 2.5.x (fixes issue #29) + +** Release 0.8 <2010-10-30 Sat> + + - There's now 'mu extract' for getting information about MIME-parts + (attachments) and extracting them + - Queries are now internally converted to lowercase; this solves some of the + false-negative issues + - All mu sub-commands now have their own man-page + - 'mu find' now takes a --summary-len= argument to print a summary of + up-to-n lines of the message + - Same for 'mu view'; the summary replaces the full body + - Setting the mu home dir now goes with -m, --muhome + - --log-stderr, --reindex, --rebuild, --autoupgrade, --nocleanup, --mode, + --linksdir, --clearlinks lost their single char version + +** Release 0.7 <2010-02-27 Sat> + + - Database format changed + - Automatic database scheme version check, notifies users when an upgrade + is needed + - 'mu view', to view mail message files + - Support for >10K matches + - Support for unattended upgrades - that is, the database can automatically + by upgraded (--autoupgrade). Also, the log file is automatically cleaned + when it gets too big (unless you use --nocleanup) + - Search for a certain Maildir using the maildir:,m: search prefixes. For + example, you can find all messages located in ~/Maildir/foo/bar/cur/msg + ~/Maildir/foo/bar/new/msg and with m:/foo/bar this replace the search for + path/p in 0.6 + - Fixes for reported issues () + - A test suite with a growing number of unit tests + + +** Release 0.6 <2010-01-23 Sat> + + - First new release of mu since 2008 + - No longer depends on sqlite + + +# Local Variables: +# mode: org; org-startup-folded: nil +# fill-column:80 +# End: diff --git a/README b/README new file mode 100644 index 0000000..921565b --- /dev/null +++ b/README @@ -0,0 +1,25 @@ +Welcome to mu & mu4e! + +Given the enormous amounts of e-mail many people gather and the importance of e-mail message in our +work-flows, it's essential to quickly deal with all that mail - in particular, to instantly find +that one important e-mail you need right now, and quickly file away message for later use. + +`mu` is a tool for dealing with e-mail messages stored in the Maildir-format. `mu`'s purpose in life +is to help you to quickly find the messages you need; in addition, it allows you to view messages, +extract attachments, create new maildirs, and so on. See the [mu cheatsheet] for some examples. =mu= +is fully documented. + +After indexing your messages into a [Xapian](http://www.xapian.org)-database, you can search them +using a custom query language. You can use various message fields or words in the body text to find +the right messages. + +Built on top of `mu` are some extensions (included in this package): + + * mu4e: a full-featured e-mail client that runs inside emacs + + * mu-guile: bindings for the Guile/Scheme programming language (version 2.2 and later) + +`mu` is written in C and C++; `mu4e` is written in elisp, and `mu-guile` in a mix of C and Scheme. + +Note, `mu` is available in Linux distributions (e.g. Debian/Ubuntu and Fedora) under the name +`maildir-utils`; apparently because they don't like short names. diff --git a/TODO b/TODO new file mode 100644 index 0000000..f152855 --- /dev/null +++ b/TODO @@ -0,0 +1,159 @@ +#+STARTUP: showall + +* TODO (fixes, ideas, etc.) + +** Future stuff + +*** mu + + - put threading information in the database, and enable getting the complete + threads when searching + - refactor fill_database function in test cases + - don't show duplicate e-mails (i.e.. for Gmail); check the message-id + +*** mu-guile + + - move contact export to separate scm + - fix logging + +*** mu4e + + - special-case replying to messages sent by self + - identities (see Jacek's 'mu4e: From field in replies' mail) + ==> [ workaround available, using mu4e-pre-compose-hook, dynamic folders ] + - new-mail warning + ==> [ workaround available, using mu4e-index-updated-hook ] + - custom header fields in headers-view, message-view + - show maildirs as a tree, not a list in speed bar + - review emacs menus + - re-factor / separate window/buffer management + - enable keeping message view buffers around + - better naming for draft/view buffers + - header updating interferes with marks (when updating for 'mark as read', + when reading a marked message) + - set/unset flag editing command + - handling of database upgrades + - restore point after rerunning a search + - make the mu4e-bookmarks format similar to the other ones + - refresh current query after update? + - fix mu4e-mark-set to work from the view buffer as well + - open links to mails through headers-mode somehow (i.e.., + mu4e-view-message-with-msgid) + - improve mouse interaction (i.e., cursor vs point) + - show counts of messages in searches (in main view) + - show flush only if there's something to flush (and # of flushables) + - fix unsafe temp-file handling + - make copy paste name/address in mu4e-view possible + + +* Done (0.9.9.x) + + - mu4e: scroll down –> go to next message + - mu: add contact: as a shortcut for matching from/to/cc/bcc: + - guile integration + - statistics + - 'human' dates in the headers view + - :tags in headers, message view + +* Done + :PROPERTIES: + :VISIBILITY: folded + :END: + + +** Done (0.9.9) + + - make contacts in the view clickable (toggle long/short display, compose message) + - opening urls is too eager (now use M-RET for opening url at point, not just + RET, which conflicted with using RET for scrolling) + - document quoting of queries + - use mu-error + - tooltips in header labels + - tooltip for flags field + - remove --summary option (for mu find, mu view); use --summary-len instead + - add sort buttons to header labels (and do the sorting) + - cleanup mu-cmd-find + - implement --after for mu find, to only show message files changed after a + certain time (mtime) + - add mu:timestamp for guile (referring to the message file's mtime) + - guile automated tests + - add 'mu verify' + - automated tests + - handle verbose/quiet/normal output 'mu verify' + - check gmime 2.4 does not break + - hook up mu4e with 'mu verify' + - add 'help' command + - refactor mu-msg-part + - move widgets/ into toys/mug2, remove toys/mug/, rename toys/mug2 -> toys/mug + - add guile mu:count + - don't show GPG/PKCS7 sigs as attachments + - fix address completion (quote names) + - add support for X-Keywords (in addition to X-Label) + - guile: add stats test cases + - fixed iso-2022-jp (japanese) decoding + - make address completion case-insensitive + - recognize '*' in urls + - handle exception 'The revision being read has been discarded - you should + call Xapian::Database::reopen() and retry the operation' + - handle passwords from get-mail shell command + - support fancy (non-ascii) chars for header flags, thread prefix strings + - improve performance of getting the list of maildirs + - fix setting wrapped/hide state in viewer + - fix ' realpath() failed for...' stuff + - allow for fancy chars (> ascii), make it configurable (mu4e-use-fancy-chars) + - don't user `error' for user-errors + - better echo-area reporting + - improve help feedback for user (command line) + - handling of encrypted messages + - improved checked for gmime-2.6 crypto funcs + - handling of command line options / help + - fix / add support for :size + - mu4e~view-wrap-lines (use visual-line-mode? see Jacek's mu4e~view-wrap-lines + mail) + - better help + - threading optimizations + - actions for /all/ headers, actions for /all/ attachment + - handle attached messages with attachments + +** Done (0.8.9.5) + + - make next/prev header respect prefix argument (Jacek's patch) + - make search results a stack (well, multiple stacks) + - optionally keep cc with user's email + - enable setting/unsetting 'Flagged' on messages + - allow narrowing of search results + - interactive split-view control (Jacek) + - view images inline + - *FIX* slow maildirs when there are many + - *FIX* ignore unrecognized maildir flag letters + - *FIX*: reply-to does not make it to the frontend + - *FIX* wrong buffer deleted after sending (see '(non mu) buffer is killed') + - rich text composing (with org-mode) + - let message-mode deal with burying/killing compose buffers + - *FIX* add runtime check for imagemagick + - *FIX* no error note if target message already exists (when moving) + - sorting + show / hide threads + - *FIX* having multiple header views visible + - *FIX* fix for strings where len (g_utf8_strdown (str)) > len (str) + - make sure marks correspond to the *current* message in message view (see + https://github.com/djcb/mu/issues/26) + - *FIX* don't remove unknown message flags when moving + - make guile/gtk/webkit dependency optional + - improve fringe marks (see https://github.com/djcb/mu/issues/21) + - mark message, decide what to do with them later (i.e.. 'deferred marking') + - custom predicate functions for marking + - make mu4e buffer killing less aggressive (i.e.., DWIM) + - about mu4e + - hide some headers when composing + - fix sorting subjects with ':' (but not 'Re:' or 'Fwd:') + - strip signature from original when replying + - make refresh after changing sort, threads the default + - contact completion (see Jacek's 'mu4e: using' mail) + - *FIX* emacs23 mailto: handling + - *FIX* message interference + - *FIX* emacs23.2+ auto-completion + + +# Local Variables: +# mode: org +# End: diff --git a/autogen.sh b/autogen.sh new file mode 100755 index 0000000..1155320 --- /dev/null +++ b/autogen.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Run this to generate all the initial makefiles, etc. + +test -f mu/mu.cc || { + echo "*** Run this script from the top-level mu source directory" + exit 1 +} + +# opportunistically; usually not needed, but occasionally it'll +# avoid build errors that would otherwise confuse users. +test -f Makefile && { + echo "*** clear out old things" + make distclean 2> /dev/null +} + + +command -V autoreconf > /dev/null +if [ $? != 0 ]; then + echo "*** No autoreconf found, please install it ***" + exit 1 +fi + +rm -f config.cache +rm -rf autom4te.cache + +autoreconf --force --install --verbose || exit $? + +if test -z "$*"; then + echo "# Configuring without parameters" +else + echo "# Configure with parameters $*" +fi + +./configure --config-cache $@ diff --git a/build-aux/config.rpath b/build-aux/config.rpath new file mode 100644 index 0000000..e69de29 diff --git a/c.cfg b/c.cfg new file mode 100644 index 0000000..0a1d2d0 --- /dev/null +++ b/c.cfg @@ -0,0 +1,1578 @@ +# Uncrustify 0.60 + +# +# General options +# + +# The type of line endings +newlines = lf # auto/lf/crlf/cr + +# The original size of tabs in the input +input_tab_size = 8 # number + +# The size of tabs in the output (only used if align_with_tabs=true) +output_tab_size = 8 # number + +# The ASCII value of the string escape char, usually 92 (\) or 94 (^). (Pawn) +string_escape_char = 92 # number + +# Alternate string escape char for Pawn. Only works right before the quote char. +string_escape_char2 = 0 # number + +# Allow interpreting '>=' and '>>=' as part of a template in 'void f(list>=val);'. +# If true (default), 'assert(x<0 && y>=3)' will be broken. +# Improvements to template detection may make this option obsolete. +tok_split_gte = false # false/true + +# Control what to do with the UTF-8 BOM (recommend 'remove') +utf8_bom = ignore # ignore/add/remove/force + +# If the file contains bytes with values between 128 and 255, but is not UTF-8, then output as UTF-8 +utf8_byte = false # false/true + +# Force the output encoding to UTF-8 +utf8_force = false # false/true + +# +# Indenting +# + +# The number of columns to indent per level. +# Usually 2, 3, 4, or 8. +indent_columns = 8 # number + +# The continuation indent. If non-zero, this overrides the indent of '(' and '=' continuation indents. +# For FreeBSD, this is set to 4. Negative value is absolute and not increased for each ( level +indent_continue = 0 # number + +# How to use tabs when indenting code +# 0=spaces only +# 1=indent with tabs to brace level, align with spaces +# 2=indent and align with tabs, using spaces when not on a tabstop +indent_with_tabs = 1 # number + +# Comments that are not a brace level are indented with tabs on a tabstop. +# Requires indent_with_tabs=2. If false, will use spaces. +indent_cmt_with_tabs = false # false/true + +# Whether to indent strings broken by '\' so that they line up +indent_align_string = false # false/true + +# The number of spaces to indent multi-line XML strings. +# Requires indent_align_string=True +indent_xml_string = 0 # number + +# Spaces to indent '{' from level +indent_brace = 0 # number + +# Whether braces are indented to the body level +indent_braces = false # false/true + +# Disabled indenting function braces if indent_braces is true +indent_braces_no_func = false # false/true + +# Disabled indenting class braces if indent_braces is true +indent_braces_no_class = false # false/true + +# Disabled indenting struct braces if indent_braces is true +indent_braces_no_struct = false # false/true + +# Indent based on the size of the brace parent, i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc. +indent_brace_parent = false # false/true + +# Whether the 'namespace' body is indented +indent_namespace = false # false/true + +# The number of spaces to indent a namespace block +indent_namespace_level = 0 # number + +# If the body of the namespace is longer than this number, it won't be indented. +# Requires indent_namespace=true. Default=0 (no limit) +indent_namespace_limit = 0 # number + +# Whether the 'extern "C"' body is indented +indent_extern = false # false/true + +# Whether the 'class' body is indented +indent_class = false # false/true + +# Whether to indent the stuff after a leading class colon +indent_class_colon = false # false/true + +# Virtual indent from the ':' for member initializers. Default is 2 +indent_ctor_init_leading = 2 # number + +# Additional indenting for constructor initializer list +indent_ctor_init = 0 # number + +# False=treat 'else\nif' as 'else if' for indenting purposes +# True=indent the 'if' one level +indent_else_if = false # false/true + +# Amount to indent variable declarations after a open brace. neg=relative, pos=absolute +indent_var_def_blk = 0 # number + +# Indent continued variable declarations instead of aligning. +indent_var_def_cont = false # false/true + +# True: force indentation of function definition to start in column 1 +# False: use the default behavior +indent_func_def_force_col1 = false # false/true + +# True: indent continued function call parameters one indent level +# False: align parameters under the open paren +indent_func_call_param = false # false/true + +# Same as indent_func_call_param, but for function defs +indent_func_def_param = false # false/true + +# Same as indent_func_call_param, but for function protos +indent_func_proto_param = false # false/true + +# Same as indent_func_call_param, but for class declarations +indent_func_class_param = false # false/true + +# Same as indent_func_call_param, but for class variable constructors +indent_func_ctor_var_param = false # false/true + +# Same as indent_func_call_param, but for templates +indent_template_param = false # false/true + +# Double the indent for indent_func_xxx_param options +indent_func_param_double = false # false/true + +# Indentation column for standalone 'const' function decl/proto qualifier +indent_func_const = 0 # number + +# Indentation column for standalone 'throw' function decl/proto qualifier +indent_func_throw = 0 # number + +# The number of spaces to indent a continued '->' or '.' +# Usually set to 0, 1, or indent_columns. +indent_member = 0 # number + +# Spaces to indent single line ('//') comments on lines before code +indent_sing_line_comments = 0 # number + +# If set, will indent trailing single line ('//') comments relative +# to the code instead of trying to keep the same absolute column +indent_relative_single_line_comments = false # false/true + +# Spaces to indent 'case' from 'switch' +# Usually 0 or indent_columns. +indent_switch_case = 0 # number + +# Spaces to shift the 'case' line, without affecting any other lines +# Usually 0. +indent_case_shift = 0 # number + +# Spaces to indent '{' from 'case'. +# By default, the brace will appear under the 'c' in case. +# Usually set to 0 or indent_columns. +indent_case_brace = 0 # number + +# Whether to indent comments found in first column +indent_col1_comment = false # false/true + +# How to indent goto labels +# >0 : absolute column where 1 is the leftmost column +# <=0 : subtract from brace indent +indent_label = 1 # number + +# Same as indent_label, but for access specifiers that are followed by a colon +indent_access_spec = 1 # number + +# Indent the code after an access specifier by one level. +# If set, this option forces 'indent_access_spec=0' +indent_access_spec_body = false # false/true + +# If an open paren is followed by a newline, indent the next line so that it lines up after the open paren (not recommended) +indent_paren_nl = false # false/true + +# Controls the indent of a close paren after a newline. +# 0: Indent to body level +# 1: Align under the open paren +# 2: Indent to the brace level +indent_paren_close = 1 # number + +# Controls the indent of a comma when inside a paren.If TRUE, aligns under the open paren +indent_comma_paren = false # false/true + +# Controls the indent of a BOOL operator when inside a paren.If TRUE, aligns under the open paren +indent_bool_paren = false # false/true + +# If 'indent_bool_paren' is true, controls the indent of the first expression. If TRUE, aligns the first expression to the following ones +indent_first_bool_expr = true # false/true + +# If an open square is followed by a newline, indent the next line so that it lines up after the open square (not recommended) +indent_square_nl = false # false/true + +# Don't change the relative indent of ESQL/C 'EXEC SQL' bodies +indent_preserve_sql = false # false/true + +# Align continued statements at the '='. Default=True +# If FALSE or the '=' is followed by a newline, the next line is indent one tab. +indent_align_assign = true # false/true + +# Indent OC blocks at brace level instead of usual rules. +indent_oc_block = false # false/true + +# Indent OC blocks in a message relative to the parameter name. +# 0=use indent_oc_block rules, 1+=spaces to indent +indent_oc_block_msg = 0 # number + +# Minimum indent for subsequent parameters +indent_oc_msg_colon = 0 # number + +# +# Spacing options +# + +# Add or remove space around arithmetic operator '+', '-', '/', '*', etc +sp_arith = force # ignore/add/remove/force + +# Add or remove space around assignment operator '=', '+=', etc +sp_assign = force # ignore/add/remove/force + +# Add or remove space around '=' in C++11 lambda capture specifications. Overrides sp_assign +sp_cpp_lambda_assign = ignore # ignore/add/remove/force + +# Add or remove space after the capture specification in C++11 lambda. +sp_cpp_lambda_paren = ignore # ignore/add/remove/force + +# Add or remove space around assignment operator '=' in a prototype +sp_assign_default = ignore # ignore/add/remove/force + +# Add or remove space before assignment operator '=', '+=', etc. Overrides sp_assign. +sp_before_assign = ignore # ignore/add/remove/force + +# Add or remove space after assignment operator '=', '+=', etc. Overrides sp_assign. +sp_after_assign = ignore # ignore/add/remove/force + +# Add or remove space around assignment '=' in enum +sp_enum_assign = force # ignore/add/remove/force + +# Add or remove space before assignment '=' in enum. Overrides sp_enum_assign. +sp_enum_before_assign = ignore # ignore/add/remove/force + +# Add or remove space after assignment '=' in enum. Overrides sp_enum_assign. +sp_enum_after_assign = ignore # ignore/add/remove/force + +# Add or remove space around preprocessor '##' concatenation operator. Default=Add +sp_pp_concat = add # ignore/add/remove/force + +# Add or remove space after preprocessor '#' stringify operator. Also affects the '#@' charizing operator. +sp_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space before preprocessor '#' stringify operator as in '#define x(y) L#y'. +sp_before_pp_stringify = ignore # ignore/add/remove/force + +# Add or remove space around boolean operators '&&' and '||' +sp_bool = force # ignore/add/remove/force + +# Add or remove space around compare operator '<', '>', '==', etc +sp_compare = force # ignore/add/remove/force + +# Add or remove space inside '(' and ')' +sp_inside_paren = ignore # ignore/add/remove/force + +# Add or remove space between nested parens +sp_paren_paren = ignore # ignore/add/remove/force + +# Whether to balance spaces inside nested parens +sp_balance_nested_parens = false # false/true + +# Add or remove space between ')' and '{' +sp_paren_brace = force # ignore/add/remove/force + +# Add or remove space before pointer star '*' +sp_before_ptr_star = force # ignore/add/remove/force + +# Add or remove space before pointer star '*' that isn't followed by a variable name +# If set to 'ignore', sp_before_ptr_star is used instead. +sp_before_unnamed_ptr_star = remove # ignore/add/remove/force + +# Add or remove space between pointer stars '*' +sp_between_ptr_star = remove # ignore/add/remove/force + +# Add or remove space after pointer star '*', if followed by a word. +sp_after_ptr_star = remove # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by a func proto/def. +sp_after_ptr_star_func = ignore # ignore/add/remove/force + +# Add or remove space after a pointer star '*', if followed by an open paren (function types). +sp_ptr_star_paren = ignore # ignore/add/remove/force + +# Add or remove space before a pointer star '*', if followed by a func proto/def. +sp_before_ptr_star_func = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&' +sp_before_byref = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&' that isn't followed by a variable name +# If set to 'ignore', sp_before_byref is used instead. +sp_before_unnamed_byref = ignore # ignore/add/remove/force + +# Add or remove space after reference sign '&', if followed by a word. +sp_after_byref = ignore # ignore/add/remove/force + +# Add or remove space after a reference sign '&', if followed by a func proto/def. +sp_after_byref_func = ignore # ignore/add/remove/force + +# Add or remove space before a reference sign '&', if followed by a func proto/def. +sp_before_byref_func = ignore # ignore/add/remove/force + +# Add or remove space between type and word. Default=Force +sp_after_type = force # ignore/add/remove/force + +# Add or remove space before the paren in the D constructs 'template Foo(' and 'class Foo('. +sp_before_template_paren = ignore # ignore/add/remove/force + +# Add or remove space in 'template <' vs 'template<'. +# If set to ignore, sp_before_angle is used. +sp_template_angle = ignore # ignore/add/remove/force + +# Add or remove space before '<>' +sp_before_angle = ignore # ignore/add/remove/force + +# Add or remove space inside '<' and '>' +sp_inside_angle = ignore # ignore/add/remove/force + +# Add or remove space after '<>' +sp_after_angle = ignore # ignore/add/remove/force + +# Add or remove space between '<>' and '(' as found in 'new List();' +sp_angle_paren = ignore # ignore/add/remove/force + +# Add or remove space between '<>' and a word as in 'List m;' +sp_angle_word = ignore # ignore/add/remove/force + +# Add or remove space between '>' and '>' in '>>' (template stuff C++/C# only). Default=Add +sp_angle_shift = add # ignore/add/remove/force + +# Permit removal of the space between '>>' in 'foo >' (C++11 only). Default=False +# sp_angle_shift cannot remove the space without this option. +sp_permit_cpp11_shift = false # false/true + +# Add or remove space before '(' of 'if', 'for', 'switch', and 'while' +sp_before_sparen = force # ignore/add/remove/force + +# Add or remove space inside if-condition '(' and ')' +sp_inside_sparen = remove # ignore/add/remove/force + +# Add or remove space before if-condition ')'. Overrides sp_inside_sparen. +sp_inside_sparen_close = ignore # ignore/add/remove/force + +# Add or remove space before if-condition '('. Overrides sp_inside_sparen. +sp_inside_sparen_open = ignore # ignore/add/remove/force + +# Add or remove space after ')' of 'if', 'for', 'switch', and 'while' +sp_after_sparen = ignore # ignore/add/remove/force + +# Add or remove space between ')' and '{' of 'if', 'for', 'switch', and 'while' +sp_sparen_brace = force # ignore/add/remove/force + +# Add or remove space between 'invariant' and '(' in the D language. +sp_invariant_paren = ignore # ignore/add/remove/force + +# Add or remove space after the ')' in 'invariant (C) c' in the D language. +sp_after_invariant_paren = ignore # ignore/add/remove/force + +# Add or remove space before empty statement ';' on 'if', 'for' and 'while' +sp_special_semi = ignore # ignore/add/remove/force + +# Add or remove space before ';'. Default=Remove +sp_before_semi = remove # ignore/add/remove/force + +# Add or remove space before ';' in non-empty 'for' statements +sp_before_semi_for = remove # ignore/add/remove/force + +# Add or remove space before a semicolon of an empty part of a for statement. +sp_before_semi_for_empty = force # ignore/add/remove/force + +# Add or remove space after ';', except when followed by a comment. Default=Add +sp_after_semi = add # ignore/add/remove/force + +# Add or remove space after ';' in non-empty 'for' statements. Default=Force +sp_after_semi_for = force # ignore/add/remove/force + +# Add or remove space after the final semicolon of an empty part of a for statement: for ( ; ; ). +sp_after_semi_for_empty = force # ignore/add/remove/force + +# Add or remove space before '[' (except '[]') +sp_before_square = ignore # ignore/add/remove/force + +# Add or remove space before '[]' +sp_before_squares = ignore # ignore/add/remove/force + +# Add or remove space inside a non-empty '[' and ']' +sp_inside_square = ignore # ignore/add/remove/force + +# Add or remove space after ',' +sp_after_comma = force # ignore/add/remove/force + +# Add or remove space before ',' +sp_before_comma = remove # ignore/add/remove/force + +# Add or remove space between an open paren and comma: '(,' vs '( ,' +sp_paren_comma = force # ignore/add/remove/force + +# Add or remove space before the variadic '...' when preceded by a non-punctuator +sp_before_ellipsis = ignore # ignore/add/remove/force + +# Add or remove space after class ':' +sp_after_class_colon = ignore # ignore/add/remove/force + +# Add or remove space before class ':' +sp_before_class_colon = ignore # ignore/add/remove/force + +# Add or remove space before case ':'. Default=Remove +sp_before_case_colon = remove # ignore/add/remove/force + +# Add or remove space between 'operator' and operator sign +sp_after_operator = ignore # ignore/add/remove/force + +# Add or remove space between the operator symbol and the open paren, as in 'operator ++(' +sp_after_operator_sym = ignore # ignore/add/remove/force + +# Add or remove space after C/D cast, i.e. 'cast(int)a' vs 'cast(int) a' or '(int)a' vs '(int) a' +sp_after_cast = ignore # ignore/add/remove/force + +# Add or remove spaces inside cast parens +sp_inside_paren_cast = ignore # ignore/add/remove/force + +# Add or remove space between the type and open paren in a C++ cast, i.e. 'int(exp)' vs 'int (exp)' +sp_cpp_cast_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'sizeof' and '(' +sp_sizeof_paren = ignore # ignore/add/remove/force + +# Add or remove space after the tag keyword (Pawn) +sp_after_tag = ignore # ignore/add/remove/force + +# Add or remove space inside enum '{' and '}' +sp_inside_braces_enum = force # ignore/add/remove/force + +# Add or remove space inside struct/union '{' and '}' +sp_inside_braces_struct = ignore # ignore/add/remove/force + +# Add or remove space inside '{' and '}' +sp_inside_braces = force # ignore/add/remove/force + +# Add or remove space inside '{}' +sp_inside_braces_empty = remove # ignore/add/remove/force + +# Add or remove space between return type and function name +# A minimum of 1 is forced except for pointer return types. +sp_type_func = ignore # ignore/add/remove/force + +# Add or remove space between function name and '(' on function declaration +sp_func_proto_paren = force # ignore/add/remove/force + +# Add or remove space between function name and '(' on function definition +sp_func_def_paren = force # ignore/add/remove/force + +# Add or remove space inside empty function '()' +sp_inside_fparens = ignore # ignore/add/remove/force + +# Add or remove space inside function '(' and ')' +sp_inside_fparen = ignore # ignore/add/remove/force + +# Add or remove space inside the first parens in the function type: 'void (*x)(...)' +sp_inside_tparen = ignore # ignore/add/remove/force + +# Add or remove between the parens in the function type: 'void (*x)(...)' +sp_after_tparen_close = ignore # ignore/add/remove/force + +# Add or remove space between ']' and '(' when part of a function call. +sp_square_fparen = ignore # ignore/add/remove/force + +# Add or remove space between ')' and '{' of function +sp_fparen_brace = ignore # ignore/add/remove/force + +# Add or remove space between function name and '(' on function calls +sp_func_call_paren = force # ignore/add/remove/force + +# Add or remove space between function name and '()' on function calls without parameters. +# If set to 'ignore' (the default), sp_func_call_paren is used. +sp_func_call_paren_empty = force # ignore/add/remove/force + +# Add or remove space between the user function name and '(' on function calls +# You need to set a keyword to be a user function, like this: 'set func_call_user _' in the config file. +sp_func_call_user_paren = ignore # ignore/add/remove/force + +# Add or remove space between a constructor/destructor and the open paren +sp_func_class_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'return' and '(' +sp_return_paren = ignore # ignore/add/remove/force + +# Add or remove space between '__attribute__' and '(' +sp_attribute_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'defined' and '(' in '#if defined (FOO)' +sp_defined_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'throw' and '(' in 'throw (something)' +sp_throw_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'throw' and anything other than '(' as in '@throw [...];' +sp_after_throw = ignore # ignore/add/remove/force + +# Add or remove space between 'catch' and '(' in 'catch (something) { }' +# If set to ignore, sp_before_sparen is used. +sp_catch_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'version' and '(' in 'version (something) { }' (D language) +# If set to ignore, sp_before_sparen is used. +sp_version_paren = ignore # ignore/add/remove/force + +# Add or remove space between 'scope' and '(' in 'scope (something) { }' (D language) +# If set to ignore, sp_before_sparen is used. +sp_scope_paren = ignore # ignore/add/remove/force + +# Add or remove space between macro and value +sp_macro = force # ignore/add/remove/force + +# Add or remove space between macro function ')' and value +sp_macro_func = force # ignore/add/remove/force + +# Add or remove space between 'else' and '{' if on the same line +sp_else_brace = force # ignore/add/remove/force + +# Add or remove space between '}' and 'else' if on the same line +sp_brace_else = force # ignore/add/remove/force + +# Add or remove space between '}' and the name of a typedef on the same line +sp_brace_typedef = ignore # ignore/add/remove/force + +# Add or remove space between 'catch' and '{' if on the same line +sp_catch_brace = ignore # ignore/add/remove/force + +# Add or remove space between '}' and 'catch' if on the same line +sp_brace_catch = ignore # ignore/add/remove/force + +# Add or remove space between 'finally' and '{' if on the same line +sp_finally_brace = ignore # ignore/add/remove/force + +# Add or remove space between '}' and 'finally' if on the same line +sp_brace_finally = ignore # ignore/add/remove/force + +# Add or remove space between 'try' and '{' if on the same line +sp_try_brace = ignore # ignore/add/remove/force + +# Add or remove space between get/set and '{' if on the same line +sp_getset_brace = ignore # ignore/add/remove/force + +# Add or remove space before the '::' operator +sp_before_dc = ignore # ignore/add/remove/force + +# Add or remove space after the '::' operator +sp_after_dc = ignore # ignore/add/remove/force + +# Add or remove around the D named array initializer ':' operator +sp_d_array_colon = ignore # ignore/add/remove/force + +# Add or remove space after the '!' (not) operator. Default=Remove +sp_not = remove # ignore/add/remove/force + +# Add or remove space after the '~' (invert) operator. Default=Remove +sp_inv = remove # ignore/add/remove/force + +# Add or remove space after the '&' (address-of) operator. Default=Remove +# This does not affect the spacing after a '&' that is part of a type. +sp_addr = remove # ignore/add/remove/force + +# Add or remove space around the '.' or '->' operators. Default=Remove +sp_member = remove # ignore/add/remove/force + +# Add or remove space after the '*' (dereference) operator. Default=Remove +# This does not affect the spacing after a '*' that is part of a type. +sp_deref = remove # ignore/add/remove/force + +# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'. Default=Remove +sp_sign = remove # ignore/add/remove/force + +# Add or remove space before or after '++' and '--', as in '(--x)' or 'y++;'. Default=Remove +sp_incdec = remove # ignore/add/remove/force + +# Add or remove space before a backslash-newline at the end of a line. Default=Add +sp_before_nl_cont = force # ignore/add/remove/force + +# Add or remove space after the scope '+' or '-', as in '-(void) foo;' or '+(int) bar;' +sp_after_oc_scope = ignore # ignore/add/remove/force + +# Add or remove space after the colon in message specs +# '-(int) f:(int) x;' vs '-(int) f: (int) x;' +sp_after_oc_colon = ignore # ignore/add/remove/force + +# Add or remove space before the colon in message specs +# '-(int) f: (int) x;' vs '-(int) f : (int) x;' +sp_before_oc_colon = ignore # ignore/add/remove/force + +# Add or remove space after the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};' +sp_after_oc_dict_colon = ignore # ignore/add/remove/force + +# Add or remove space before the colon in immutable dictionary expression +# 'NSDictionary *test = @{@"foo" :@"bar"};' +sp_before_oc_dict_colon = ignore # ignore/add/remove/force + +# Add or remove space after the colon in message specs +# '[object setValue:1];' vs '[object setValue: 1];' +sp_after_send_oc_colon = ignore # ignore/add/remove/force + +# Add or remove space before the colon in message specs +# '[object setValue:1];' vs '[object setValue :1];' +sp_before_send_oc_colon = ignore # ignore/add/remove/force + +# Add or remove space after the (type) in message specs +# '-(int)f: (int) x;' vs '-(int)f: (int)x;' +sp_after_oc_type = ignore # ignore/add/remove/force + +# Add or remove space after the first (type) in message specs +# '-(int) f:(int)x;' vs '-(int)f:(int)x;' +sp_after_oc_return_type = ignore # ignore/add/remove/force + +# Add or remove space between '@selector' and '(' +# '@selector(msgName)' vs '@selector (msgName)' +# Also applies to @protocol() constructs +sp_after_oc_at_sel = ignore # ignore/add/remove/force + +# Add or remove space between '@selector(x)' and the following word +# '@selector(foo) a:' vs '@selector(foo)a:' +sp_after_oc_at_sel_parens = ignore # ignore/add/remove/force + +# Add or remove space inside '@selector' parens +# '@selector(foo)' vs '@selector( foo )' +# Also applies to @protocol() constructs +sp_inside_oc_at_sel_parens = ignore # ignore/add/remove/force + +# Add or remove space before a block pointer caret +# '^int (int arg){...}' vs. ' ^int (int arg){...}' +sp_before_oc_block_caret = ignore # ignore/add/remove/force + +# Add or remove space after a block pointer caret +# '^int (int arg){...}' vs. '^ int (int arg){...}' +sp_after_oc_block_caret = ignore # ignore/add/remove/force + +# Add or remove space between the receiver and selector in a message. +# '[receiver selector ...]' +sp_after_oc_msg_receiver = ignore # ignore/add/remove/force + +# Add or remove space after @property. +sp_after_oc_property = ignore # ignore/add/remove/force + +# Add or remove space around the ':' in 'b ? t : f' +sp_cond_colon = force # ignore/add/remove/force + +# Add or remove space around the '?' in 'b ? t : f' +sp_cond_question = force # ignore/add/remove/force + +# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make sense here. +sp_case_label = force # ignore/add/remove/force + +# Control the space around the D '..' operator. +sp_range = ignore # ignore/add/remove/force + +# Control the spacing after ':' in 'for (TYPE VAR : EXPR)' (Java) +sp_after_for_colon = ignore # ignore/add/remove/force + +# Control the spacing before ':' in 'for (TYPE VAR : EXPR)' (Java) +sp_before_for_colon = ignore # ignore/add/remove/force + +# Control the spacing in 'extern (C)' (D) +sp_extern_paren = ignore # ignore/add/remove/force + +# Control the space after the opening of a C++ comment '// A' vs '//A' +sp_cmt_cpp_start = force # ignore/add/remove/force + +# Controls the spaces between #else or #endif and a trailing comment +sp_endif_cmt = ignore # ignore/add/remove/force + +# Controls the spaces after 'new', 'delete', and 'delete[]' +sp_after_new = ignore # ignore/add/remove/force + +# Controls the spaces before a trailing or embedded comment +sp_before_tr_emb_cmt = ignore # ignore/add/remove/force + +# Number of spaces before a trailing or embedded comment +sp_num_before_tr_emb_cmt = 0 # number + +# Control space between a Java annotation and the open paren. +sp_annotation_paren = ignore # ignore/add/remove/force + +# +# Code alignment (not left column spaces/tabs) +# + +# Whether to keep non-indenting tabs +align_keep_tabs = false # false/true + +# Whether to use tabs for aligning +align_with_tabs = false # false/true + +# Whether to bump out to the next tab when aligning +align_on_tabstop = false # false/true + +# Whether to left-align numbers +align_number_left = false # false/true + +# Align variable definitions in prototypes and functions +align_func_params = true # false/true + +# Align parameters in single-line functions that have the same name. +# The function names must already be aligned with each other. +align_same_func_call_params = false # false/true + +# The span for aligning variable definitions (0=don't align) +align_var_def_span = 1 # number + +# How to align the star in variable definitions. +# 0=Part of the type 'void * foo;' +# 1=Part of the variable 'void *foo;' +# 2=Dangling 'void *foo;' +align_var_def_star_style = 1 # number + +# How to align the '&' in variable definitions. +# 0=Part of the type +# 1=Part of the variable +# 2=Dangling +align_var_def_amp_style = 0 # number + +# The threshold for aligning variable definitions (0=no limit) +align_var_def_thresh = 0 # number + +# The gap for aligning variable definitions +align_var_def_gap = 0 # number + +# Whether to align the colon in struct bit fields +align_var_def_colon = false # false/true + +# Whether to align any attribute after the variable name +align_var_def_attribute = false # false/true + +# Whether to align inline struct/enum/union variable definitions +align_var_def_inline = false # false/true + +# The span for aligning on '=' in assignments (0=don't align) +align_assign_span = 1 # number + +# The threshold for aligning on '=' in assignments (0=no limit) +align_assign_thresh = 0 # number + +# The span for aligning on '=' in enums (0=don't align) +align_enum_equ_span = 1 # number + +# The threshold for aligning on '=' in enums (0=no limit) +align_enum_equ_thresh = 0 # number + +# The span for aligning struct/union (0=don't align) +align_var_struct_span = 1 # number + +# The threshold for aligning struct/union member definitions (0=no limit) +align_var_struct_thresh = 0 # number + +# The gap for aligning struct/union member definitions +align_var_struct_gap = 0 # number + +# The span for aligning struct initializer values (0=don't align) +align_struct_init_span = 1 # number + +# The minimum space between the type and the synonym of a typedef +align_typedef_gap = 0 # number + +# The span for aligning single-line typedefs (0=don't align) +align_typedef_span = 1 # number + +# How to align typedef'd functions with other typedefs +# 0: Don't mix them at all +# 1: align the open paren with the types +# 2: align the function type name with the other type names +align_typedef_func = 0 # number + +# Controls the positioning of the '*' in typedefs. Just try it. +# 0: Align on typedef type, ignore '*' +# 1: The '*' is part of type name: typedef int *pint; +# 2: The '*' is part of the type, but dangling: typedef int *pint; +align_typedef_star_style = 0 # number + +# Controls the positioning of the '&' in typedefs. Just try it. +# 0: Align on typedef type, ignore '&' +# 1: The '&' is part of type name: typedef int &pint; +# 2: The '&' is part of the type, but dangling: typedef int &pint; +align_typedef_amp_style = 0 # number + +# The span for aligning comments that end lines (0=don't align) +align_right_cmt_span = 1 # number + +# If aligning comments, mix with comments after '}' and #endif with less than 3 spaces before the comment +align_right_cmt_mix = false # false/true + +# If a trailing comment is more than this number of columns away from the text it follows, +# it will qualify for being aligned. This has to be > 0 to do anything. +align_right_cmt_gap = 1 # number + +# Align trailing comment at or beyond column N; 'pulls in' comments as a bonus side effect (0=ignore) +align_right_cmt_at_col = 0 # number + +# The span for aligning function prototypes (0=don't align) +align_func_proto_span = 0 # number + +# Minimum gap between the return type and the function name. +align_func_proto_gap = 0 # number + +# Align function protos on the 'operator' keyword instead of what follows +align_on_operator = false # false/true + +# Whether to mix aligning prototype and variable declarations. +# If true, align_var_def_XXX options are used instead of align_func_proto_XXX options. +align_mix_var_proto = false # false/true + +# Align single-line functions with function prototypes, uses align_func_proto_span +align_single_line_func = false # false/true + +# Aligning the open brace of single-line functions. +# Requires align_single_line_func=true, uses align_func_proto_span +align_single_line_brace = false # false/true + +# Gap for align_single_line_brace. +align_single_line_brace_gap = 0 # number + +# The span for aligning ObjC msg spec (0=don't align) +align_oc_msg_spec_span = 0 # number + +# Whether to align macros wrapped with a backslash and a newline. +# This will not work right if the macro contains a multi-line comment. +align_nl_cont = true # false/true + +# # Align macro functions and variables together +align_pp_define_together = true # false/true + +# The minimum space between label and value of a preprocessor define +align_pp_define_gap = 0 # number + +# The span for aligning on '#define' bodies (0=don't align) +align_pp_define_span = 1 # number + +# Align lines that start with '<<' with previous '<<'. Default=true +align_left_shift = true # false/true + +# Span for aligning parameters in an Obj-C message call on the ':' (0=don't align) +align_oc_msg_colon_span = 0 # number + +# If true, always align with the first parameter, even if it is too short. +align_oc_msg_colon_first = false # false/true + +# Aligning parameters in an Obj-C '+' or '-' declaration on the ':' +align_oc_decl_colon = false # false/true + +# +# Newline adding and removing options +# + +# Whether to collapse empty blocks between '{' and '}' +nl_collapse_empty_body = false # false/true + +# Don't split one-line braced assignments - 'foo_t f = { 1, 2 };' +nl_assign_leave_one_liners = false # false/true + +# Don't split one-line braced statements inside a class xx { } body +nl_class_leave_one_liners = false # false/true + +# Don't split one-line enums: 'enum foo { BAR = 15 };' +nl_enum_leave_one_liners = false # false/true + +# Don't split one-line get or set functions +nl_getset_leave_one_liners = false # false/true + +# Don't split one-line function definitions - 'int foo() { return 0; }' +nl_func_leave_one_liners = false # false/true + +# Don't split one-line if/else statements - 'if(a) b++;' +nl_if_leave_one_liners = false # false/true + +# Don't split one-line OC messages +nl_oc_msg_leave_one_liner = false # false/true + +# Add or remove newlines at the start of the file +nl_start_of_file = ignore # ignore/add/remove/force + +# The number of newlines at the start of the file (only used if nl_start_of_file is 'add' or 'force' +nl_start_of_file_min = 0 # number + +# Add or remove newline at the end of the file +nl_end_of_file = force # ignore/add/remove/force + +# The number of newlines at the end of the file (only used if nl_end_of_file is 'add' or 'force') +nl_end_of_file_min = 1 # number + +# Add or remove newline between '=' and '{' +nl_assign_brace = force # ignore/add/remove/force + +# Add or remove newline between '=' and '[' (D only) +nl_assign_square = ignore # ignore/add/remove/force + +# Add or remove newline after '= [' (D only). Will also affect the newline before the ']' +nl_after_square_assign = ignore # ignore/add/remove/force + +# The number of blank lines after a block of variable definitions at the top of a function body +# 0 = No change (default) +nl_func_var_def_blk = 0 # number + +# The number of newlines before a block of typedefs +# 0 = No change (default) +nl_typedef_blk_start = 0 # number + +# The number of newlines after a block of typedefs +# 0 = No change (default) +nl_typedef_blk_end = 0 # number + +# The maximum consecutive newlines within a block of typedefs +# 0 = No change (default) +nl_typedef_blk_in = 0 # number + +# The number of newlines before a block of variable definitions not at the top of a function body +# 0 = No change (default) +nl_var_def_blk_start = 0 # number + +# The number of newlines after a block of variable definitions not at the top of a function body +# 0 = No change (default) +nl_var_def_blk_end = 0 # number + +# The maximum consecutive newlines within a block of variable definitions +# 0 = No change (default) +nl_var_def_blk_in = 0 # number + +# Add or remove newline between a function call's ')' and '{', as in: +# list_for_each(item, &list) { } +nl_fcall_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'enum' and '{' +nl_enum_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'struct and '{' +nl_struct_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'union' and '{' +nl_union_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'if' and '{' +nl_if_brace = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and 'else' +nl_brace_else = ignore # ignore/add/remove/force + +# Add or remove newline between 'else if' and '{' +# If set to ignore, nl_if_brace is used instead +nl_elseif_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'else' and '{' +nl_else_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'else' and 'if' +nl_else_if = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and 'finally' +nl_brace_finally = ignore # ignore/add/remove/force + +# Add or remove newline between 'finally' and '{' +nl_finally_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'try' and '{' +nl_try_brace = ignore # ignore/add/remove/force + +# Add or remove newline between get/set and '{' +nl_getset_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'for' and '{' +nl_for_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'catch' and '{' +nl_catch_brace = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and 'catch' +nl_brace_catch = ignore # ignore/add/remove/force + +# Add or remove newline between 'while' and '{' +nl_while_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'scope (x)' and '{' (D) +nl_scope_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'unittest' and '{' (D) +nl_unittest_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'version (x)' and '{' (D) +nl_version_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'using' and '{' +nl_using_brace = ignore # ignore/add/remove/force + +# Add or remove newline between two open or close braces. +# Due to general newline/brace handling, REMOVE may not work. +nl_brace_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'do' and '{' +nl_do_brace = ignore # ignore/add/remove/force + +# Add or remove newline between '}' and 'while' of 'do' statement +nl_brace_while = ignore # ignore/add/remove/force + +# Add or remove newline between 'switch' and '{' +nl_switch_brace = ignore # ignore/add/remove/force + +# Add a newline between ')' and '{' if the ')' is on a different line than the if/for/etc. +# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch, and nl_catch_brace. +nl_multi_line_cond = false # false/true + +# Force a newline in a define after the macro name for multi-line defines. +nl_multi_line_define = true # false/true + +# Whether to put a newline before 'case' statement +nl_before_case = false # false/true + +# Add or remove newline between ')' and 'throw' +nl_before_throw = ignore # ignore/add/remove/force + +# Whether to put a newline after 'case' statement +nl_after_case = false # false/true + +# Add or remove a newline between a case ':' and '{'. Overrides nl_after_case. +nl_case_colon_brace = ignore # ignore/add/remove/force + +# Newline between namespace and { +nl_namespace_brace = ignore # ignore/add/remove/force + +# Add or remove newline between 'template<>' and whatever follows. +nl_template_class = ignore # ignore/add/remove/force + +# Add or remove newline between 'class' and '{' +nl_class_brace = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in the constructor member initialization +nl_class_init_args = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name in a function definition +nl_func_type_name = force # ignore/add/remove/force + +# Add or remove newline between return type and function name inside a class {} +# Uses nl_func_type_name or nl_func_proto_type_name if set to ignore. +nl_func_type_name_class = ignore # ignore/add/remove/force + +# Add or remove newline between function scope and name in a definition +# Controls the newline after '::' in 'void A::f() { }' +nl_func_scope_name = ignore # ignore/add/remove/force + +# Add or remove newline between return type and function name in a prototype +nl_func_proto_type_name = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' +nl_func_paren = ignore # ignore/add/remove/force + +# Add or remove newline between a function name and the opening '(' in the definition +nl_func_def_paren = ignore # ignore/add/remove/force + +# Add or remove newline after '(' in a function declaration +nl_func_decl_start = ignore # ignore/add/remove/force + +# Add or remove newline after '(' in a function definition +nl_func_def_start = ignore # ignore/add/remove/force + +# Overrides nl_func_decl_start when there is only one parameter. +nl_func_decl_start_single = ignore # ignore/add/remove/force + +# Overrides nl_func_def_start when there is only one parameter. +nl_func_def_start_single = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in a function declaration +nl_func_decl_args = ignore # ignore/add/remove/force + +# Add or remove newline after each ',' in a function definition +nl_func_def_args = ignore # ignore/add/remove/force + +# Add or remove newline before the ')' in a function declaration +nl_func_decl_end = ignore # ignore/add/remove/force + +# Add or remove newline before the ')' in a function definition +nl_func_def_end = ignore # ignore/add/remove/force + +# Overrides nl_func_decl_end when there is only one parameter. +nl_func_decl_end_single = ignore # ignore/add/remove/force + +# Overrides nl_func_def_end when there is only one parameter. +nl_func_def_end_single = ignore # ignore/add/remove/force + +# Add or remove newline between '()' in a function declaration. +nl_func_decl_empty = ignore # ignore/add/remove/force + +# Add or remove newline between '()' in a function definition. +nl_func_def_empty = ignore # ignore/add/remove/force + +# Whether to put each OC message parameter on a separate line +# See nl_oc_msg_leave_one_liner +nl_oc_msg_args = false # false/true + +# Add or remove newline between function signature and '{' +nl_fdef_brace = force # ignore/add/remove/force + +# Add or remove a newline between the return keyword and return expression. +nl_return_expr = ignore # ignore/add/remove/force + +# Whether to put a newline after semicolons, except in 'for' statements +nl_after_semicolon = false # false/true + +# Whether to put a newline after brace open. +# This also adds a newline before the matching brace close. +nl_after_brace_open = false # false/true + +# If nl_after_brace_open and nl_after_brace_open_cmt are true, a newline is +# placed between the open brace and a trailing single-line comment. +nl_after_brace_open_cmt = false # false/true + +# Whether to put a newline after a virtual brace open with a non-empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open = false # false/true + +# Whether to put a newline after a virtual brace open with an empty body. +# These occur in un-braced if/while/do/for statement bodies. +nl_after_vbrace_open_empty = false # false/true + +# Whether to put a newline after a brace close. +# Does not apply if followed by a necessary ';'. +nl_after_brace_close = false # false/true + +# Whether to put a newline after a virtual brace close. +# Would add a newline before return in: 'if (foo) a++; return;' +nl_after_vbrace_close = false # false/true + +# Control the newline between the close brace and 'b' in: 'struct { int a; } b;' +# Affects enums, unions, and structures. If set to ignore, uses nl_after_brace_close +nl_brace_struct_var = ignore # ignore/add/remove/force + +# Whether to alter newlines in '#define' macros +nl_define_macro = false # false/true + +# Whether to not put blanks after '#ifxx', '#elxx', or before '#endif' +nl_squeeze_ifdef = false # false/true + +# Add or remove blank line before 'if' +nl_before_if = ignore # ignore/add/remove/force + +# Add or remove blank line after 'if' statement +nl_after_if = ignore # ignore/add/remove/force + +# Add or remove blank line before 'for' +nl_before_for = ignore # ignore/add/remove/force + +# Add or remove blank line after 'for' statement +nl_after_for = ignore # ignore/add/remove/force + +# Add or remove blank line before 'while' +nl_before_while = ignore # ignore/add/remove/force + +# Add or remove blank line after 'while' statement +nl_after_while = ignore # ignore/add/remove/force + +# Add or remove blank line before 'switch' +nl_before_switch = force # ignore/add/remove/force + +# Add or remove blank line after 'switch' statement +nl_after_switch = force # ignore/add/remove/force + +# Add or remove blank line before 'do' +nl_before_do = ignore # ignore/add/remove/force + +# Add or remove blank line after 'do/while' statement +nl_after_do = ignore # ignore/add/remove/force + +# Whether to double-space commented-entries in struct/enum +nl_ds_struct_enum_cmt = false # false/true + +# Whether to double-space before the close brace of a struct/union/enum +# (lower priority than 'eat_blanks_before_close_brace)' +nl_ds_struct_enum_close_brace = false # false/true + +# Add or remove a newline around a class colon. +# Related to pos_class_colon, nl_class_init_args, and pos_comma. +nl_class_colon = ignore # ignore/add/remove/force + +# Change simple unbraced if statements into a one-liner +# 'if(b)\n i++;' => 'if(b) i++;' +nl_create_if_one_liner = false # false/true + +# Change simple unbraced for statements into a one-liner +# 'for (i=0;i<5;i++)\n foo(i);' => 'for (i=0;i<5;i++) foo(i);' +nl_create_for_one_liner = false # false/true + +# Change simple unbraced while statements into a one-liner +# 'while (i<5)\n foo(i++);' => 'while (i<5) foo(i++);' +nl_create_while_one_liner = false # false/true + +# +# Positioning options +# + +# The position of arithmetic operators in wrapped expressions +pos_arith = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of assignment in wrapped expressions. +# Do not affect '=' followed by '{' +pos_assign = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of boolean operators in wrapped expressions +pos_bool = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of comparison operators in wrapped expressions +pos_compare = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of conditional (b ? t : f) operators in wrapped expressions +pos_conditional = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of the comma in wrapped expressions +pos_comma = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of the comma in the constructor initialization list +pos_class_comma = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# The position of colons between constructor and member initialization +pos_class_colon = ignore # ignore/join/lead/lead_break/lead_force/trail/trail_break/trail_force + +# +# Line Splitting options +# + +# Try to limit code width to N number of columns +code_width = 80 # number + +# Whether to fully split long 'for' statements at semi-colons +ls_for_split_full = false # false/true + +# Whether to fully split long function protos/calls at commas +ls_func_split_full = false # false/true + +# Whether to split lines as close to code_width as possible and ignore some groupings +ls_code_width = false # false/true + +# +# Blank line options +# + +# The maximum consecutive newlines +nl_max = 0 # number + +# The number of newlines after a function prototype, if followed by another function prototype +nl_after_func_proto = 0 # number + +# The number of newlines after a function prototype, if not followed by another function prototype +nl_after_func_proto_group = 0 # number + +# The number of newlines after '}' of a multi-line function body +nl_after_func_body = 0 # number + +# The number of newlines after '}' of a multi-line function body in a class declaration +nl_after_func_body_class = 0 # number + +# The number of newlines after '}' of a single line function body +nl_after_func_body_one_liner = 0 # number + +# The minimum number of newlines before a multi-line comment. +# Doesn't apply if after a brace open or another multi-line comment. +nl_before_block_comment = 0 # number + +# The minimum number of newlines before a single-line C comment. +# Doesn't apply if after a brace open or other single-line C comments. +nl_before_c_comment = 0 # number + +# The minimum number of newlines before a CPP comment. +# Doesn't apply if after a brace open or other CPP comments. +nl_before_cpp_comment = 0 # number + +# Whether to force a newline after a multi-line comment. +nl_after_multiline_comment = false # false/true + +# The number of newlines after '}' or ';' of a struct/enum/union definition +nl_after_struct = 0 # number + +# The number of newlines after '}' or ';' of a class definition +nl_after_class = 0 # number + +# The number of newlines before a 'private:', 'public:', 'protected:', 'signals:', or 'slots:' label. +# Will not change the newline count if after a brace open. +# 0 = No change. +nl_before_access_spec = 0 # number + +# The number of newlines after a 'private:', 'public:', 'protected:', 'signals:', or 'slots:' label. +# 0 = No change. +nl_after_access_spec = 0 # number + +# The number of newlines between a function def and the function comment. +# 0 = No change. +nl_comment_func_def = 0 # number + +# The number of newlines after a try-catch-finally block that isn't followed by a brace close. +# 0 = No change. +nl_after_try_catch_finally = 0 # number + +# The number of newlines before and after a property, indexer or event decl. +# 0 = No change. +nl_around_cs_property = 0 # number + +# The number of newlines between the get/set/add/remove handlers in C#. +# 0 = No change. +nl_between_get_set = 0 # number + +# Add or remove newline between C# property and the '{' +nl_property_brace = ignore # ignore/add/remove/force + +# Whether to remove blank lines after '{' +eat_blanks_after_open_brace = false # false/true + +# Whether to remove blank lines before '}' +eat_blanks_before_close_brace = false # false/true + +# How aggressively to remove extra newlines not in preproc. +# 0: No change +# 1: Remove most newlines not handled by other config +# 2: Remove all newlines and reformat completely by config +nl_remove_extra_newlines = 0 # number + +# Whether to put a blank line before 'return' statements, unless after an open brace. +nl_before_return = true # false/true + +# Whether to put a blank line after 'return' statements, unless followed by a close brace. +nl_after_return = false # false/true + +# Whether to put a newline after a Java annotation statement. +# Only affects annotations that are after a newline. +nl_after_annotation = ignore # ignore/add/remove/force + +# Controls the newline between two annotations. +nl_between_annotation = ignore # ignore/add/remove/force + +# +# Code modifying options (non-whitespace) +# + +# Add or remove braces on single-line 'do' statement +mod_full_brace_do = ignore # ignore/add/remove/force + +# Add or remove braces on single-line 'for' statement +mod_full_brace_for = ignore # ignore/add/remove/force + +# Add or remove braces on single-line function definitions. (Pawn) +mod_full_brace_function = ignore # ignore/add/remove/force + +# Add or remove braces on single-line 'if' statement. Will not remove the braces if they contain an 'else'. +mod_full_brace_if = ignore # ignore/add/remove/force + +# Make all if/elseif/else statements in a chain be braced or not. Overrides mod_full_brace_if. +# If any must be braced, they are all braced. If all can be unbraced, then the braces are removed. +mod_full_brace_if_chain = false # false/true + +# Don't remove braces around statements that span N newlines +mod_full_brace_nl = 0 # number + +# Add or remove braces on single-line 'while' statement +mod_full_brace_while = ignore # ignore/add/remove/force + +# Add or remove braces on single-line 'using ()' statement +mod_full_brace_using = ignore # ignore/add/remove/force + +# Add or remove unnecessary paren on 'return' statement +mod_paren_on_return = remove # ignore/add/remove/force + +# Whether to change optional semicolons to real semicolons +mod_pawn_semicolon = false # false/true + +# Add parens on 'while' and 'if' statement around bools +mod_full_paren_if_bool = false # false/true + +# Whether to remove superfluous semicolons +mod_remove_extra_semicolon = false # false/true + +# If a function body exceeds the specified number of newlines and doesn't have a comment after +# the close brace, a comment will be added. +mod_add_long_function_closebrace_comment = 0 # number + +# If a switch body exceeds the specified number of newlines and doesn't have a comment after +# the close brace, a comment will be added. +mod_add_long_switch_closebrace_comment = 0 # number + +# If an #ifdef body exceeds the specified number of newlines and doesn't have a comment after +# the #endif, a comment will be added. +mod_add_long_ifdef_endif_comment = 1 # number + +# If an #ifdef or #else body exceeds the specified number of newlines and doesn't have a comment after +# the #else, a comment will be added. +mod_add_long_ifdef_else_comment = 1 # number + +# If TRUE, will sort consecutive single-line 'import' statements [Java, D] +mod_sort_import = false # false/true + +# If TRUE, will sort consecutive single-line 'using' statements [C#] +mod_sort_using = false # false/true + +# If TRUE, will sort consecutive single-line '#include' statements [C/C++] and '#import' statements [Obj-C] +# This is generally a bad idea, as it may break your code. +mod_sort_include = false # false/true + +# If TRUE, it will move a 'break' that appears after a fully braced 'case' before the close brace. +mod_move_case_break = true # false/true + +# Will add or remove the braces around a fully braced case statement. +# Will only remove the braces if there are no variable declarations in the block. +mod_case_brace = ignore # ignore/add/remove/force + +# If TRUE, it will remove a void 'return;' that appears as the last statement in a function. +mod_remove_empty_return = true # false/true + +# +# Comment modifications +# + +# Try to wrap comments at cmt_width columns +cmt_width = 80 # number + +# Set the comment reflow mode (default: 0) +# 0: no reflowing (apart from the line wrapping due to cmt_width) +# 1: no touching at all +# 2: full reflow +cmt_reflow_mode = 0 # number + +# If false, disable all multi-line comment changes, including cmt_width. keyword substitution, and leading chars. +# Default is true. +cmt_indent_multi = true # false/true + +# Whether to group c-comments that look like they are in a block +cmt_c_group = false # false/true + +# Whether to put an empty '/*' on the first line of the combined c-comment +cmt_c_nl_start = false # false/true + +# Whether to put a newline before the closing '*/' of the combined c-comment +cmt_c_nl_end = true # false/true + +# Whether to group cpp-comments that look like they are in a block +cmt_cpp_group = false # false/true + +# Whether to put an empty '/*' on the first line of the combined cpp-comment +cmt_cpp_nl_start = false # false/true + +# Whether to put a newline before the closing '*/' of the combined cpp-comment +cmt_cpp_nl_end = true # false/true + +# Whether to change cpp-comments into c-comments +cmt_cpp_to_c = true # false/true + +# Whether to put a star on subsequent comment lines +cmt_star_cont = true # false/true + +# The number of spaces to insert at the start of subsequent comment lines +cmt_sp_before_star_cont = 0 # number + +# The number of spaces to insert after the star on subsequent comment lines +cmt_sp_after_star_cont = 0 # number + +# For multi-line comments with a '*' lead, remove leading spaces if the first and last lines of +# the comment are the same length. Default=True +cmt_multi_check_last = true # false/true + +# The filename that contains text to insert at the head of a file if the file doesn't start with a C/C++ comment. +# Will substitute $(filename) with the current file's name. +cmt_insert_file_header = "" # string + +# The filename that contains text to insert at the end of a file if the file doesn't end with a C/C++ comment. +# Will substitute $(filename) with the current file's name. +cmt_insert_file_footer = "" # string + +# The filename that contains text to insert before a function implementation if the function isn't preceded with a C/C++ comment. +# Will substitute $(function) with the function name and $(javaparam) with the javadoc @param and @return stuff. +# Will also substitute $(fclass) with the class name: void CFoo::Bar() { ... } +cmt_insert_func_header = "" # string + +# The filename that contains text to insert before a class if the class isn't preceded with a C/C++ comment. +# Will substitute $(class) with the class name. +cmt_insert_class_header = "" # string + +# The filename that contains text to insert before a Obj-C message specification if the method isn't preceded with a C/C++ comment. +# Will substitute $(message) with the function name and $(javaparam) with the javadoc @param and @return stuff. +cmt_insert_oc_msg_header = "" # string + +# If a preprocessor is encountered when stepping backwards from a function name, then +# this option decides whether the comment should be inserted. +# Affects cmt_insert_oc_msg_header, cmt_insert_func_header and cmt_insert_class_header. +cmt_insert_before_preproc = false # false/true + +# +# Preprocessor options +# + +# Control indent of preprocessors inside #if blocks at brace level 0 +pp_indent = ignore # ignore/add/remove/force + +# Whether to indent #if/#else/#endif at the brace level (true) or from column 1 (false) +pp_indent_at_level = false # false/true + +# If pp_indent_at_level=false, specifies the number of columns to indent per level. Default=1. +pp_indent_count = 1 # number + +# Add or remove space after # based on pp_level of #if blocks +pp_space = ignore # ignore/add/remove/force + +# Sets the number of spaces added with pp_space +pp_space_count = 0 # number + +# The indent for #region and #endregion in C# and '#pragma region' in C/C++ +pp_indent_region = 0 # number + +# Whether to indent the code between #region and #endregion +pp_region_indent_code = false # false/true + +# If pp_indent_at_level=true, sets the indent for #if, #else, and #endif when not at file-level +pp_indent_if = 0 # number + +# Control whether to indent the code between #if, #else and #endif when not at file-level +pp_if_indent_code = false # false/true + +# Whether to indent '#define' at the brace level (true) or from column 1 (false) +pp_define_at_level = false # false/true + +# You can force a token to be a type with the 'type' option. +# Example: +# type myfoo1 myfoo2 +# +# You can create custom macro-based indentation using macro-open, +# macro-else and macro-close. +# Example: +# macro-open BEGIN_TEMPLATE_MESSAGE_MAP +# macro-open BEGIN_MESSAGE_MAP +# macro-close END_MESSAGE_MAP +# +# You can assign any keyword to any type with the set option. +# set func_call_user _ N_ +# +# The full syntax description of all custom definition config entries +# is shown below: +# +# define custom tokens as: +# - embed whitespace in token using '' escape character, or +# put token in quotes +# - these: ' " and ` are recognized as quote delimiters +# +# type token1 token2 token3 ... +# ^ optionally specify multiple tokens on a single line +# define def_token output_token +# ^ output_token is optional, then NULL is assumed +# macro-open token +# macro-close token +# macro-else token +# set id token1 token2 ... +# ^ optionally specify multiple tokens on a single line +# ^ id is one of the names in token_enum.h sans the CT_ prefix, +# e.g. PP_PRAGMA +# +# all tokens are separated by any mix of ',' commas, '=' equal signs +# and whitespace (space, tab) +# diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..9740eed --- /dev/null +++ b/configure.ac @@ -0,0 +1,360 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +AC_PREREQ([2.68]) +AC_INIT([mu],[1.4.15],[https://github.com/djcb/mu/issues],[mu]) +AC_COPYRIGHT([Copyright (C) 2008-2020 Dirk-Jan C. Binnema]) +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_SRCDIR([mu/mu.cc]) +# libtoolize wants to put some stuff in here; if you have an old +# autotools/libtool setup. you can try to comment this out +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_AUX_DIR([build-aux]) + +m4_ifdef([AX_IS_RELEASE],[AX_IS_RELEASE([git-directory])]) +m4_ifdef([AX_CHECK_ENABLE_DEBUG],[AX_CHECK_ENABLE_DEBUG([yes])]) + +AM_INIT_AUTOMAKE([1.14 foreign no-dist-gzip tar-ustar dist-xz]) + +# silent build if we have a new enough automake +m4_ifdef([AM_SILENT_RULES], [AM_SILENT_RULES([yes])]) + +AS_IF([test x$prefix = xNONE],[prefix=/usr/local]) +AC_SUBST(prefix) + +# AC_PROG_CXX *before* AC_PROG_CC, otherwise configure won't error out +# when a c++ compiler is not found. Weird, huh? +AC_PROG_CXX +AC_PROG_CC +AC_PROG_CC_STDC +AC_PROG_CC_C99 +AC_PROG_INSTALL +AC_HEADER_STDC + +extra_flags="-Wformat-security \ + -Wstack-protector \ + -Wstack-protector-all \ + -Wno-cast-function-type" + +AX_CXX_COMPILE_STDCXX_14 +m4_ifdef([AX_COMPILER_FLAGS],[AX_COMPILER_FLAGS(,,[yes],${extra_flags})]) +AX_VALGRIND_CHECK + +LT_INIT + +AX_CODE_COVERAGE + +AC_PROG_AWK +AC_CHECK_PROG(SORT,sort,sort) + +AC_CHECK_HEADERS([wordexp.h]) + +# use the 64-bit versions +AC_SYS_LARGEFILE + +# asan is somewhat similar to valgrind, but has low enough overhead so it +# can be used during normal operation. +AC_ARG_ENABLE([asan],[AS_HELP_STRING([--enable-asan], + [Enable Address Sanitizer])], [use_asan=$enableval], [use_asan=no]) +AS_IF([test "x$use_asan" = "xyes"],[ + AC_SUBST(ASAN_CFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer") + AC_SUBST(ASAN_CXXFLAGS,"-fsanitize=address -static-libasan -fno-omit-frame-pointer") + AC_SUBST(ASAN_LDFLAGS, "-fsanitize=address -static-libasan -fno-omit-frame-pointer") +]) + +# check for makeinfo +AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no) +AM_CONDITIONAL(HAVE_MAKEINFO,test "x$have_makeinfo" = "xyes") + +# we need emacs for byte-compiling mu4e + +build_mu4e=no +AC_ARG_ENABLE([mu4e], + AS_HELP_STRING([--disable-mu4e],[Disable building mu4e])) +AS_IF([test "x$enable_mu4e" != "xno"], [ + AM_PATH_LISPDIR + AS_IF([test "x$lispdir" != "xno"], [ + emacs_version="$($EMACS --version | head -1)" + lispdir="${lispdir}/mu4e/" + ]) + AS_CASE([$emacs_version], + [*24.4*|*24.5*],[build_mu4e=yes], + [*25*|*26*|*27*|*28*],[build_mu4e=yes], + [AC_WARN([emacs is too old to build mu4e (need emacs >= 24.4)])]) +]) +AM_CONDITIONAL(BUILD_MU4E, test "x$build_mu4e" = "xyes") + +# we need some special tricks for filesystems that don't have d_type; +# e.g. Solaris. See mu-maildir.c. Explicitly disabling it is for +# testing purposes only +AC_ARG_ENABLE([dirent-d-type], + AC_HELP_STRING([--disable-dirent-d-type], + [Don't use dirent->d_type, even if you have it]), + [], [AC_STRUCT_DIRENT_D_TYPE] +) +AS_IF([test "x$ac_cv_member_struct_dirent_d_type" != "xyes"], + [use_dirent_d_type="no"], [use_dirent_d_type="yes"]) + +# support for d_ino (inode) in struct dirent is optional; if it's +# available we can sort direntries by inode and access them in that +# order; this is much faster on some file systems (such as extfs3). +# Explicitly disabling it is for testing purposes only. +AC_ARG_ENABLE([dirent-d-ino], + AC_HELP_STRING([--disable-dirent-d-ino], + [Don't use dirent->d_ino, even if you have it]), + [], [AC_STRUCT_DIRENT_D_INO] +) +AS_IF([test "x$ac_cv_member_struct_dirent_d_ino" != "xyes"], + [use_dirent_d_ino="no"], [use_dirent_d_ino="yes"]) + +AC_CHECK_FUNCS([memset memcpy realpath setlocale strerror getpass setsid]) +AC_CHECK_FUNCS([vasprintf strptime]) +AC_CHECK_FUNC(timegm,[],AC_MSG_ERROR([missing required function timegm])) + +# require pkg-config >= 0.28 (release in 2013; should be old enough...) +# with that version, we don't need the AC_SUBST stuff after PKG_CHECK. +m4_ifndef([PKG_PROG_PKG_CONFIG], + [m4_fatal([please install pkg-config >= 0.28 before running autoconf/autogen])]) +PKG_PROG_PKG_CONFIG(0.28) # latest version in buildroot +AS_IF([test -z "$PKG_CONFIG"], + AC_MSG_ERROR([ + *** pkg-config with version >= 0.28 could not be found. + *** + *** Make sure it is in your path, or set the PKG_CONFIG environment variable + *** to the full path to pkg-config.]) +) + +# glib2? +PKG_CHECK_MODULES(GLIB,glib-2.0 >= 2.38 gobject-2.0 gio-2.0) +glib_version="$($PKG_CONFIG --modversion glib-2.0)" + +# gmime, version 3.0 or higher +PKG_CHECK_MODULES(GMIME,gmime-3.0) +gmime_version="$($PKG_CONFIG --modversion gmime-3.0)" + +# gmime, version 3.0 or higher +PKG_CHECK_MODULES(JSON_GLIB,json-glib-1.0 >= 1.4,[have_json_glib=yes],[have_json_glib=no]) +AS_IF([test "x$have_json_glib" = "xyes"],[ + json_glib_version="$($PKG_CONFIG --modversion json-glib-1.0)" + AC_DEFINE(HAVE_JSON_GLIB,[1], [Do we support json-glib?]) +]) +AM_CONDITIONAL(HAVE_JSON_GLIB,[test "x$have_json_glib" = "xyes"]) + +# xapian checking - we need 1.4 at least +PKG_CHECK_MODULES(XAPIAN,xapian-core >= 1.4,[ + have_xapian=yes + xapian_version=$($PKG_CONFIG xapian-core --modversion) + AC_SUBST(XAPIAN_CXXFLAGS,${XAPIAN_CFLAGS}) +],[ + # fall back to the xapian-config script. Not sure if there are cases where the + # pkgconfig does not work, but xapian-config does, so keep this for now. + AC_MSG_NOTICE([falling back to xapian-config]) + AC_CHECK_PROG(XAPIAN_CONFIG,xapian-config,xapian-config,no) + AS_IF([test "x$XAPIAN_CONFIG" = "xno"],[ + AC_MSG_ERROR([ + *** xapian could not be found; please install it + *** e.g., in debian/ubuntu the package would be 'libxapian-dev' + *** If you compiled it yourself, you should ensure that xapian-config + *** is in your PATH.])], + [xapian_version=$($XAPIAN_CONFIG --version | sed -e 's/.* //')]) + + AS_CASE([$xapian_version], + [1.[[4-9]].[[0-9]]*], + [AC_MSG_NOTICE([xapian $xapian_version found.])], + [AC_MSG_ERROR([*** xapian version >= 1.4 needed, but version $xapian_version found.])]) + + XAPIAN_CXXFLAGS="$($XAPIAN_CONFIG --cxxflags)" + XAPIAN_LIBS="$($XAPIAN_CONFIG --libs)" + have_xapian="yes" + + AC_SUBST(XAPIAN_CXXFLAGS) + AC_SUBST(XAPIAN_LIBS) +]) +############################################################################### +# we set the set the version of the MuStore (Xapian database) layout +# here; it will become part of the db name, so we can automatically +# recreate the database when we have incompatible changes. +# +# note that MU_STORE_SCHEMA_VERSION does not follow mu versioning, as we +# hopefully don't have updates for each version; also, this has nothing to do +# with Xapian's software version +AC_DEFINE(MU_STORE_SCHEMA_VERSION,["451"],['Schema' version of the database]) +############################################################################### + +############################################################################### +# we need GTK+3 for some of the graphical tools +# use --without-gtk to disable it +AC_ARG_ENABLE([gtk],AS_HELP_STRING([--disable-gtk],[Disable GTK+])) +AS_IF([test "x$enable_gtk" != "xno"],[ + PKG_CHECK_MODULES(GTK,gtk+-3.0,[have_gtk=yes],[have_gtk=no]) + gtk_version="$($PKG_CONFIG --modversion gtk+-3.0)" +]) +AM_CONDITIONAL(HAVE_GTK,[test "x$have_gtk" = "xyes"]) + +# webkit? needed for the fancy web widget +# use --disable-webkit to disable it, even if you have it +# +# and note this is just a toy, not for distribution. +AC_ARG_ENABLE([webkit],AS_HELP_STRING([--disable-webkit],[Disable webkit])) +AS_IF([test "x$enable_webkit" != "xno"],[ + PKG_CHECK_MODULES(WEBKIT,webkit2gtk-4.0 >= 2.0, [have_webkit=yes],[have_webkit=no]) + AS_IF([test "x$have_webkit" = "xyes"],[ + webkit_version="$($PKG_CONFIG --modversion webkit2gtk-4.0)"]) +]) +AM_CONDITIONAL(HAVE_WEBKIT, [test "x$have_webkit" = "xyes"]) +AM_CONDITIONAL(BUILD_GUI,[test "x$have_webkit" = "xyes" -a "x$have_gtk" = "xyes"]) +############################################################################### + +############################################################################### +# build with guile2.2 when available and not disabled. +AC_ARG_ENABLE([guile], AS_HELP_STRING([--disable-guile],[Disable guile])) +AS_IF([test "x$enable_guile" != "xno"],[ + PKG_CHECK_MODULES(GUILE22, guile-2.2, [have_guile22=yes],[have_guile22=no]) + # this is a bit hacky; GUILE_PKG + AS_IF([test "x$have_guile22" = "xyes"],[ + GUILE_PKG([2.2]) + GUILE_PROGS + GUILE_FLAGS + AC_DEFINE_UNQUOTED([GUILE_BINARY],"$GUILE",[guile binary]) + AC_DEFINE(BUILD_GUILE,[1], [Do we support Guile?]) + AC_SUBST(GUILE_SNARF, [guile-snarf]) + guile_version=$($PKG_CONFIG guile-2.2 --modversion) + ]) +]) +AM_CONDITIONAL(BUILD_GUILE,[test "x$have_guile22" = "xyes"]) +############################################################################### + +############################################################################### +# optional readline +saved_libs=$LIBS +AX_LIB_READLINE +AC_SUBST(READLINE_LIBS,${LIBS}) +LIBS=$saved_libs +############################################################################### + +############################################################################### +# check for makeinfo +AC_CHECK_PROG(have_makeinfo,makeinfo,yes,no) +AM_CONDITIONAL(HAVE_MAKEINFO, [test "x$have_makeinfo" = "xyes"]) +############################################################################### + +############################################################################### +# docdir, so we can use it in mu4e-meta.el.in +AC_SUBST(MU_DOC_DIR, "${prefix}/share/doc/mu") +############################################################################### + +AC_CONFIG_FILES([ +Makefile +mu/Makefile +mu/mu-memcheck +lib/Makefile +lib/doxyfile +lib/utils/Makefile +lib/query/Makefile +mu4e/Makefile +mu4e/mu4e-meta.el +guile/Makefile +guile/texi.texi +guile/mu/Makefile +guile/examples/Makefile +guile/tests/Makefile +guile/scripts/Makefile +toys/Makefile +toys/mug/Makefile +man/Makefile +m4/Makefile +contrib/Makefile +],[ + [chmod +x mu/mu-memcheck] +]) +AC_OUTPUT + +dnl toys/msg2pdf/Makefile + +echo +echo "mu configuration is complete." +echo "------------------------------------------------" + +echo "mu version : $VERSION" +echo +echo "Xapian version : $xapian_version" +echo "GLib version : $glib_version" +echo "GMime version : $gmime_version" + +AM_COND_IF([HAVE_JSON_GLIB],[ +echo "Json-Glib version : $json_glib_version" +]) + +AM_COND_IF([BUILD_GUI],[ +echo "GTK+ version : $gtk_version" +echo "Webkit2/GTK+ version : $webkit_version" +]) + +AM_COND_IF([BUILD_GUILE],[ +echo "Guile version : $guile_version" +]) + +if test "x$build_mu4e" = "xyes"; then +echo "Emacs version : $emacs_version" +fi + +echo +echo "Have wordexp : $ac_cv_header_wordexp_h" +echo "Build mu4e emacs frontend : $build_mu4e" + +AM_COND_IF([BUILD_GUI],[ +echo "Build 'mug' toy-ui (gtk+/webkit) : yes"],[ +echo "Build 'mug' toy-ui (gtk+/webkit) : no" +]) + +echo + +echo "Have direntry->d_ino : $use_dirent_d_ino" +echo "Have direntry->d_type : $use_dirent_d_type" +echo "------------------------------------------------" +echo + +# +# Warnings / notes +# + +# makeinfo +if test "x$have_makeinfo" != "xyes"; then + echo "* You do not seem to have the makeinfo program; if you are building from git" + echo " you need that to create documentation for guile and emacs. It is in the" + echo " texinfo package in debian/ubuntu/fedora/... " + echo +fi + +# gui +AS_IF([test "x$buildgui" = "xyes"],[ + echo "* The demo UI will be built in toys/mug" + echo +]) + +# wordexp +AS_IF([test "x$ac_cv_header_wordexp_h" != "xyes"],[ + echo "* Your system does not seem to have the 'wordexp' function." + echo " This means that you cannot use shell-like expansion in options and " + echo " some other places. So, for example, instead of" + echo " --maildir=~/Maildir" + echo " you should use the complete path, something like:" + echo " --maildir=/home/user/Maildir" +]) + +echo +echo "Now, type 'make' (or 'gmake') to build mu" +echo diff --git a/contrib/Makefile.am b/contrib/Makefile.am new file mode 100644 index 0000000..faaf7cc --- /dev/null +++ b/contrib/Makefile.am @@ -0,0 +1,31 @@ +## Copyright (C) 2008-2013 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS=$(GMIME_CFLAGS) $(GLIB_CFLAGS) -I${prefix}/include +AM_CFLAGS=-Wall -Wextra -Wno-unused-parameter -Wdeclaration-after-statement -pedantic + +noinst_PROGRAMS=gmime-test +gmime_test_SOURCES=gmime-test.c +gmime_test_LDADD=$(GMIME_LIBS) $(GLIB_LIBS) + + +EXTRA_DIST= \ + mu-completion.zsh \ + mu-sexp-convert \ + mu.spec + diff --git a/contrib/gmime-test.c b/contrib/gmime-test.c new file mode 100644 index 0000000..5c59ed2 --- /dev/null +++ b/contrib/gmime-test.c @@ -0,0 +1,275 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2011-2017 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +/* gmime-test; compile with: + gcc -o gmime-test gmime-test.c -Wall -O0 -ggdb \ + `pkg-config --cflags --libs gmime-2.6` + */ + +#include +#include +#include +#include +#include + +static gchar* +get_recip (GMimeMessage *msg, GMimeAddressType atype) +{ + char *recep; + InternetAddressList *receps; + + receps = g_mime_message_get_addresses (msg, atype); + recep = (char*)internet_address_list_to_string (receps, NULL, FALSE); + + if (!recep || !*recep) { + g_free (recep); + return NULL; + } + + return recep; +} + +static gchar* +get_refs_str (GMimeMessage *msg) +{ + const gchar *str; + GMimeReferences *mime_refs; + int i, refs_len; + gchar *rv; + + str = g_mime_object_get_header (GMIME_OBJECT(msg), "References"); + if (!str) + return NULL; + + mime_refs = g_mime_references_parse (NULL, str); + refs_len = g_mime_references_length (mime_refs); + for (rv = NULL, i = 0; i < refs_len; ++i) { + const char* msgid; + msgid = g_mime_references_get_message_id (mime_refs, i); + rv = g_strdup_printf ("%s%s%s", + rv ? rv : "", + rv ? "," : "", + msgid); + } + g_mime_references_free (mime_refs); + + return rv; +} + +static void +print_date (GMimeMessage *msg) +{ + GDateTime *dt; + gchar *buf; + + dt = g_mime_message_get_date (msg); + if (!dt) + return; + + dt = g_date_time_to_local (dt); + buf = g_date_time_format (dt, "%c"); + g_date_time_unref (dt); + + if (buf) { + g_print ("Date : %s\n", buf); + g_free (buf); + } +} + + +static void +print_body (GMimeMessage *msg) +{ + GMimeObject *body; + GMimeDataWrapper *wrapper; + GMimeStream *stream; + + body = g_mime_message_get_body (msg); + + if (GMIME_IS_MULTIPART(body)) + body = g_mime_multipart_get_part (GMIME_MULTIPART(body), 0); + + if (!GMIME_IS_PART(body)) + return; + + wrapper = g_mime_part_get_content (GMIME_PART(body)); + if (!GMIME_IS_DATA_WRAPPER(wrapper)) + return; + + stream = g_mime_data_wrapper_get_stream (wrapper); + if (!GMIME_IS_STREAM(stream)) + return; + + do { + char buf[512]; + ssize_t len; + + len = g_mime_stream_read (stream, buf, sizeof(buf)); + if (len == -1) + break; + + if (write (fileno(stdout), buf, len) == -1) + break; + + if (len < (int)sizeof(buf)) + break; + + } while (1); +} + +static gboolean +test_message (GMimeMessage *msg) +{ + gchar *val; + const gchar *str; + + val = get_recip (msg, GMIME_ADDRESS_TYPE_FROM); + g_print ("From : %s\n", val ? val : "" ); + g_free (val); + + val = get_recip (msg, GMIME_ADDRESS_TYPE_TO); + g_print ("To : %s\n", val ? val : "" ); + g_free (val); + + val = get_recip (msg, GMIME_ADDRESS_TYPE_CC); + g_print ("Cc : %s\n", val ? val : "" ); + g_free (val); + + val = get_recip (msg, GMIME_ADDRESS_TYPE_BCC); + g_print ("Bcc : %s\n", val ? val : "" ); + g_free (val); + + str = g_mime_message_get_subject (msg); + g_print ("Subject: %s\n", str ? str : ""); + + print_date (msg); + + str = g_mime_message_get_message_id (msg); + g_print ("Msg-id : %s\n", str ? str : ""); + + { + gchar *refsstr; + refsstr = get_refs_str (msg); + g_print ("Refs : %s\n", refsstr ? refsstr : ""); + g_free (refsstr); + } + + print_body (msg); + + return TRUE; +} + + + +static gboolean +test_stream (GMimeStream *stream) +{ + GMimeParser *parser; + GMimeMessage *msg; + gboolean rv; + + parser = NULL; + msg = NULL; + + parser = g_mime_parser_new_with_stream (stream); + if (!parser) { + g_warning ("failed to create parser"); + rv = FALSE; + goto leave; + } + + msg = g_mime_parser_construct_message (parser, NULL); + if (!msg) { + g_warning ("failed to construct message"); + rv = FALSE; + goto leave; + } + + rv = test_message (msg); + +leave: + if (parser) + g_object_unref (parser); + + if (msg) + g_object_unref (msg); + + return rv; +} + + +static gboolean +test_file (const char *path) +{ + FILE *file; + GMimeStream *stream; + gboolean rv; + + stream = NULL; + file = NULL; + + file = fopen (path, "r"); + if (!file) { + g_warning ("cannot open file '%s': %s", path, + strerror(errno)); + rv = FALSE; + goto leave; + } + + stream = g_mime_stream_file_new (file); + if (!stream) { + g_warning ("cannot open stream for '%s'", path); + rv = FALSE; + goto leave; + } + + rv = test_stream (stream); + g_object_unref (stream); + return rv; + +leave: + if (file) + fclose (file); + + return rv; +} + + +int +main (int argc, char *argv[]) +{ + gboolean rv; + + if (argc != 2) { + g_printerr ("usage: %s \n", argv[0]); + return 1; + } + + setlocale (LC_ALL, ""); + + g_mime_init(); + + rv = test_file (argv[1]); + + g_mime_shutdown (); + + return rv ? 0 : 1; +} diff --git a/contrib/mu-completion.zsh b/contrib/mu-completion.zsh new file mode 100644 index 0000000..ea2bdbd --- /dev/null +++ b/contrib/mu-completion.zsh @@ -0,0 +1,124 @@ +#compdef mu + +## Copyright (C) 2011-2012 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# zsh completion for mu. Install this by copying/linking to this file somewhere in +# your $fpath; the link/copy must have a name starting with an underscore "_" + +# main dispatcher function +_mu() { + if (( CURRENT > 2 )) ; then + local cmd=${words[2]} + curcontext="${curcontext%:*:*}:mu-$cmd" + (( CURRENT-- )) + shift words + _call_function ret _mu_$cmd + return ret + else + _mu_commands + fi +} + + + +_mu_commands() { + local -a mu_commands + mu_commands=( + 'index:scan your maildirs and import their metadata in the database' + 'find:search for messages in the database' + 'view:display specific messages' + 'cfind:search for contacts (name + email) in the database' + 'extract:extract message-parts (attachments) and save or open them' + 'mkdir:create maildirs' +# below are not generally very useful, so let's not auto-complete them +# 'add: add a message to the database.' +# 'remove:remove a message from the database.' +# 'server:sart the mu server' +) + + _describe -t command 'command' mu_commands +} + +_mu_common_options=( + '--debug[output information useful for debugging mu]' + '--quiet[do not give any non-critical information]' + '--nocolor[do not use colors in some of the output]' + '--version[display mu version and copyright information]' + '--log-stderr[log to standard error]' +) + +_mu_db_options=( + '--muhome[use some non-default location for the mu database]:directory:_files' +) + +_mu_find_options=( + '--fields[fields to display in the output]' + '--sortfield[field to sort the output by]' + '--descending[sort in descending order]' + '--summary[include a summary of the message]' + '--summary-len[number of lines to use for the summary]' + '--bookmark[use a named bookmark]' + '--output[set the kind of output for the query]' +) + +_mu_view_options=( + '--summary[only show a summary of the message]' + '--summary-len[number of lines to use for the summary]' +) + + +_mu_view() { + _arguments -s : \ + $_mu_common_options \ + $_mu_view_options +} + +_mu_extract() { + _files +} + +_mu_find() { + _arguments -s : \ + $_mu_common_options \ + $_mu_db_options \ + $_mu_find_options +} + +_mu_index() { + _arguments -s : \ + $_mu_db_options \ + $_mu_common_options +}mu + +_mu_cleanup() { + _arguments -s : \ + $_mu_db_options \ + $_mu_common_options +} + + +_mu_mkdir() { + _arguments -s : \ + '--mode=[file mode for the new Maildir]:file mode: ' \ + $_mu_common_options +} + +_mu "$@" + +# Local variables: +# mode: sh +# End: diff --git a/contrib/mu-sexp-convert b/contrib/mu-sexp-convert new file mode 100755 index 0000000..b2835ac --- /dev/null +++ b/contrib/mu-sexp-convert @@ -0,0 +1,204 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; +;; a little hack to convert the output of +;; mu find --format=sexp +;; and +;; mu view --format=sexp +;; into XML or JSON + +(use-modules (ice-9 getopt-long) (ice-9 format) (ice-9 regex)) +(use-modules (sxml simple)) + +(define (mapconcat func lst sepa) + "Apply FUNC to elements of LST, concat the result as strings +separated by SEPA." + (if (null? lst) + "" + (string-append + (func (car lst)) + (if (null? (cdr lst)) + "" + (string-append sepa (mapconcat func (cdr lst) sepa)))))) + +(define (property-list? obj) + "Is OBJ a elisp-style property list (ie. a list of the +form (:symbol1 something :symbol2 somethingelse), as in an elisp +proplilst." + (and (list? obj) + (not (null? obj)) + (symbol? (car obj)) + (string= ":" (substring (symbol->string (car obj)) 0 1)))) + +(define (plist->pairs plist) + "Convert an elisp-style property list; e.g: + (:prop1 foo :prop2: bar ...) +into a list of pairs + ((prop1 . foo) (prop2 . bar) ...)." + (if (null? plist) + '() + (cons + (cons + (substring (symbol->string (car plist)) 1) + (cadr plist)) + (plist->pairs (cddr plist))))) + +(define (string->xml str) + "XML-encode STR." + ;; sneakily re-using sxml->xml + (call-with-output-string (lambda (port) (sxml->xml str port)))) + +(define (string->json str) + "Convert string into a JSON-encoded string." + (letrec ((convert + (lambda (lst) + (if (null? lst) + "" + (string-append + (cond + ((equal? (car lst) #\") "\\\"") + ((equal? (car lst) #\\) "\\\\") + ((equal? (car lst) #\/) "\\/") + ((equal? (car lst) #\bs) "\\b") + ((equal? (car lst) #\ff) "\\f") + ((equal? (car lst) #\lf) "\\n") + ((equal? (car lst) #\cr) "\\r") + ((equal? (car lst) #\ht) "\\t") + (#t (string (car lst)))) + (convert (cdr lst))))))) + (convert (string->list str)))) + +(define (etime->time_t t) + "Convert elisp time object T into a time_t value." + (logior (ash (car t) 16) (car (cdr t)))) + +(define (sexp->xml) + "Convert string INPUT to XML, return the XML (string)." + (letrec ((convert-xml + (lambda* (expr #:optional parent) + (cond + ((property-list? expr) + (mapconcat + (lambda (pair) + (format #f "\t<~a>~a\n" + (car pair) (convert-xml (cdr pair) (car pair)) (car pair))) + (plist->pairs expr) " ")) + ((list? expr) + (cond + ((member parent '("from" "to" "cc" "bcc")) + (mapconcat (lambda (addr) + (format #f "
~a~a
" + (if (string? (car addr)) + (format #f "~a" + (string->xml (car addr))) "") + (if (string? (cdr addr)) + (format #f "~a" + (string->xml (cdr addr))) ""))) + expr " ")) + ((string= parent "parts") "") ;; for now, ignore + ;; convert the crazy emacs time thingy to time_t... + ((string= parent "date") (format #f "~a" (etime->time_t expr))) + (#t + (mapconcat + (lambda (elm) (format #f "~a" (convert-xml elm))) expr "")))) + ((string? expr) (string->xml expr)) + ((symbol? expr) (format #f "~a" expr)) + ((number? expr) (number->string expr)) + (#t ".")))) + (msg->xml + (lambda () + (let ((expr (read))) + (if (not (eof-object? expr)) + (string-append (format #f "\n~a\n" (convert-xml expr)) (msg->xml)) + ""))))) + (format #f "\n\n~a" (msg->xml)))) + + +(define (sexp->json) + "Convert string INPUT to JSON, return the JSON (string)." + (letrec ((convert-json + (lambda* (expr #:optional parent) + (cond + ((property-list? expr) + (mapconcat + (lambda (pair) + (format #f "\n\t\"~a\": ~a" + (car pair) (convert-json (cdr pair) (car pair)))) + (plist->pairs expr) ", ")) + ((list? expr) + (cond + ((member parent '("from" "to" "cc" "bcc")) + (string-append "[" + (mapconcat (lambda (addr) + (format #f "{~a~a}" + (if (string? (car addr)) + (format #f "\"name\": \"~a\"," + (string->json (car addr))) "") + (if (string? (cdr addr)) + (format #f "\"email\": \"~a\"" + (string->json (cdr addr))) ""))) + expr ", ") + "]")) + ((string= parent "parts") "[]") ;; todo + ;; convert the crazy emacs time thingy to time_t... + ((string= parent "date") + (format #f "~a" (format #f "~a" (etime->time_t expr)))) + (#t + (string-append "[" + (mapconcat (lambda (elm) (format #f "~a" (convert-json elm))) expr ",") "]")))) + ((string? expr) + (format #f "\"~a\"" (string->json expr))) + ((symbol? expr) + (format #f "\"~a\"" expr)) + ((number? expr) (number->string expr)) + (#t ".")))) + (msg->json + (lambda (first) + (let ((expr (read))) + (if (not (eof-object? expr)) + (string-append (format #f "~a{~a\n}" + (if first "" ",\n") + (convert-json expr)) (msg->json #f)) + ""))))) + (format #f "[\n~a\n]" (msg->json #t)))) + +(define (main args) + (let* ((optionspec '((format (value #t)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-sexp-convert " + "--format=\n" + "reads from standard-input and prints to standard output\n")) + (outformat (or (option-ref options 'format #f) + (begin (display msg) (exit 1))))) + (cond + ((string= outformat "xml") + (format #t "~a\n" (sexp->xml))) + ((string= outformat "json") + (format #t "~a\n" (sexp->json))) + (#t (begin + (display msg) + (exit 1)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/contrib/mu.spec b/contrib/mu.spec new file mode 100644 index 0000000..1ed66d4 --- /dev/null +++ b/contrib/mu.spec @@ -0,0 +1,129 @@ + +# These refer to the release version +# When 0.9.9.6 gets out, remove the global pre line +%global pre pre2 +%global rel 1 + +Summary: A lightweight email search engine for Maildirs +Name: mu +Version: 0.9.9.6 +URL: https://github.com/djcb/mu +# From Packaging:NamingGuidelines for pre-relase versions: +# Release: 0.%{X}.%{alphatag} where %{X} is the release number +%if %{pre} +Release: 0.%{rel}.%{prerelease}%{?dist} +%else +Release: %{rel}%{?dist} +%endif + +License: GPLv3 +Group: Applications/Internet +BuildRoot: %{_tmppath}/%{name}-%{version}-build + +# Source is at ssaavedra repo because djcb has not yet this version tag created +Source0: http://github.com/ssaavedra/%{name}/archive/v%{version}%{?pre}.tar.gz +BuildRequires: emacs-el +BuildRequires: emacs +BuildRequires: gmime-devel +BuildRequires: guile-devel +BuildRequires: xapian-core-devel +BuildRequires: libuuid-devel +BuildRequires: texinfo +Requires: gmime +Requires: guile +Requires: xapian-core-libs +Requires: emacs-filesystem >= %{_emacs_version} + + +%description +E-mail is the 'flow' in the work flow of many people. Consequently, one spends a lot of time searching for old e-mails, to dig up some important piece of information. With people having tens of thousands of e-mails (or more), this is becoming harder and harder. How to find that one e-mail in an ever-growing haystack? +Enter mu. +'mu' is a set of command-line tools for Linux/Unix that enable you to quickly find the e-mails you are looking for, assuming that you store your e-mails in Maildirs (if you don't know what 'Maildirs' are, you are probably not using them). + +%package gtk +Group: Applications/Internet +Summary: GUI for using mu (called mug) +BuildRequires: gtk3-devel +BuildRequires: webkitgtk3-devel +Requires: gtk3 +Requires: gmime +Requires: webkitgtk3 +Requires: mu = %{version}-%{release} + +%description gtk +Mug is a simple GUI for mu from version 0.9. + +%package guile +Group: Applications/Internet +Summary: Guile scripting capabilities for mu +Requires: guile +Requires: mu = %{version}-%{release} +Requires(post): info +Requires(preun): info + +%description guile +Bindings for Guile to interact with mu. + + +%prep +%setup -n %{name}-%{version}%{?pre} -q + +%build +autoreconf -i +%configure +make %{?_smp_mflags} + +%install +rm -rf %{buildroot} +make install DESTDIR=%{buildroot} +install -p -c -m 755 %{_builddir}/%{buildsubdir}/toys/mug/mug %{buildroot}%{_bindir}/mug +cp -p %{_builddir}/%{buildsubdir}/mu4e/*.el %{buildroot}%{_emacs_sitelispdir}/mu4e/ +rm -f %{buildroot}%{_infodir}/dir + +%clean +rm -rf %{buildroot} + +%post +/sbin/install-info \ + --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || : +%preun +if [ $1 = 0 -a -f %{_infodir}/mu4e.info.gz ]; then + /sbin/install-info --delete \ + --info-dir=%{_infodir} %{_infodir}/mu4e.info.gz || : +fi + +%post guile +/sbin/install-info \ + --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || : + +%preun guile +if [ $1 = 0 -a -f %{_infodir}/mu-guile.info.gz ]; then + /sbin/install-info --delete \ + --info-dir=%{_infodir} %{_infodir}/mu-guile.info.gz || : +fi + + +%files +%defattr(-,root,root) +%{_bindir}/mu +%{_mandir}/man1/* +%{_mandir}/man5/* +%{_datadir}/mu/* + +%{_emacs_sitelispdir}/mu4e +%{_emacs_sitelispdir}/mu4e/*.elc +%{_emacs_sitelispdir}/mu4e/*.el +%{_infodir}/mu4e.info.gz + +%files gtk +%{_bindir}/mug + +%files guile +%{_libdir}/libguile-mu.* +%{_datadir}/guile/site/2.0/mu/* +%{_datadir}/guile/site/2.0/mu.scm +%{_infodir}/mu-guile.info.gz + +%changelog +* Wed Feb 12 2014 Santiago Saavedra - 0.9.9.5-1 +- Create first SPEC. diff --git a/gtest.mk b/gtest.mk new file mode 100644 index 0000000..159576e --- /dev/null +++ b/gtest.mk @@ -0,0 +1,34 @@ +## Copyright (C) 2011 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by the +## Free Software Foundation; either version 3, or (at your option) any +## later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +TEST_PROGS= + + +# NOTE: we set the locale/tz to some well-know values, so the tests +# (at least when running under 'make check') run in a predictable +# environment. There are specific tests different timezone, though. +# +test: all $(TEST_PROGS) + @export LC_ALL="en_US.utf8" + @export TZ="Europe/Helsinki" + @test -z "$(TEST_PROGS)" || gtester --verbose $(TEST_PROGS) || exit $$?; \ + test -z "$(SUBDIRS)" || \ + for subdir in $(SUBDIRS); do \ + test "$$subdir" = "." || \ + (cd ./$$subdir && $(MAKE) $(AM_MAKEFLAGS) $@ ) || exit $$? ; \ + done + +.PHONY: test gprof diff --git a/guile/Makefile.am b/guile/Makefile.am new file mode 100644 index 0000000..319b079 --- /dev/null +++ b/guile/Makefile.am @@ -0,0 +1,79 @@ +## Copyright (C) 2011-2013 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +# note, we need top_builddir for snarfing with 'make distcheck' (ie., +# with separate builddir) +SUBDIRS= . mu scripts examples tests + +AM_CPPFLAGS= \ + -I. -I${top_builddir} -I${top_srcdir}/lib \ + ${GUILE_CFLAGS} \ + ${GLIB_CFLAGS} + +# don't use -Werror, as it might break on other compilers +# use -Wno-unused-parameters, because some callbacks may not +# really need all the params they get +AM_CFLAGS=$(ASAN_CFLAGS) ${WARN_CFLAGS} +AM_CXXFLAGS=$(ASAN_CXXFLAGS) ${WARN_CXXFLAGS} + +lib_LTLIBRARIES= \ + libguile-mu.la + +libguile_mu_la_SOURCES= \ + mu-guile.c \ + mu-guile.h \ + mu-guile-message.c \ + mu-guile-message.h + +libguile_mu_la_LIBADD= \ + ${top_builddir}/lib/libmu.la \ + ${GUILE_LIBS} + +libguile_mu_la_LDFLAGS= \ + $(ASAN_LDFLAGS) \ + -shared \ + -export-dynamic \ + -Wl,-z,muldefs + +XFILES= \ + mu-guile.x \ + mu-guile-message.x + +info_TEXINFOS= \ + mu-guile.texi +mu_guile_TEXINFOS= \ + fdl.texi + +BUILT_SOURCES=$(XFILES) + +snarfcppopts= $(DEFS) $(AM_CPPFLAGS) $(CPPFLAGS) $(CFLAGS) $(AM_CPPFLAGS) +SUFFIXES = .x .doc +.c.x: + $(GUILE_SNARF) -o $@ $< $(snarfcppopts) + +# FIXME: GUILE_SITEDIR would be better, but that +# breaks 'make distcheck' +scmdir=${prefix}/share/guile/site/2.2/ +scm_DATA=mu.scm + +EXTRA_DIST=$(scm_DATA) + +## Add -MG to make the .x magic work with auto-dep code. +MKDEP = $(CC) -M -MG $(snarfcppopts) + +CLEANFILES=$(XFILES) diff --git a/guile/examples/Makefile.am b/guile/examples/Makefile.am new file mode 100644 index 0000000..7e126c5 --- /dev/null +++ b/guile/examples/Makefile.am @@ -0,0 +1,23 @@ +## Copyright (C) 2012-2013 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + msg-graphs \ + contacts-export \ + org2mu4e \ + mu-biff diff --git a/guile/examples/contacts-export b/guile/examples/contacts-export new file mode 100755 index 0000000..7e33c54 --- /dev/null +++ b/guile/examples/contacts-export @@ -0,0 +1,85 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (srfi srfi-1)) +(use-modules (mu)) + +(define (sort-by-freq c1 c2) + (< (mu:frequency c1) (mu:frequency c2))) + +(define (sort-by-newness c1 c2) + (< (mu:last-seen c1) (mu:last-seen c2))) + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (sort-by (value #t)) + (revert (value #f)) + (format (value #t)) + (limit (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: contacts-export [--help] [--muhome=] " + "--format= " + "--sort-by= [--revert] [--limit=]\n")) + (help (option-ref options 'help #f)) + (muhome (option-ref options 'muhome #f)) + (sort-by (or (option-ref options 'sort-by #f) "frequency")) + (revert (option-ref options 'revert #f)) + (form (or (option-ref options 'format #f) "plain")) + (limit (string->number (option-ref options 'limit "1000000")))) + (if help + (begin + (display msg) + (exit 0)) + (begin + (setlocale LC_ALL "") + (mu:initialize muhome) + (let* ((sort-func + (cond + ((string= sort-by "frequency") sort-by-freq) + ((string= sort-by "newness") sort-by-newness) + (else (begin (display msg) (exit 1))))) + (contacts '())) + ;; make a list of all contacts + (mu:for-each-contact + (lambda (c) (set! contacts (cons c contacts)))) + + ;; should we sort it? + (if sort-by + (set! contacts (sort! contacts + (if revert (negate sort-func) sort-func)))) + + ;; should we limit the number? + (if (and limit (< limit (length contacts))) + (set! contacts (take! contacts limit))) + ;; export! + (for-each + (lambda (c) + (format #t "~a\n" (mu:contact->string c form))) + contacts)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/msg-graphs b/guile/examples/msg-graphs new file mode 100755 index 0000000..654dd28 --- /dev/null +++ b/guile/examples/msg-graphs @@ -0,0 +1,133 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(setlocale LC_ALL "") + +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats) (mu plot)) +;;(use-modules (mu) (mu message) (mu stats) (mu plot)) + +(define (per-hour expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per hour matching ~a" expr) "Hour" "Messages" output)) + +(define (per-day expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (mu:weekday-numbers->names + (sort (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per weekday matching ~a" expr) "Day" "Messages" output)) + +(define (per-month expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (mu:month-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:mon (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per month matching ~a" expr) "Month" "Messages" output)) + + +(define (per-year-month expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort (mu:tabulate + (lambda (msg) + (string->number + (format #f "~d~2'0d" + (+ 1900 (tm:year (localtime (mu:date msg)))) + (tm:mon (localtime (mu:date msg)))))) + expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year/month matching ~a" expr) + "Year/Month" "Messages" output)) + + + +(define (per-year expr output) + "Count the total number of messages for each weekday (0-6 for Sun..Sat) that +match EXPR. OUTPUT corresponds to the output format, as per gnuplot's 'set +terminal'." + (mu:plot + (sort (mu:tabulate + (lambda (msg) + (+ 1900 (tm:year (localtime (mu:date msg))))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year matching ~a" expr) "Year" "Messages" output)) + + + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (what (value #t)) + (text (value #f)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-msg-stats [--help] [--text] " + "[--muhome=] " + "--what= [searchexpr]\n")) + (help (option-ref options 'help #f)) + (what (option-ref options 'what #f)) + (text (option-ref options 'text #f)) + ;; if `text' is `#f', use a graphical window by setting output to "wxt", + ;; else use text-mode plotting ("dumb") + (output (if text "dumb" "wxt")) + (muhome (option-ref options 'muhome #f)) + (restargs (option-ref options '() #f)) + (expr (if restargs (string-join restargs) ""))) + (if (or help (not what)) + (begin + (display msg) + (exit (if help 0 1)))) + (mu:initialize muhome) + (cond + ((string= what "per-hour") (per-hour expr output)) + ((string= what "per-day") (per-day expr output)) + ((string= what "per-month") (per-month expr output)) + ((string= what "per-year-month") (per-year-month expr output)) + ((string= what "per-year") (per-year expr output)) + (else (begin + (display msg) + (exit 1)))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/mu-biff b/guile/examples/mu-biff new file mode 100755 index 0000000..bc6d507 --- /dev/null +++ b/guile/examples/mu-biff @@ -0,0 +1,59 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2012 Dirk-Jan C. Binnema +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; script to list the message matching which are newer than +;; minutes + +;; use it, eg. like: +;; $ mu-biff --newer-than=`date +%s --date='5 minutes ago'` "maildir:/inbox" + + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (mu)) + +(define (main args) + (let* ((optionspec '((muhome (value #t)) + (newer-than (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu-biff [--help] [--muhome=]" + " [--newer-than=] ")) + (help (option-ref options 'help #f)) + (newer-than (string->number (option-ref options 'newer-than "0"))) + (muhome (option-ref options 'muhome #f)) + (query (string-concatenate (option-ref options '() '())))) + (if help + (begin (display msg) (newline) (exit 0)) + (begin + (mu:initialize muhome) + (mu:for-each-message + (lambda (msg) + (if (> (mu:timestamp msg) newer-than) + (format #t "~a ~a\n" + (mu:from msg) + (mu:subject msg)))) + query))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/examples/org2mu4e b/guile/examples/org2mu4e new file mode 100755 index 0000000..3556b9a --- /dev/null +++ b/guile/examples/org2mu4e @@ -0,0 +1,78 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; +;; Copyright (C) 2011-2012 Dirk-Jan C. Binnema +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(use-modules (ice-9 getopt-long) (ice-9 format)) +(use-modules (mu)) + +(define (display-org-header query) + "Print the header for the org-file for QUERY." + (format #t "* Messages matching '~a'\n\n" query)) + +(define (org-mu4e-link msg) + "Create a link for this message understandable by org-mu4e." + (let* ((subject ;; cleanup subject + (string-map + (lambda (kar) + (if (member kar '(#\] #\[)) #\space kar)) + (or (mu:subject msg) "No subject")))) + (format #f "[[mu4e:msgid:~a][~s]]" + (mu:message-id msg) subject))) + +(define (display-org-entry msg tag) + "Write an org entry for MSG." + (format #t "** ~a ~a\n\t~s\n\t~s\n" + (org-mu4e-link msg) + (if tag (string-concatenate `(":" ,tag "::")) "") + (or (mu:from msg) "?") + (let ((body (mu:body-txt msg))) + (if (not body) ;; get a 'summary' of the body text + "" + (string-map + (lambda (c) + (if (or (char=? c #\newline) (char=? c #\return)) + #\space + c)) + (substring body 0 (min (string-length body) 100))))))) + +(define (main args) + (let* ((optionspec '( (muhome (value #t)) + (tag (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (msg (string-append + "usage: mu4e-org [--help] [--muhome=] [--tag=] ")) + (help (option-ref options 'help #f)) + (tag (option-ref options 'tag #f)) + (muhome (option-ref options 'muhome #f)) + (query (string-concatenate (option-ref options '() '())))) + (if help + (begin (display msg) (exit 0)) + (begin + (mu:initialize muhome) + (display-org-header query) + (mu:for-each-message + (lambda (msg) (display-org-entry msg tag)) + query))))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/fdl.texi b/guile/fdl.texi new file mode 100644 index 0000000..96ce74e --- /dev/null +++ b/guile/fdl.texi @@ -0,0 +1,451 @@ +@c The GNU Free Documentation License. +@center Version 1.2, November 2002 + +@c This file is intended to be included within another document, +@c hence no sectioning command or @node. + +@display +Copyright @copyright{} 2000,2001,2002 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. +@end display + +@enumerate 0 +@item +PREAMBLE + +The purpose of this License is to make a manual, textbook, or other +functional and useful document @dfn{free} in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of ``copyleft'', which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + +@item +APPLICABILITY AND DEFINITIONS + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The ``Document'', below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as ``you''. You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A ``Modified Version'' of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A ``Secondary Section'' is a named appendix or a front-matter section +of the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall +subject (or to related matters) and contains nothing that could fall +directly within that overall subject. (Thus, if the Document is in +part a textbook of mathematics, a Secondary Section may not explain +any mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The ``Invariant Sections'' are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The ``Cover Texts'' are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A ``Transparent'' copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not ``Transparent'' is called ``Opaque''. + +Examples of suitable formats for Transparent copies include plain +@sc{ascii} without markup, Texinfo input format, La@TeX{} input +format, @acronym{SGML} or @acronym{XML} using a publicly available +@acronym{DTD}, and standard-conforming simple @acronym{HTML}, +PostScript or @acronym{PDF} designed for human modification. Examples +of transparent image formats include @acronym{PNG}, @acronym{XCF} and +@acronym{JPG}. Opaque formats include proprietary formats that can be +read and edited only by proprietary word processors, @acronym{SGML} or +@acronym{XML} for which the @acronym{DTD} and/or processing tools are +not generally available, and the machine-generated @acronym{HTML}, +PostScript or @acronym{PDF} produced by some word processors for +output purposes only. + +The ``Title Page'' means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, ``Title Page'' means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +A section ``Entitled XYZ'' means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as ``Acknowledgements'', +``Dedications'', ``Endorsements'', or ``History''.) To ``Preserve the Title'' +of such a section when you modify the Document means that it remains a +section ``Entitled XYZ'' according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + +@item +VERBATIM COPYING + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section 3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + +@item +COPYING IN QUANTITY + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + +@item +MODIFICATIONS + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +@enumerate A +@item +Use in the Title Page (and on the covers, if any) a title distinct +from that of the Document, and from those of previous versions +(which should, if there were any, be listed in the History section +of the Document). You may use the same title as a previous version +if the original publisher of that version gives permission. + +@item +List on the Title Page, as authors, one or more persons or entities +responsible for authorship of the modifications in the Modified +Version, together with at least five of the principal authors of the +Document (all of its principal authors, if it has fewer than five), +unless they release you from this requirement. + +@item +State on the Title page the name of the publisher of the +Modified Version, as the publisher. + +@item +Preserve all the copyright notices of the Document. + +@item +Add an appropriate copyright notice for your modifications +adjacent to the other copyright notices. + +@item +Include, immediately after the copyright notices, a license notice +giving the public permission to use the Modified Version under the +terms of this License, in the form shown in the Addendum below. + +@item +Preserve in that license notice the full lists of Invariant Sections +and required Cover Texts given in the Document's license notice. + +@item +Include an unaltered copy of this License. + +@item +Preserve the section Entitled ``History'', Preserve its Title, and add +to it an item stating at least the title, year, new authors, and +publisher of the Modified Version as given on the Title Page. If +there is no section Entitled ``History'' in the Document, create one +stating the title, year, authors, and publisher of the Document as +given on its Title Page, then add an item describing the Modified +Version as stated in the previous sentence. + +@item +Preserve the network location, if any, given in the Document for +public access to a Transparent copy of the Document, and likewise +the network locations given in the Document for previous versions +it was based on. These may be placed in the ``History'' section. +You may omit a network location for a work that was published at +least four years before the Document itself, or if the original +publisher of the version it refers to gives permission. + +@item +For any section Entitled ``Acknowledgements'' or ``Dedications'', Preserve +the Title of the section, and preserve in the section all the +substance and tone of each of the contributor acknowledgements and/or +dedications given therein. + +@item +Preserve all the Invariant Sections of the Document, +unaltered in their text and in their titles. Section numbers +or the equivalent are not considered part of the section titles. + +@item +Delete any section Entitled ``Endorsements''. Such a section +may not be included in the Modified Version. + +@item +Do not retitle any existing section to be Entitled ``Endorsements'' or +to conflict in title with any Invariant Section. + +@item +Preserve any Warranty Disclaimers. +@end enumerate + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled ``Endorsements'', provided it contains +nothing but endorsements of your Modified Version by various +parties---for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + +@item +COMBINING DOCUMENTS + +You may combine the Document with other documents released under this +License, under the terms defined in section 4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled ``History'' +in the various original documents, forming one section Entitled +``History''; likewise combine any sections Entitled ``Acknowledgements'', +and any sections Entitled ``Dedications''. You must delete all +sections Entitled ``Endorsements.'' + +@item +COLLECTIONS OF DOCUMENTS + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + +@item +AGGREGATION WITH INDEPENDENT WORKS + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an ``aggregate'' if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section 3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + +@item +TRANSLATION + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section 4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled ``Acknowledgements'', +``Dedications'', or ``History'', the requirement (section 4) to Preserve +its Title (section 1) will typically require changing the actual +title. + +@item +TERMINATION + +You may not copy, modify, sublicense, or distribute the Document except +as expressly provided for under this License. Any other attempt to +copy, modify, sublicense or distribute the Document is void, and will +automatically terminate your rights under this License. However, +parties who have received copies, or rights, from you under this +License will not have their licenses terminated so long as such +parties remain in full compliance. + +@item +FUTURE REVISIONS OF THIS LICENSE + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. See +@uref{http://www.gnu.org/copyleft/}. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License ``or any later version'' applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. +@end enumerate + +@page +@heading ADDENDUM: How to use this License for your documents + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + +@smallexample +@group + Copyright (C) @var{year} @var{your name}. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.2 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover + Texts. A copy of the license is included in the section entitled ``GNU + Free Documentation License''. +@end group +@end smallexample + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the ``with@dots{}Texts.'' line with this: + +@smallexample +@group + with the Invariant Sections being @var{list their titles}, with + the Front-Cover Texts being @var{list}, and with the Back-Cover Texts + being @var{list}. +@end group +@end smallexample + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. + +@c Local Variables: +@c ispell-local-pdict: "ispell-dict" +@c End: + diff --git a/guile/mu-guile-message.c b/guile/mu-guile-message.c new file mode 100644 index 0000000..04a1669 --- /dev/null +++ b/guile/mu-guile-message.c @@ -0,0 +1,627 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include +#include + +#include "mu-guile.h" + +#include +#include +#include +#include +#include + +/* pseudo field, not in Xapian */ +#define MU_GUILE_MSG_FIELD_ID_TIMESTAMP (MU_MSG_FIELD_ID_NUM + 1) + +/* some symbols */ +static SCM SYMB_PRIO_LOW, SYMB_PRIO_NORMAL, SYMB_PRIO_HIGH; +static SCM SYMB_FLAG_NEW, SYMB_FLAG_PASSED, SYMB_FLAG_REPLIED, + SYMB_FLAG_SEEN, SYMB_FLAG_TRASHED, SYMB_FLAG_DRAFT, + SYMB_FLAG_FLAGGED, SYMB_FLAG_SIGNED, SYMB_FLAG_ENCRYPTED, + SYMB_FLAG_HAS_ATTACH, SYMB_FLAG_UNREAD; +static SCM SYMB_CONTACT_TO, SYMB_CONTACT_CC, SYMB_CONTACT_BCC, + SYMB_CONTACT_FROM; + +struct _MuMsgWrapper { + MuMsg *_msg; + gboolean _unrefme; +}; +typedef struct _MuMsgWrapper MuMsgWrapper; +static long MSG_TAG; + +static gboolean +mu_guile_scm_is_msg (SCM scm) +{ + return SCM_NIMP(scm) && (long)SCM_CAR(scm) == MSG_TAG; +} + +SCM +mu_guile_msg_to_scm (MuMsg *msg) +{ + MuMsgWrapper *msgwrap; + + g_return_val_if_fail (msg, SCM_UNDEFINED); + + msgwrap = scm_gc_malloc (sizeof (MuMsgWrapper), "msg"); + msgwrap->_msg = msg; + msgwrap->_unrefme = FALSE; + + SCM_RETURN_NEWSMOB (MSG_TAG, msgwrap); +} + +struct _FlagData { + MuFlags flags; + SCM lst; +}; +typedef struct _FlagData FlagData; + + +#define MU_GUILE_INITIALIZED_OR_ERROR \ + do { if (!(mu_guile_initialized())) \ + return mu_guile_error (FUNC_NAME, 0, \ + "mu not initialized; call mu:initialize", \ + SCM_UNDEFINED); \ + } while (0) + + +static void +check_flag (MuFlags flag, FlagData *fdata) +{ + SCM flag_scm; + + if (!(fdata->flags & flag)) + return; + + switch (flag) { + case MU_FLAG_NEW: flag_scm = SYMB_FLAG_NEW; break; + case MU_FLAG_PASSED: flag_scm = SYMB_FLAG_PASSED; break; + case MU_FLAG_REPLIED: flag_scm = SYMB_FLAG_REPLIED; break; + case MU_FLAG_SEEN: flag_scm = SYMB_FLAG_SEEN; break; + case MU_FLAG_TRASHED: flag_scm = SYMB_FLAG_TRASHED; break; + case MU_FLAG_SIGNED: flag_scm = SYMB_FLAG_SIGNED; break; + case MU_FLAG_DRAFT: flag_scm = SYMB_FLAG_DRAFT; break; + case MU_FLAG_FLAGGED: flag_scm = SYMB_FLAG_FLAGGED; break; + case MU_FLAG_ENCRYPTED: flag_scm = SYMB_FLAG_ENCRYPTED; break; + case MU_FLAG_HAS_ATTACH: flag_scm = SYMB_FLAG_HAS_ATTACH; break; + case MU_FLAG_UNREAD: flag_scm = SYMB_FLAG_UNREAD; break; + default: flag_scm = SCM_UNDEFINED; + } + + fdata->lst = scm_append_x + (scm_list_2(fdata->lst, + scm_list_1 (flag_scm))); +} + +static SCM +get_flags_scm (MuMsg *msg) +{ + FlagData fdata; + + fdata.flags = mu_msg_get_flags (msg); + fdata.lst = SCM_EOL; + + mu_flags_foreach ((MuFlagsForeachFunc)check_flag, &fdata); + + return fdata.lst; +} + + +static SCM +get_prio_scm (MuMsg *msg) +{ + switch (mu_msg_get_prio (msg)) { + + case MU_MSG_PRIO_LOW: return SYMB_PRIO_LOW; + case MU_MSG_PRIO_NORMAL: return SYMB_PRIO_NORMAL; + case MU_MSG_PRIO_HIGH: return SYMB_PRIO_HIGH; + + default: + g_return_val_if_reached (SCM_UNDEFINED); + } +} + +static SCM +msg_string_list_field (MuMsg *msg, MuMsgFieldId mfid) +{ + SCM scmlst; + const GSList *lst; + + lst = mu_msg_get_field_string_list (msg, mfid); + + for (scmlst = SCM_EOL; lst; + lst = g_slist_next(lst)) { + SCM item; + item = scm_list_1 + (mu_guile_scm_from_str((const char*)lst->data)); + scmlst = scm_append_x (scm_list_2(scmlst, item)); + } + + return scmlst; +} + + +static SCM +get_body (MuMsg *msg, gboolean html) +{ + SCM data; + const char* body; + MuMsgOptions opts; + + opts = MU_MSG_OPTION_NONE; + + if (html) + body = mu_msg_get_body_html (msg, opts); + else + body = mu_msg_get_body_text (msg, opts); + + if (body) + data = mu_guile_scm_from_str (body); + else + data = SCM_BOOL_F; + + /* explicitly close the file backend, so we won't run of fds */ + mu_msg_unload_msg_file (msg); + + return data; +} + + +SCM_DEFINE (get_field, "mu:c:get-field", 2, 0, 0, + (SCM MSG, SCM FIELD), + "Get the field FIELD from message MSG.\n") +#define FUNC_NAME s_get_field +{ + MuMsgWrapper *msgwrap; + MuMsgFieldId mfid; + msgwrap = (MuMsgWrapper*) SCM_CDR(MSG); + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_integer_p(FIELD), FIELD, SCM_ARG2, FUNC_NAME); + + mfid = scm_to_int (FIELD); + SCM_ASSERT (mfid < MU_MSG_FIELD_ID_NUM || + mfid == MU_GUILE_MSG_FIELD_ID_TIMESTAMP, + FIELD, SCM_ARG2, FUNC_NAME); + + switch (mfid) { + case MU_MSG_FIELD_ID_PRIO: return get_prio_scm (msgwrap->_msg); + case MU_MSG_FIELD_ID_FLAGS: return get_flags_scm (msgwrap->_msg); + + case MU_MSG_FIELD_ID_BODY_HTML: + return get_body (msgwrap->_msg, TRUE); + case MU_MSG_FIELD_ID_BODY_TEXT: + return get_body (msgwrap->_msg, FALSE); + + /* our pseudo-field; we get it from the message file */ + case MU_GUILE_MSG_FIELD_ID_TIMESTAMP: + return scm_from_uint ( + (unsigned)mu_msg_get_timestamp(msgwrap->_msg)); + default: break; + } + + switch (mu_msg_field_type (mfid)) { + case MU_MSG_FIELD_TYPE_STRING: + return mu_guile_scm_from_str + (mu_msg_get_field_string(msgwrap->_msg, mfid)); + case MU_MSG_FIELD_TYPE_BYTESIZE: + case MU_MSG_FIELD_TYPE_TIME_T: + return scm_from_uint ( + mu_msg_get_field_numeric (msgwrap->_msg, mfid)); + case MU_MSG_FIELD_TYPE_INT: + return scm_from_int ( + mu_msg_get_field_numeric (msgwrap->_msg, mfid)); + case MU_MSG_FIELD_TYPE_STRING_LIST: + return msg_string_list_field (msgwrap->_msg, mfid); + default: + SCM_ASSERT (0, FIELD, SCM_ARG2, FUNC_NAME); + } +} +#undef FUNC_NAME + + + +struct _EachContactData { + SCM lst; + MuMsgContactType ctype; +}; +typedef struct _EachContactData EachContactData; + +static void +contacts_to_list (MuMsgContact *contact, EachContactData *ecdata) +{ + SCM item; + + if (ecdata->ctype != MU_MSG_CONTACT_TYPE_ALL && + mu_msg_contact_type (contact) != ecdata->ctype) + return; + + item = scm_list_1 + (scm_cons + (mu_guile_scm_from_str(mu_msg_contact_name (contact)), + mu_guile_scm_from_str(mu_msg_contact_email (contact)))); + + ecdata->lst = scm_append_x (scm_list_2(ecdata->lst, item)); +} + + +SCM_DEFINE (get_contacts, "mu:c:get-contacts", 2, 0, 0, + (SCM MSG, SCM CONTACT_TYPE), + "Get a list of contact information pairs.\n") +#define FUNC_NAME s_get_contacts +{ + MuMsgWrapper *msgwrap; + EachContactData ecdata; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_symbol_p (CONTACT_TYPE) || scm_is_bool(CONTACT_TYPE), + CONTACT_TYPE, SCM_ARG2, FUNC_NAME); + + if (CONTACT_TYPE == SCM_BOOL_F) + return SCM_UNSPECIFIED; /* nothing to do */ + else if (CONTACT_TYPE == SCM_BOOL_T) + ecdata.ctype = MU_MSG_CONTACT_TYPE_ALL; + else { + if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_TO)) + ecdata.ctype = MU_MSG_CONTACT_TYPE_TO; + else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_CC)) + ecdata.ctype = MU_MSG_CONTACT_TYPE_CC; + else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_BCC)) + ecdata.ctype = MU_MSG_CONTACT_TYPE_BCC; + else if (scm_is_eq (CONTACT_TYPE, SYMB_CONTACT_FROM)) + ecdata.ctype = MU_MSG_CONTACT_TYPE_FROM; + else + return mu_guile_error (FUNC_NAME, 0, + "invalid contact type", + SCM_UNDEFINED); + } + + ecdata.lst = SCM_EOL; + msgwrap = (MuMsgWrapper*) SCM_CDR(MSG); + mu_msg_contact_foreach (msgwrap->_msg, + (MuMsgContactForeachFunc)contacts_to_list, + &ecdata); + /* explicitly close the file backend, so we won't run out of fds */ + mu_msg_unload_msg_file (msgwrap->_msg); + + return ecdata.lst; +} +#undef FUNC_NAME + +struct _AttInfo { + SCM attlist; + gboolean attachments_only; +}; +typedef struct _AttInfo AttInfo; + +static void +each_part (MuMsg *msg, MuMsgPart *part, AttInfo *attinfo) +{ + char *mime_type, *filename; + SCM elm; + + if (!part->type) + return; + if (attinfo->attachments_only && + !mu_msg_part_maybe_attachment (part)) + return; + + mime_type = g_strdup_printf ("%s/%s", part->type, part->subtype); + filename = mu_msg_part_get_filename (part, FALSE); + + elm = scm_list_5 ( + /* msg */ + mu_guile_scm_from_str (mu_msg_get_path(msg)), + /* index */ + scm_from_uint(part->index), + /* filename or #f */ + filename ? mu_guile_scm_from_str (filename) : SCM_BOOL_F, + /* mime-type */ + mime_type ? mu_guile_scm_from_str (mime_type): SCM_BOOL_F, + /* size */ + part->size > 0 ? scm_from_uint (part->size) : SCM_BOOL_F); + + g_free (mime_type); + g_free (filename); + + attinfo->attlist = scm_cons (elm, attinfo->attlist); +} + + +SCM_DEFINE (get_parts, "mu:c:get-parts", 1, 1, 0, + (SCM MSG, SCM ATTS_ONLY), + "Get the list of mime-parts for MSG. If ATTS_ONLY is #t, only" + "get parts that are (look like) attachments. The resulting list has " + "elements which are list of the form (index name mime-type size).\n") +#define FUNC_NAME s_get_parts +{ + MuMsgWrapper *msgwrap; + AttInfo attinfo; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_is_bool(ATTS_ONLY), ATTS_ONLY, SCM_ARG2, FUNC_NAME); + + attinfo.attlist = SCM_EOL; /* empty list */ + attinfo.attachments_only = ATTS_ONLY == SCM_BOOL_T ? TRUE : FALSE; + + msgwrap = (MuMsgWrapper*) SCM_CDR(MSG); + mu_msg_part_foreach (msgwrap->_msg, MU_MSG_OPTION_NONE, + (MuMsgPartForeachFunc)each_part, + &attinfo); + + /* explicitly close the file backend, so we won't run of fds */ + mu_msg_unload_msg_file (msgwrap->_msg); + + return attinfo.attlist; +} +#undef FUNC_NAME + + +SCM_DEFINE (get_header, "mu:c:get-header", 2, 0, 0, + (SCM MSG, SCM HEADER), "Get an arbitrary HEADER from MSG.\n") +#define FUNC_NAME s_get_header +{ + MuMsgWrapper *msgwrap; + char *header; + SCM val; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT (mu_guile_scm_is_msg(MSG), MSG, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_is_string (HEADER)||HEADER==SCM_UNDEFINED, + HEADER, SCM_ARG2, FUNC_NAME); + + msgwrap = (MuMsgWrapper*) SCM_CDR(MSG); + header = scm_to_utf8_string (HEADER); + val = mu_guile_scm_from_str + (mu_msg_get_header(msgwrap->_msg, header)); + free (header); + + /* explicitly close the file backend, so we won't run of fds */ + mu_msg_unload_msg_file (msgwrap->_msg); + + return val; +} +#undef FUNC_NAME + + +static void +call_func (SCM FUNC, MuMsgIter *iter, const char* func_name) +{ + SCM msgsmob; + MuMsg *msg; + + msg = mu_msg_iter_get_msg_floating (iter); /* don't unref */ + + msgsmob = mu_guile_msg_to_scm (mu_msg_ref(msg)); + scm_call_1 (FUNC, msgsmob); +} + + +static MuMsgIter* +get_query_iter (MuQuery *query, const char* expr, int maxnum) +{ + MuMsgIter *iter; + GError *err; + + err = NULL; + iter = mu_query_run (query, expr, MU_MSG_FIELD_ID_NONE, maxnum, + MU_QUERY_FLAG_NONE, &err); + if (!iter) { + mu_guile_g_error ("", err); + g_clear_error (&err); + } + + return iter; +} + + +SCM_DEFINE (for_each_message, "mu:c:for-each-message", 3, 0, 0, + (SCM FUNC, SCM EXPR, SCM MAXNUM), +"Call FUNC for each msg in the message store matching EXPR. EXPR is" +"either a string containing a mu search expression or a boolean; in the former " +"case, limit the messages to only those matching the expression, in the " +"latter case, match /all/ messages if the EXPR equals #t, and match " +"none if EXPR equals #f.") +#define FUNC_NAME s_for_each_message +{ + MuMsgIter *iter; + char* expr; + + MU_GUILE_INITIALIZED_OR_ERROR; + + SCM_ASSERT (scm_procedure_p (FUNC), FUNC, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_is_bool(EXPR) || scm_is_string (EXPR), + EXPR, SCM_ARG2, FUNC_NAME); + SCM_ASSERT (scm_is_integer (MAXNUM), MAXNUM, SCM_ARG3, FUNC_NAME); + + if (EXPR == SCM_BOOL_F) + return SCM_UNSPECIFIED; /* nothing to do */ + + if (EXPR == SCM_BOOL_T) + expr = strdup (""); /* note, "" matches *all* messages */ + else + expr = scm_to_utf8_string(EXPR); + + iter = get_query_iter (mu_guile_instance()->query, expr, + scm_to_int(MAXNUM)); + free (expr); + if (!iter) + return SCM_UNSPECIFIED; + + while (!mu_msg_iter_is_done(iter)) { + call_func (FUNC, iter, FUNC_NAME); + mu_msg_iter_next (iter); + } + + mu_msg_iter_destroy (iter); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + + +static SCM +register_symbol (const char *name) +{ + SCM scm; + + scm = scm_from_utf8_symbol (name); + scm_c_define (name, scm); + scm_c_export (name, NULL); + + return scm; +} + +static void +define_symbols (void) +{ + SYMB_CONTACT_TO = register_symbol ("mu:contact:to"); + SYMB_CONTACT_CC = register_symbol ("mu:contact:cc"); + SYMB_CONTACT_FROM = register_symbol ("mu:contact:from"); + SYMB_CONTACT_BCC = register_symbol ("mu:contact:bcc"); + + SYMB_PRIO_LOW = register_symbol ("mu:prio:low"); + SYMB_PRIO_NORMAL = register_symbol ("mu:prio:normal"); + SYMB_PRIO_HIGH = register_symbol ("mu:prio:high"); + + SYMB_FLAG_NEW = register_symbol ("mu:flag:new"); + SYMB_FLAG_PASSED = register_symbol ("mu:flag:passed"); + SYMB_FLAG_REPLIED = register_symbol ("mu:flag:replied"); + SYMB_FLAG_SEEN = register_symbol ("mu:flag:seen"); + SYMB_FLAG_TRASHED = register_symbol ("mu:flag:trashed"); + SYMB_FLAG_DRAFT = register_symbol ("mu:flag:draft"); + SYMB_FLAG_FLAGGED = register_symbol ("mu:flag:flagged"); + SYMB_FLAG_SIGNED = register_symbol ("mu:flag:signed"); + SYMB_FLAG_ENCRYPTED = register_symbol ("mu:flag:encrypted"); + SYMB_FLAG_HAS_ATTACH = register_symbol ("mu:flag:has-attach"); + SYMB_FLAG_UNREAD = register_symbol ("mu:flag:unread"); +} + + +static struct { + const char* name; + unsigned val; +} VAR_PAIRS[] = { + + { "mu:field:bcc", MU_MSG_FIELD_ID_BCC }, + { "mu:field:body-html", MU_MSG_FIELD_ID_BODY_HTML }, + { "mu:field:body-txt", MU_MSG_FIELD_ID_BODY_TEXT }, + { "mu:field:cc", MU_MSG_FIELD_ID_CC }, + { "mu:field:date", MU_MSG_FIELD_ID_DATE }, + { "mu:field:flags", MU_MSG_FIELD_ID_FLAGS }, + { "mu:field:from", MU_MSG_FIELD_ID_FROM }, + { "mu:field:maildir", MU_MSG_FIELD_ID_MAILDIR }, + { "mu:field:message-id",MU_MSG_FIELD_ID_MSGID }, + { "mu:field:path", MU_MSG_FIELD_ID_PATH }, + { "mu:field:prio", MU_MSG_FIELD_ID_PRIO }, + { "mu:field:refs", MU_MSG_FIELD_ID_REFS }, + { "mu:field:size", MU_MSG_FIELD_ID_SIZE }, + { "mu:field:subject", MU_MSG_FIELD_ID_SUBJECT }, + { "mu:field:tags", MU_MSG_FIELD_ID_TAGS }, + { "mu:field:to", MU_MSG_FIELD_ID_TO }, + + /* non-Xapian field: timestamp */ + { "mu:field:timestamp", MU_GUILE_MSG_FIELD_ID_TIMESTAMP } +}; + +static void +define_vars (void) +{ + unsigned u; + for (u = 0; u != G_N_ELEMENTS(VAR_PAIRS); ++u) { + scm_c_define (VAR_PAIRS[u].name, + scm_from_uint (VAR_PAIRS[u].val)); + scm_c_export (VAR_PAIRS[u].name, NULL); + } +} + + +static SCM +msg_mark (SCM msg_smob) +{ + MuMsgWrapper *msgwrap; + msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob); + + msgwrap->_unrefme = TRUE; + + return SCM_UNSPECIFIED; +} + +static size_t +msg_free (SCM msg_smob) +{ + MuMsgWrapper *msgwrap; + msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob); + + if (msgwrap->_unrefme) + mu_msg_unref (msgwrap->_msg); + + return sizeof (MuMsgWrapper); +} + +static int +msg_print (SCM msg_smob, SCM port, scm_print_state * pstate) +{ + MuMsgWrapper *msgwrap; + msgwrap = (MuMsgWrapper*) SCM_CDR(msg_smob); + + scm_puts ("#_msg), + port); + + scm_puts (">", port); + + return 1; +} + + +void* +mu_guile_message_init (void *data) +{ + MSG_TAG = scm_make_smob_type ("msg", sizeof(MuMsgWrapper)); + + scm_set_smob_mark (MSG_TAG, msg_mark); + scm_set_smob_free (MSG_TAG, msg_free); + scm_set_smob_print (MSG_TAG, msg_print); + + define_vars (); + define_symbols (); + +#ifndef SCM_MAGIC_SNARFER +#include "mu-guile-message.x" +#endif /*SCM_MAGIC_SNARFER*/ + + return NULL; +} diff --git a/guile/mu-guile-message.h b/guile/mu-guile-message.h new file mode 100644 index 0000000..411644e --- /dev/null +++ b/guile/mu-guile-message.h @@ -0,0 +1,39 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_GUILE_MESSAGE_H__ +#define __MU_GUILE_MESSAGE_H__ + +#include + +G_BEGIN_DECLS + +/** + * Initialize this mu guile module. + * + * @param data + * + * @return + */ +void* mu_guile_message_init (void *data); + + +G_END_DECLS + +#endif /*__MU_GUILE_MESSAGE_H__*/ diff --git a/guile/mu-guile.c b/guile/mu-guile.c new file mode 100644 index 0000000..dcd7beb --- /dev/null +++ b/guile/mu-guile.c @@ -0,0 +1,256 @@ +/* +** Copyright (C) 2011-2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include +#include + +#include + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include +#pragma GCC diagnostic pop + +#include +#include +#include +#include +#include +#include + +#include "mu-guile.h" + + +SCM +mu_guile_scm_from_str (const char *str) +{ + if (!str) + return SCM_BOOL_F; + else + return scm_from_stringn (str, strlen(str), "UTF-8", + SCM_FAILED_CONVERSION_QUESTION_MARK); +} + + +SCM +mu_guile_error (const char *func_name, int status, + const char *fmt, SCM args) +{ + scm_error_scm (scm_from_locale_symbol ("MuError"), + scm_from_utf8_string (func_name ? func_name : ""), + scm_from_utf8_string (fmt), args, + scm_list_1 (scm_from_int (status))); + + return SCM_UNSPECIFIED; +} + +SCM +mu_guile_g_error (const char *func_name, GError *err) +{ + scm_error_scm (scm_from_locale_symbol ("MuError"), + scm_from_utf8_string (func_name), + scm_from_utf8_string (err ? err->message : "error"), + SCM_UNDEFINED, SCM_UNDEFINED); + + return SCM_UNSPECIFIED; +} + + + +/* there can be only one */ + +static MuGuile *_singleton = NULL; + +static gboolean +mu_guile_init_instance (const char *muhome) +{ + MuStore *store; + MuQuery *query; + GError *err; + + setlocale (LC_ALL, ""); + + if (!mu_runtime_init (muhome, "guile")) + return FALSE; + + err = NULL; + store = mu_store_new_readable + (mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB), + &err); + if (!store) + goto errexit; + + query = mu_query_new (store, &err); + mu_store_unref (store); + if (!query) + goto errexit; + + _singleton = g_new0 (MuGuile, 1); + _singleton->query = query; + + return TRUE; + +errexit: + mu_guile_g_error (__func__, err); + g_clear_error (&err); + return FALSE; +} + +static void +mu_guile_uninit_instance (void) +{ + g_return_if_fail (_singleton); + + mu_query_destroy (_singleton->query); + g_free (_singleton); + + _singleton = NULL; + + mu_runtime_uninit (); +} + + +MuGuile* +mu_guile_instance (void) +{ + g_return_val_if_fail (_singleton, NULL); + return _singleton; +} + +gboolean +mu_guile_initialized (void) +{ + return _singleton != NULL; +} + + +SCM_DEFINE_PUBLIC (mu_initialize, "mu:initialize", 0, 1, 0, + (SCM MUHOME), + "Initialize mu - needed before you call any of the other " + "functions. Optionally, you can provide MUHOME which should be an " + "absolute path to your mu home directory " + "-- typically, the default, ~/.cache/mu, should be just fine.") +#define FUNC_NAME s_mu_initialize +{ + char *muhome; + gboolean rv; + + SCM_ASSERT (scm_is_string (MUHOME) || MUHOME == SCM_BOOL_F || + SCM_UNBNDP(MUHOME), MUHOME, SCM_ARG1, FUNC_NAME); + + if (mu_guile_initialized()) + return mu_guile_error (FUNC_NAME, 0, "Already initialized", + SCM_UNSPECIFIED); + + if (SCM_UNBNDP(MUHOME) || MUHOME == SCM_BOOL_F) + muhome = NULL; + else + muhome = scm_to_utf8_string (MUHOME); + + rv = mu_guile_init_instance (muhome); + free (muhome); + + if (!rv) + return mu_guile_error (FUNC_NAME, 0, "Failed to initialize mu", + SCM_UNSPECIFIED); + + /* cleanup when we're exiting */ + atexit (mu_guile_uninit_instance); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + +SCM_DEFINE_PUBLIC (mu_initialized_p, "mu:initialized?", 0, 0, 0, + (void), "Whether mu is initialized or not.\n") +#define FUNC_NAME s_mu_initialized_p +{ + return mu_guile_initialized() ? SCM_BOOL_T : SCM_BOOL_F; +} +#undef FUNC_NAME + + +SCM_DEFINE (log_func, "mu:c:log", 1, 0, 1, (SCM LEVEL, SCM FRM, SCM ARGS), + "log some message at LEVEL using a list of ARGS applied to FRM" + "(in 'simple-format' notation).\n") +#define FUNC_NAME s_log_func +{ + gchar *output; + SCM str; + int level; + + SCM_ASSERT (scm_integer_p(LEVEL), LEVEL, SCM_ARG1, FUNC_NAME); + SCM_ASSERT (scm_is_string(FRM), FRM, SCM_ARG2, ""); + SCM_VALIDATE_REST_ARGUMENT(ARGS); + + level = scm_to_int (LEVEL); + if (level != G_LOG_LEVEL_MESSAGE && + level != G_LOG_LEVEL_WARNING && + level != G_LOG_LEVEL_CRITICAL) + return mu_guile_error (FUNC_NAME, 0, "invalid log level", + SCM_UNSPECIFIED); + + str = scm_simple_format (SCM_BOOL_F, FRM, ARGS); + + if (!scm_is_string (str)) + return SCM_UNSPECIFIED; + + output = scm_to_utf8_string (str); + g_log (G_LOG_DOMAIN, level, "%s", output); + free (output); + + return SCM_UNSPECIFIED; +} +#undef FUNC_NAME + + +static struct { + const char* name; + unsigned val; +} VAR_PAIRS[] = { + + { "mu:message", G_LOG_LEVEL_MESSAGE }, + { "mu:warning", G_LOG_LEVEL_WARNING }, + { "mu:critical", G_LOG_LEVEL_CRITICAL } +}; + +static void +define_vars (void) +{ + unsigned u; + for (u = 0; u != G_N_ELEMENTS(VAR_PAIRS); ++u) { + scm_c_define (VAR_PAIRS[u].name, + scm_from_uint (VAR_PAIRS[u].val)); + scm_c_export (VAR_PAIRS[u].name, NULL); + } +} + + +void* +mu_guile_init (void *data) +{ + define_vars (); + + +#ifndef SCM_MAGIC_SNARFER +#include "mu-guile.x" +#endif /*SCM_MAGIC_SNARFER*/ + + return NULL; +} diff --git a/guile/mu-guile.h b/guile/mu-guile.h new file mode 100644 index 0000000..fd230a5 --- /dev/null +++ b/guile/mu-guile.h @@ -0,0 +1,99 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_GUILE_H__ +#define __MU_GUILE_H__ + +#include +#include + +G_BEGIN_DECLS + + +struct _MuGuile { + MuQuery *query; +}; +typedef struct _MuGuile MuGuile; + +/** + * get the single MuGuile instance + * + * @return the instance or NULL in case of error + */ +MuGuile *mu_guile_instance (void); + + +/** + * whether mu-guile is initialized + * + * @return TRUE if MuGuile is Initialized, FALSE otherwise + */ +gboolean mu_guile_initialized (void); + + +/** + * raise a guile error (based on a GError) + * + * @param func_name function name + * @param err the error + * + * @return SCM_UNSPECIFIED + */ +SCM mu_guile_g_error (const char *func_name, GError *err); + + +/** + * raise a guile error + * + * @param func_name function + * @param status err code + * @param fmt format string for error msg + * @param args params for format string + * + * @return SCM_UNSPECIFIED + */ +SCM mu_guile_error (const char *func_name, int status, + const char *fmt, SCM args); + + +/** + * convert a const char* into an SCM -- either a string or, if str == + * NULL, #f. It assumes str is in UTF8 encoding, and replace + * characters with '?' if needed. + * + * @param str a string or NULL + * + * @return a guile string or #f + */ +SCM mu_guile_scm_from_str (const char *str); + + +/** + * Initialize this mu guile module. + * + * @param data + * + * @return + */ +void* mu_guile_init (void *data); + + +G_END_DECLS + +#endif /*__MU_GUILE_H__*/ diff --git a/guile/mu-guile.texi b/guile/mu-guile.texi new file mode 100644 index 0000000..c52213e --- /dev/null +++ b/guile/mu-guile.texi @@ -0,0 +1,995 @@ +\input texinfo.tex @c -*-texinfo-*- +@c %**start of header +@setfilename mu-guile.info +@settitle mu-guile user manual + +@c Use proper quote and backtick for code sections in PDF output +@c Cf. Texinfo manual 14.2 +@set txicodequoteundirected +@set txicodequotebacktick + +@documentencoding UTF-8 +@c %**end of header + +@include texi.texi + +@copying +Copyright @copyright{} 2012 Dirk-Jan C. Binnema + +@quotation +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.3 or +any later version published by the Free Software Foundation; with no +Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. A +copy of the license is included in the section entitled ``GNU Free +Documentation License.'' +@end quotation +@end copying + +@titlepage +@title @t{mu-guile} - extending @t{mu} with Guile Scheme +@subtitle version @value{mu-version} +@author Dirk-Jan C. Binnema + +@c The following two commands start the copyright page. +@page +@vskip 0pt plus 1filll +@insertcopying +@end titlepage + +@dircategory The Algorithmic Language Scheme +@direntry +* Mu-guile: (mu-guile). Guile-bindings for the mu e-mail indexer/searcher +@end direntry + +@contents + +@ifnottex +@node Top +@top mu-guile manual +@end ifnottex + +@iftex +@node Welcome to mu-guile +@unnumbered Welcome to mu-guile +@end iftex + +Welcome to @t{mu-guile}! + +@t{mu} is a program for indexing and searching your e-mails. It can search +your messages in many different ways, but sometimes that may not be +enough. If you have very specific queries, or want do generate some +statistics, you need some more power. + +@t{mu-guile} is made for those cases. @t{mu-guile} exposes the internals of +@t{mu} and its database to the @t{guile} programming language. Guile is the +@emph{GNU Ubiquitous Intelligent Language for Extensions} - a version of the +@emph{Scheme} programming language and the official GNU extension language. + +Guile/Scheme is a member of the @emph{Lisp} family of programming languages -- +like emacs-lisp, @emph{Racket}, Common Lisp. If you're not familiar with +Scheme, @t{mu-guile} is an excellent opportunity to learn a bit about! + +Trust me, it's not very hard -- and it's @emph{fun}! + +@menu +* Getting started:: +* Initializing mu-guile:: +* Messages:: +* Contacts:: +* Attachments and other parts:: +* Statistics:: +* Plotting data:: +* Writing scripts:: + +Appendices + +* Recipes:: Snippets do specific things +* GNU Free Documentation License:: The license of this manual. +@end menu + +@node Getting started +@chapter Getting started + +@menu +* Installation:: +* Making sure it works:: +@end menu + +This chapter walks you through the installation and the basic setup. + +@node Installation +@section Installation + +@t{mu-guile} is part of @t{mu} - by installing the latter, the former is +necessarily installed as well. At the time of writing, there are no +distribution-provided packaged versions of @t{mu-guile}; so for now, you need +to follow the steps below. + +@subsection Guile 2.x + +@t{mu-guile} is built automatically when @t{mu} is built, if you have +@t{guile} version 2 or higher. (@t{mu} checks for this during +@t{configure}). Thus, the first step is to ensure you have @t{guile} +installed. + +On Debian/Ubuntu you can install @t{guile} 2.x using the @t{guile-2.0-dev} +package (and its dependencies): +@example +$ sudo apt-get install guile-2.0-dev +@end example + +At the time of writing, there are no official packages for +Fedora@footnote{@url{https://bugzilla.redhat.com/show_bug.cgi?id=678238}}. If +you are using Fedora or any other system that does not have packages, you need +to compile @t{guile} from +source@footnote{@url{http://www.gnu.org/software/guile/manual/html_node/Obtaining-and-Installing-Guile.html#Obtaining-and-Installing-Guile}}. + +@subsection gnuplot + +For creating graphs with @t{mu-guile}, you need the @t{gnuplot} program -- +most likely, there is a package available for your system; for example: + +@example +$ sudo apt-get install gnuplot +@end example + +and in Fedora: + +@example +$ sudo yum install gnuplot +@end example + +@subsection mu + +Assuming @t{guile} 2.x is installed correctly, @t{mu} finds it during its +@t{configure}-stage, and creates @t{mu-guile}. Building @t{mu} follows the +normal steps -- please see the @t{mu} documentation for the details. + +The output of @t{./configure} should end with a little text describing the +detected versions of various libraries @t{mu} depends on. In particular, it +should mention the @t{guile} version, e.g. + +@example +Guile version : 2.0.3.82-a2c66 +@end example + +If you don't see any line referring to @t{guile}, please install it, and run +@t{configure} again. After a successful @t{./configure}, we can make and +install the package: + +@example +$ make && sudo make install +@end example + +@subsection mu-guile + +After this, @t{mu} and @t{mu-guile} are installed -- usually somewhere under +@t{/usr/local}.You may need to update @t{guile}'s @code{%load-path} to find it +there. You can check the current @code{%load-path} with the following: + +@example +guile -c '(display %load-path)(newline)' +@end example + +If necessary, you can add the @t{%load-path} by adding to your +@file{~/.guile}: + +@lisp +(set! %load-path (cons "/usr/local/share/guile/site/2.0" %load-path)) +@end lisp + +Or, alternatively, you can set @t{GUILE_LOAD_PATH}: +@example +export GUILE_LOAD_PATH=/usr/local/share/guile/site/2.0 +@end example + +In both cases the directory should be the directory that contains the +installed @t{mu.scm}; if you installed @t{mu} under a different prefix, you +must change the @code{%load-path} accordingly. After this, you should be ready +to go! + +Furthermore, you need to ensure that @t{guile} can find the mu-guile +library; for this we can use @code{LTDL_LIBRARY_PATH}, e.g. +@example +export LTDL_LIBRARY_PATH=/usr/local/lib +@end example + +@node Making sure it works +@section Making sure it works + +Assuming @t{mu-guile} has been installed correctly (@ref{Installation}), and +also assuming that you have already indexed your e-mail messages (if +necessary, see the @t{mu-index} man-page), we are ready to start @t{mu-guile}; +a session may look something like this: + +@cartouche +@verbatim +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@(guile-user)> +@end verbatim +@end cartouche + +@noindent +Now, copy-paste the following after the prompt: + +@cartouche +@lisp +(use-modules (mu)) +(mu:initialize) +(for-each + (lambda(msg) + (format #t "Subject: ~a\n" (mu:subject msg))) + (mu:message-list "hello")) +@end lisp +@end cartouche + +@noindent +After pressing @key{Enter}, you should get a list of all subjects of messages +that match @t{hello}: + +@verbatim +... +Subject: RE: The Bird Serpent War Cataclysm +Subject: Hello! +Subject: Re: post-run tomorrow +Subject: When all is lost +... +@end verbatim + +@noindent +If all this works, congratulations! @t{mu-guile} is installed now, ready to +serve your every searching need! + +@node Initializing mu-guile +@chapter Initializing mu-guile + +We now have installed @t{mu-guile}, and in @ref{Making sure it works} +confirmed that things work by trying some simple script. In this and the +following chapters, we take a closer look at programming with @t{mu-guile}. + +It is possible to write separate programs with @t{mu-guile}, but for now we'll +do things @emph{interactively}, that is, from the Guile-prompt +(``@abbr{REPL}''). + +As we have seen, we start our @t{mu-guile} session by starting @t{guile}: + +@verbatim +$ guile +@end verbatim + +@cartouche +@verbatim +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@(guile-user)> +@end verbatim +@end cartouche + +The first thing we need to do is loading the modules. All the basics are in +the @t{(mu)} module, with some statistical extras in @t{(mu stats)}, and some +graph plotting functionality in @t{(mu plot)}@footnote{@code{(mu plot)} +requires the @t{gnuplot} program}. Let's load all of them: +@verbatim +scheme@(guile-user)> (use-modules (mu) (mu stats) (mu plot)) +@end verbatim + +The first time you do this, @t{guile} will probably respond by showing some +messages about compiling the modules, and then return to you with another +prompt. Before we can do anything with @t{mu guile}, we need to initialize the +system: + +@verbatim +scheme@(guile-user)> (mu:initialize) +@end verbatim + +This opens the database for reading, using the default location of +@file{~/.cache/mu}@footnote{If you keep your @t{mu} database in a non-standard +place, use @code{(mu:initialize "/path/to/my/mu/")}} + +Now, @t{mu-guile} is ready to go. In the next chapter, we go through the +modules and show what you can do with them. + +@node Messages +@chapter Messages + +In this chapter, we discuss searching messages and doing things with them. + +@menu +* Finding messages:: query for messages in the database +* Message methods:: what methods are available for messages? +* Example - the longest subject:: find the messages with the longest subject +@end menu + +@node Finding messages +@section Finding messages +Now we are ready to retrieve some messages from the system. There are two main +procedures to do this: + +@itemize +@item @code{(mu:message-list [])} +@item @code{(mu:for-each-message [])} +@end itemize + +@noindent +The first procedure, @code{mu:message-list} returns a list of all messages +matching @t{}; if you leave @t{} out, it +returns @emph{all} messages. For example, to get all messages with @t{coffee} +in the subject line: + +@verbatim +scheme@(guile-user)> (mu:message-list "subject:coffee") +$1 = (#< 9040640> #< 9040630> + #< 9040570>) +@end verbatim + +@noindent +Apparently, we have three messages matching @t{subject:coffee}, so we get a +list of three @code{} objects. Let's just use the +@code{mu:subject} procedure ('method') provided by @code{} objects +to retrieve the subject-field (more about methods in the next section). + +For your convenience, @t{guile} has saved the result of our last query in a +variable called @t{$1}, so to get the subject of the first message in the +list, we can do: + +@verbatim +scheme@(guile-user)> (mu:subject (car $1)) +$2 = "Re: best coffee ever!" +@end verbatim + +@noindent +The second procedure we mentioned, @code{mu:for-each-message}, executes some +procedure for each message matched by the search expression (or @emph{all} +messages if the search expression is omitted): + +@verbatim +scheme@(guile-user)> (mu:for-each-message + (lambda(msg) + (display (mu:subject msg)) + (newline)) + "subject:coffee") +Re: best coffee ever! +best coffee ever! +Coffee beans +scheme@(guile-user)> +@end verbatim + +@noindent +Using @code{mu:message-list} and/or +@code{mu:for-each-message}@footnote{Implementation node: +@code{mu:message-list} is implemented in terms of @code{mu:for-each-message}, +not the other way around. Due to the way @t{mu} works, +@code{mu:for-each-message} is rather more efficient than a combination of +@code{for-each} and @code{mu:message-list}} and a couple of @t{} +methods, together with what Guile/Scheme provides, should allow for many +interesting programs. + +@node Message methods +@section Message methods + +Now that we've seen how to retrieve lists of message objects +(@code{}), let's see what we can do with such an object. + +@code{} defines the following methods that all take a single +@code{} object as a parameter. We won't go into the exact meanings +for all of these procedures here - for the details about various flags / +properties, please refer to the @t{mu-find} man-page. + +@itemize +@item @code{(mu:bcc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none +@item @code{(mu:body-html msg)}: : the html body of the message, or @t{#f} if there is none +@item @code{(mu:body-txt msg)}: the plain-text body of the message, or @t{#f} if there is none +@item @code{(mu:cc msg)}: the @t{Bcc} field of the message, or @t{#f} if there is none +@item @code{(mu:date msg)}: the @t{Date} field of the message, or 0 if there is none +@item @code{(mu:flags msg)}: list of message-flags for this message +@item @code{(mu:from msg)}: the @t{From} field of the message, or @t{#f} if there is none +@item @code{(mu:maildir msg)}: the maildir this message lives in, or @t{#f} if there is none +@item @code{(mu:message-id msg)}: the @t{Message-Id} field of the message, or @t{#f} if there is none +@item @code{(mu:path msg)}: the file system path for this message +@item @code{(mu:priority msg)}: the priority of this message (either @t{mu:prio:low}, @t{mu:prio:normal} or @t{mu:prio:high} +@item @code{(mu:references msg)}: the list of messages (message-ids) this message +refers to in(mu: the @t{References:} header +@item @code{(mu:size msg)}: size of the message in bytes +@item @code{(mu:subject msg)}: the @t{Subject} field of the message, or @t{#f} if there is none. +@item @code{(mu:tags msg)}: list of tags for this message +@item @code{(mu:timestamp msg)}: the timestamp (mtime) of the message file, or +#f if there is none. +message file +@item @code{(mu:to msg)}: the sender of the message, or @t{#f} if there is none +@end itemize + +With these methods, we can query messages for their properties; for example: + +@verbatim +scheme@(guile-user)> (define msg (car (mu:message-list "snow"))) +scheme@(guile-user)> (mu:subject msg) +$1 = "Re: Running in the snow is beautiful" +scheme@(guile-user)> (mu:flags msg) +$2 = (mu:flag:replied mu:flag:seen) +scheme@(guile-user)> (strftime "%F" (localtime (mu:date msg))) +$3 = "2011-01-15" +@end verbatim + +There are a couple more methods: +@itemize +@item @code{(mu:header msg "")} returns an arbitrary message +header (or @t{#f} if not found) -- e.g. @code{(header msg "User-Agent")} +@item If you include the @t{mu contact} module, the @code{(mu:contacts +msg [contact-type])} method (to get a list of contacts) is +added. @xref{Contacts}. +@item If you include the @t{mu part} module, the @code{((mu:parts msg)} and +@code{(mu:attachments msg)} methods are added. @xref{Attachments and other parts}. +@end itemize + +@node Example - the longest subject +@section Example - the longest subject + +Now, let's write a little example -- let's find out what is the @emph{longest +subject} of any e-mail messages we received in the year 2011. You can try +this if you put the following in a separate file, make it executable, and run +it like any program. + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(use-modules (srfi srfi-1)) + +(mu:initialize) + +;; note: (subject msg) => #f if there is no subject +(define list-of-subjects + (map (lambda (msg) + (or (mu:subject msg) "")) (mu:message-list "date:2011..2011"))) +;; see the mu-find manpage for the date syntax + +(define longest-subject + (fold (lambda (subj1 subj2) + (if (> (string-length subj1) (string-length subj2)) + subj1 subj2)) + "" list-of-subjects)) + +(format #t "Longest subject: ~s\n" longest-subject) +@end lisp + +There are many other ways to solve the same problem, for example by using an +iterative approach with @code{mu:for-each-message}, but it should show how one +can easily write little programs to answer specific questions about your +e-mail corpus. + +@node Contacts +@chapter Contacts + +We can retrieve the sender and recipients of an e-mail message using methods +like @code{mu:from}, @code{mu:to} etc.; @xref{Message methods}. These +procedures return the list of recipients as a single string; however, often it +is more useful to deal with recipients as separate objects. + +@menu +* Contact procedures and objects:: +* All contacts:: +* Utility procedures:: +* Example - mutt export:: +@end menu + + +@node Contact procedures and objects +@section Contact procedures and objects + +Message objects (@pxref{Messages}) have a method @t{mu:contacts}: + + @code{(mu:contacts [])} + +The @t{} is a symbol, one of @code{mu:to}, @code{mu:from}, +@code{mu:cc} or @code{mu:bcc}. This will then get the contact objects for the +contacts of the corresponding type. If you leave out the contact-type (or +specify @t{#t} for it, you will get a list of @emph{all} contact objects for +the message. + +A contact object (@code{}) has two methods: +@itemize +@item @code{mu:name} returns the name of the contact, or #f if there is none +@item @code{mu:email} returns the e-mail address of the contact, or #f if there is none +@end itemize + +Let's get a list of all names and e-mail addresses in the 'To:' field, of +messages matching 'book': + +@lisp +(use-modules (mu)) +(mu:initialize) +(mu:for-each-message + (lambda (msg) + (for-each + (lambda (contact) + (format #t "~a => ~a\n" + (or (mu:email contact) "") (or (mu:name contact) "no-name"))) + (mu:contacts msg mu:contact:to))) + "book") +@end lisp + +This shows what the methods do, but for many uses, it would be more useful to +have each of the contacts only show up @emph{once} - for that, please refer to +@xref{All contacts}. + +@node All contacts +@section All contacts + +Sometimes you may want to inspect @emph{all} the different contacts in the +@t{mu} database. This is useful, for instance, when exporting contacts to some +external format that can then be important in an e-mail program. + +To enable this, there is the procedure @code{mu:for-each-contact}, defined as + + @code{(mu:for-each-contact procedure [search-expression])}. + +This will aggregate the unique contacts from @emph{all} messages matching +@t{} (when it is left empty, it will match all messages in +the database), and execute @t{procedure} for each of them. + +The @t{procedure} receives an object of the type @t{}, +which is a @emph{subclass} of the @t{} class discussed in +@xref{Contact procedures and objects}. @t{} objects +expose the following additional methods: + +@itemize +@item @code{(mu:frequency )}: returns the @emph{number of times} this contact occurred in +one of the address fields +@item @code{(mu:last-seen )}: returns the @emph{most recent time} the contact was +seen in one of the address fields, as a @t{time_t} value +@end itemize + +The method assumes an e-mail address is unique for a certain contact; if a +certain e-mail address occurs with different names, it uses the most recent +non-empty name. + +@node Utility procedures +@section Utility procedures + +To make dealing with contacts even easier, there are a number of utility +procedures that can save you a bit of typing. + +For converting contacts to some textual form, there is @code{(mu:contact->string + format)}, which takes a contact and returns a text string with +the given format. Currently supported formats are @t{"org-contact}, @t{"mutt-alias"}, +@t{"mutt-ab"}, @t{"wanderlust"} and @t{"plain"}. + + +@node Example - mutt export +@section Example - mutt export + +Let's see how we could export the addresses in the @t{mu} database to the +addressbook format of the venerable +@t{mutt}@footnote{@url{http://www.mutt.org/}} e-mail client. + +The addressbook format that @t{mutt} uses is a sequence of lines that look +something like: +@verbatim +alias [] "<" ">" +@end verbatim + +@t{mu guile} provides the procedure @code{(mu:contact->string +format)} that we can use to do the conversion. + +We may want to focus on people with whom we have frequent correspondence; so +we may want to limit ourselves to people we have seen at least 10 times in the +last year. + +It is a bit hard to @emph{guess} the nick name for e-mail contacts, but +@code{mu:contact->string} tries something based on the name. You can always +adjust them later by hand, obviously. + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(mu:initialize) + +;; Get a list of contacts that were seen at least 20 times since 2010 +(define (selected-contacts) + (let ((addrs '()) + (start (car (mktime (car (strptime "%F" "2010-01-01"))))) + (minfreq 20)) + (mu:for-each-contact + (lambda (contact) + (if (and (mu:email contact) + (>= (mu:frequency contact) minfreq) + (>= (mu:last-seen contact) start)) + (set! addrs (cons contact addrs))))) + addrs)) + +(for-each + (lambda (contact) + (format #t "~a\n" (mu:contact->string contact "mutt-alias"))) + (selected-contacts)) +@end lisp + +This simple program could be improved in many ways; this is left as an +exercise to the reader. + +@node Attachments and other parts +@chapter Attachments and other parts + +To deal with @emph{attachments}, or, more in general @emph{MIME-parts}, there +is the @t{mu part} module. + +@menu +* Parts and their methods:: +* Attachment example:: +@end menu + +@node Parts and their methods +@section Parts and their methods +The module defines the @code{} class, and adds two methods to +@code{} objects: +@itemize +@item @code{(mu:parts msg)} - returns a list @code{} objects, one for +each MIME-parts in the message. +@item @code{(mu:attachments msg)} - like @code{parts}, but only list those MIME-parts +that look like proper attachments. +@end itemize + +A @code{} object exposes a few methods to get information about the +part: +@itemize +@item @code{(mu:name )} - returns the file name of the mime-part, or @code{#f} if +there is none. +@item @code{(mu:mime-type )} - returns the mime-type of the mime-part, or @code{#f} +if there is none. +@item @code{(mu:size )} - returns the size in bytes of the mime-part +@end itemize + +@c Then, we may want to save the part to a file; this can be done using either: +@c @itemize +@c @item @code{(mu:save part )} - save a part to a temporary file, return the file +@c name@footnote{the temporary filename is a predictable procedure of (user-id, +@c msg-path, part-index)} +@c @item @code{(mu:save-as )} - save part to file at path +@c @end itemize + +@node Attachment example +@section Attachment example + +Let's look at some small example. Let's get a list of the biggest attachments +in messages about Luxemburg: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu)) +(mu:initialize) + +(define (all-attachments expr) + "Return a list of (name . size) for all attachments in messages +matching EXPR." + (let ((pairs '())) + (mu:for-each-message + (lambda (msg) + (for-each + (lambda (att) ;; add (filename . size) to the list + (set! pairs (cons (cons (mu:name att) (or (mu:size att) 0)) pairs))) + (mu:attachments msg))) + expr) + pairs)) + +(for-each + (lambda (att) + (format #t "~a: ~,1fKb\n" + (car att) (exact->inexact (/ (cdr att) 1024)))) + (sort (all-attachments "Luxemburg") + (lambda (att1 att2) + (< (cdr att1) (cdr att2))))) +@end lisp + +As an exercise for the reader, you might want to re-rewrite the +@code{all-attachments} in terms of @code{mu:message-list}, which would +probably be a bit more elegant. + + +@node Statistics +@chapter Statistics + +@t{mu-guile} offers some convenience procedures to determine various statistics +about the messages in the database. + +@menu +* Basics:: @code{mu:count}, @code{mu:average}, ... +* Tabulating values:: @code{mu:tabulate} +* Most frequent values:: @code{mu:top-n-most-frequent} +@end menu + +@node Basics +@section Basics + +Let's look at some of the basic statistical operations available, in an +interactive session: +@example +GNU Guile 2.0.5.123-4bd53 +Copyright (C) 1995-2012 Free Software Foundation, Inc. + +Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'. +This program is free software, and you are welcome to redistribute it +under certain conditions; type `,show c' for details. + +Enter `,help' for help. +scheme@@(guile-user)> ;; load modules, initialize mu +scheme@@(guile-user)> (use-modules (mu) (mu stats)) +scheme@@(guile-user)> (mu:initialize) +scheme@@(guile-user)> +scheme@@(guile-user)> ;; count the number of messages with 'hello' in their subject +scheme@@(guile-user)> (mu:count "subject:hello") +$1 = 162 +scheme@@(guile-user)> ;; average the size of messages with hello in their subject +scheme@@(guile-user)> (mu:average mu:size "subject:hello") +$2 = 34597733/81 +scheme@@(guile-user)> (exact->inexact $2) +$3 = 427132.506172839 +scheme@@(guile-user)> ;; calculate the correlation between message size and +scheme@@(guile-user)> ;; subject length +scheme@@(guile-user)> (mu:correl mu:size (lambda (msg) + (string-length (mu:subject msg))) "subject:hello") +$5 = -0.10804368622292 +scheme@@(guile-user)> +@end example + +@node Tabulating values +@section Tabulating values + +@code{(mu:tabulate [])} applies @t{} to each +message matching @t{} (leave empty to match @emph{all} messages), +and returns a associative list (a list of pairs) with each of the different +results of @t{} and their frequencies. For fields that contain lists +of values (such as address-fields), each of the values in the list is added +separately. + +@subsection Example: messages per weekday + +We demonstrate @code{mu:tabulate} with an example. Suppose we want to know how +many messages we receive per weekday: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu) (mu stats) (mu plot)) +(mu:initialize) + +;; create a list like (("Sun" . 13) ("Mon" . 23) ...) +(define weekday-table + (mu:weekday-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg))))) + (lambda (a b) (< (car a) (car b)))))) + +(for-each + (lambda (elm) + (format #t "~a: ~a\n" (car elm) (cdr elm))) + weekday-table) +@end lisp + + +The procedure @code{weekday-table} uses @code{mu:tabulate-message} to get the +frequencies per hour -- this returns a list of pairs: +@verbatim +((5 . 2339) (0 . 2278) (4 . 2800) (2 . 3184) (6 . 1856) (3 . 2833) (1 . 2993)) +@end verbatim + +We sort these pairs by the day number, and then apply +@code{mu:weekday-numbers->names}, which takes the list, and returns a list +where the day numbers are replace by there abbreviated name (in the current +locale). Note, there is also @code{mu:month-numbers->names}. + +The script then outputs these numbers in the following form: + +@verbatim +Sun: 2278 +Mon: 2993 +Tue: 3184 +Wed: 2833 +Thu: 2800 +Fri: 2339 +Sat: 1856 +@end verbatim + +Clearly, Saturday is a slow day for e-mail... + +@node Most frequent values +@section Most frequent values + +In the above example, the number of values is small (the seven weekdays); +however, in many cases there can be many different values (for example, all +different message subjects), many of which may not be very interesting -- all +we need to know is the top-10 of most frequently seen values. + +This is fairly easy to achieve using @code{mu:tabulate} -- to get the top-10 +subjects@footnote{this requires the @code{(srfi srfi-1)}-module}, we can use +something like this: +@lisp +(take + (sort + (mu:tabulate mu:subject) + (lambda (a b) (> (cdr a) (cdr b)))) + 10) +@end lisp + +If this is not short enough, @t{mu-guile} offers a convenience procedure to do +this: @code{mu:top-n-most-frequent}. For example, to get the top-10 people we +sent mail to most often: + +@lisp +(mu:top-n-most-frequent mu:to 10 "maildir:/sent") +@end lisp + +Can't make it much easier than that! + + +@node Plotting data +@chapter Plotting data + +You can plot the results in the format produced by @code{mu:tabulate} with the +@t{(mu plot)} module, an experimental module that requires the +@t{gnuplot}@footnote{@url{http://www.gnuplot.info/}} program to be installed +on your system. + +The @code{mu:plot-histogram} procedure takes the following arguments: + +@code{(mu:plot-histogram <x-label> <y-label> [<want-ascii>])} + +Here, @code{<data>} is a table of data in the format that @code{mu:tabulate} +produces. @code{<title>}, @code{<x-label>} and @code{<y-lablel>} are, +respectively, the title of the graph, and the labels for X- and +Y-axis. Finally, if you pass @t{#t} for the final @code{<want-ascii>} +parameter, a plain-text rendering of the graph will be produced; otherwise, a +graphical window will be shown. + +An example should clarify how this works in practice; let's plot the number of +message per hour: + +@lisp +#!/bin/sh +exec guile -s $0 $@ +!# + +(use-modules (mu) (mu stats) (mu plot)) +(mu:initialize) + +(define (mail-per-hour-table) + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg))))) + (lambda (x y) (< (car x) (car y))))) + +(mu:plot-histogram (mail-per-hour-table) "Mail per hour" "Hour" "Frequency") +@end lisp + +@cartouche +@verbatim + Mail per hour +Frequency + 1200 ++--+--+--+--+-+--+--+--+--+-+--+--+--+-+--+--+--+--+-+--+--+--+--++ + |+ + + + + + + "/tmp/fileHz7D2u" using 2:xticlabels(1) ******** + 1100 ++ *** +* + **** * * * + 1000 *+ * **** * +* + * * ****** **** * ** * * + 900 *+ * * ** **** * **** ** * +* + * * * ** * * ********* * ** ** * * + 800 *+ * **** ** * * * * ** * * ** ** * +* + 700 *+ *** **** * ** * * * * ** **** * ** ** * +* + * * * **** * * ** * * * * ** * **** ** ** * * + 600 *+ * **** * * * * ** * * * * ** * * * ** ** * +* + * * ** * * * * * ** * * * * ** * * * ** ** * * + 500 *+ * ** * * * * * ** * * * * ** * * * ** ** * +* + * * ** **** *** * * * ** * * * * ** * * * ** ** * * + 400 *+ * ** ** **** * * * * * ** * * * * ** * * * ** ** * +* + *+ *+**+**+* +*******+* +* +*+ *+**+* +*+ *+ *+**+* +*+ *+**+**+* +* + 300 ******************************************************************** + 0 1 2 3 4 5 6 7 8 910 11 12 1314 15 16 17 1819 20 21 22 23 + Hour +@end verbatim +@end cartouche + +@node Writing scripts +@chapter Writing scripts + +The @t{mu} program has built-in support for running guile-scripts, and comes +with a number of examples. + +You can get a list of all scripts with the @t{mu script} command: +@verbatim +$ mu script +Available scripts (use --verbose for details): + * find-dups: find duplicate messages + * msgs-count: count the number of messages matching some query + * msgs-per-day: graph the number of messages per day + * msgs-per-hour: graph the number of messages per hour + * msgs-per-month: graph the number of messages per month + * msgs-per-year: graph the number of messages per year + * msgs-per-year-month: graph the number of messages per year-month +@end verbatim + +You can then execute such a script by its name: +@verbatim +$ mu msgs-per-month --textonly --query=hello + + + Messages per month matching hello + + 240 ++-+-----+----+-----+-----+-----+----+-----+-----+-----+----+-----+-++ + | + + + + "/tmp/filewi9H0N" using 2:xticlabels(1) ****** | + 220 ++ * * ****** + | * * * * + 200 ++ * * * +* + | * * * * + 180 ++ ****** * * * +* + | * * * * * * + 160 ****** * * * * * +* + * * * * * * * * + * ******* * * * * ****** * * + 140 *+ ** * * * * * * ******** +* + * ** ******* * * * * * ** ** * + 120 *+ ** ** ******* * * * * ** ** +* + * ** ** ** * * * ******* ** ** * + 100 *+ ** ** ** * * * * ** ** ** +* + * + ** + ** + ** + * + * + + * + * + ** + ** + ** + * + 80 ********************************************************************** + Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + Month +@end verbatim + +Please refer to the @t{mu-script} man-page for some details on writing your +own scripts. + + +@node Recipes +@appendix Recipes + +@itemize +@item Calculating the average length of subject-lines +@lisp +;; the average length of all our +(let ((len 0) (n 0)) + (mu:for-each-message + (lambda (msg) + (set! len (+ len (string-length (or (mu:subject msg) "")))) + (set! n (+ n 1)))) + (if (= n 0) 0 (/ len n))) + ;; this gives a rational, exact result; + ;; use exact->inexact to get decimals + +;; we we can make this short with the mu:average (with (mu stats)) +(mu:average (lambda (msg) (string-length (or (mu:subject msg) "")))) + + +@end lisp +@end itemize + +@node GNU Free Documentation License +@appendix GNU Free Documentation License + +@include fdl.texi +@bye diff --git a/guile/mu.scm b/guile/mu.scm new file mode 100644 index 0000000..08eae1f --- /dev/null +++ b/guile/mu.scm @@ -0,0 +1,318 @@ +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu) + :use-module (oop goops) + :use-module (ice-9 optargs) + :use-module (texinfo string-utils) + :export + ( ;; classes + <mu:message> + <mu:contact> + <mu:part> + ;; general +;; mu:initialize + ;; mu:initialized? + mu:log-warning + mu:log-message + mu:log-critical + ;; search funcs + mu:for-each-message + mu:for-each-msg + mu:message-list + ;; message funcs + mu:header + ;; message accessors + mu:field:bcc + mu:field:body-html + mu:field:body-txt + mu:field:cc + mu:field:date + mu:field:flags + mu:field:from + mu:field:maildir + mu:field:message-id + mu:field:path + mu:field:prio + mu:field:refs + mu:field:size + mu:field:subject + mu:field:tags + mu:field:timestamp + mu:field:to + ;; contact funcs + mu:name + mu:email + mu:contact->string + ;; + mu:for-each-contact + ;; + mu:contacts + ;; + ;; <mu:contact-with-stats> + mu:frequency + mu:last-seen + ;; parts + + <mu:part> + ;; message function + mu:attachments + mu:parts + ;; <mu:part> methods + mu:name + mu:mime-type + ;; size + ;; mu:save + ;; mu:save-as + )) + +;; this is needed for guile < 2.0.4 +(setlocale LC_ALL "") + +;; load the binary +(load-extension "libguile-mu" "mu_guile_init") +(load-extension "libguile-mu" "mu_guile_message_init") + +;; define some dummies so we don't get errors during byte compilation +(eval-when (compile) + (define mu:c:get-field) + (define mu:c:get-contacts) + (define mu:c:for-each-message) + (define mu:c:get-header) + (define mu:critical) + (define mu:c:log) + (define mu:message) + (define mu:c:log) + (define mu:warning) + (define mu:c:log) + (define mu:c:get-parts)) + +(define (mu:log-warning frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:warning frm args)) + +(define (mu:log-message frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:message frm args)) + +(define (mu:log-critical frm . args) + "Log FRM with ARGS at warning." + (mu:c:log mu:critical frm args)) + +(define-class <mu:message> () + (msg #:init-keyword #:msg)) ;; the MuMsg-smob we're wrapping + +(define-syntax define-getter + (syntax-rules () + ((define-getter method-name field) + (begin + (define-method (method-name (msg <mu:message>)) + (mu:c:get-field (slot-ref msg 'msg) field)) + (export method-name))))) + +(define-getter mu:bcc mu:field:bcc) +(define-getter mu:body-html mu:field:body-html) +(define-getter mu:body-txt mu:field:body-txt) +(define-getter mu:cc mu:field:cc) +(define-getter mu:date mu:field:date) +(define-getter mu:flags mu:field:flags) +(define-getter mu:from mu:field:from) +(define-getter mu:maildir mu:field:maildir) +(define-getter mu:message-id mu:field:message-id) +(define-getter mu:path mu:field:path) +(define-getter mu:priority mu:field:prio) +(define-getter mu:references mu:field:refs) +(define-getter mu:size mu:field:size) +(define-getter mu:subject mu:field:subject) +(define-getter mu:tags mu:field:tags) +(define-getter mu:timestamp mu:field:timestamp) +(define-getter mu:to mu:field:to) + +(define-method (mu:header (msg <mu:message>) (hdr <string>)) + "Get an arbitrary header HDR from message MSG; return #f if it does +not exist." + (mu:c:get-header (slot-ref msg 'msg) hdr)) + +(define* (mu:for-each-message func #:optional (expr #t) (maxresults -1)) + "Execute function FUNC for each message that matches mu search expression EXPR. +If EXPR is not provided, match /all/ messages in the store. MAXRESULTS +specifies the maximum of messages to return, or -1 (the default) for +no limit." + (mu:c:for-each-message + (lambda (msg) + (func (make <mu:message> #:msg msg))) + expr + maxresults)) + +;; backward-compatibility alias +(define mu:for-each-msg mu:for-each-message) + +(define* (mu:message-list #:optional (expr #t) (maxresults -1)) + "Return a list of all messages matching mu search expression +EXPR. If EXPR is not provided, return a list of /all/ messages in the +store. MAXRESULTS specifies the maximum of messages to return, or +-1 (the default) for no limit." + (let ((lst '())) + (mu:for-each-message + (lambda (m) + (set! lst (append! lst (list m)))) expr maxresults) + lst)) + +;; contacts +(define-class <mu:contact> () + (name #:init-value #f #:accessor mu:name #:init-keyword #:name) + (email #:init-value #f #:accessor mu:email #:init-keyword #:email)) + +(define-method (mu:contacts (msg <mu:message>) contact-type) + "Get all contacts for MSG of the given CONTACT-TYPE. MSG is of type <mu-message>, +while contact type is either `mu:contact:to', `mu:contact:cc', +`mu:contact:from' or `mu:contact:bcc' to get the corresponding type of +contacts, or #t to get all. + +Returns a list of <mu-contact> objects." + (map (lambda (pair) ;; a pair (na . addr) + (make <mu:contact> #:name (car pair) #:email (cdr pair))) + (mu:c:get-contacts (slot-ref msg 'msg) contact-type))) + +(define-method (mu:contacts (msg <mu:message>)) + "Get contacts of all types for message MSG as a list of <mu-contact> +objects." + (mu:contacts msg #t)) + +(define-class <mu:contact-with-stats> (<mu:contact>) + (tstamp #:init-value 0 #:accessor mu:timestamp #:init-keyword #:timestamp) + (last-seen #:init-value 0 #:accessor mu:last-seen) + (freq #:init-value 1 #:accessor mu:frequency)) + +(define* (mu:for-each-contact proc #:optional (expr #t)) + "Execute PROC for each contact. PROC receives a <mu-contact> instance +as parameter. If EXPR is specified, only consider contacts in messages +matching EXPR." + (let ((c-hash (make-hash-table 4096))) + (mu:for-each-message + (lambda (msg) + (for-each + (lambda (ct) + (let ((ct-ws (make <mu:contact-with-stats> + #:name (mu:name ct) + #:email (mu:email ct) + #:timestamp (mu:date msg)))) + (update-contacts-hash c-hash ct-ws))) + (mu:contacts msg #t))) + expr) + (hash-for-each ;; c-hash now contains a map of email->contact + (lambda (email ct-ws) (proc ct-ws)) c-hash))) + +(define-method (update-contacts-hash c-hash (nc <mu:contact-with-stats>)) + "Update the contacts hash with a new and/or existing contact." + ;; xc: existing-contact, nc: new contact + (let ((xc (hash-ref c-hash (mu:email nc)))) + (if (not xc) ;; no existing contact with this email address? + (hash-set! c-hash (mu:email nc) nc) ;; store the new contact. + ;; otherwise: + (begin + ;; 1) update the frequency for the existing contact + (set! (mu:frequency xc) (1+ (mu:frequency xc))) + ;; 2) update the name if the new one is not empty and its timestamp is newer + ;; in that case, also update the timestamp + (if (and (mu:name nc) (> (string-length (mu:name nc))) + (> (mu:timestamp nc) (mu:timestamp xc))) + (set! (mu:name xc) (mu:name nc)) + (set! (mu:timestamp xc) (mu:timestamp nc))) + ;; 3) update last-seen with timestamp, if x's timestamp is newer + (if (> (mu:timestamp nc) (mu:last-seen xc)) + (set! (mu:last-seen xc) (mu:timestamp nc))) + ;; okay --> now xc has been updated; but it back in the hash + (hash-set! c-hash (mu:email xc) xc))))) + +(define-method (mu:contact->string (contact <mu:contact>) (form <string>)) + "Convert a contact to a string in format FORM, which is a string, +either \"org-contact\", \"mutt-alias\", \"mutt-ab\", +\"wanderlust\", \"quoted\" \"plain\"." + (let* ((name (mu:name contact)) (email (mu:email contact)) + (nick ;; simplistic nick guessing... + (string-map + (lambda(kar) + (if (char-alphabetic? kar) kar #\_)) + (string-downcase (or name email))))) + (cond + ((string= form "plain") + (format #f "~a~a~a" (or name "") (if name " " "") email)) + ((string= form "org-contact") + (format #f "* ~s\n:PROPERTIES:\n:EMAIL:~a\n:NICK:~a\n:END:" + (or name email) email nick)) + ((string= form "wanderlust") + (format #f "~a ~s ~s" + nick (or name email) email)) + ((string= form "mutt-alias") + (format #f "alias ~a ~a <~a>" + nick (or name email) email)) + ((string= form "mutt-ab") + (format #f "~a\t~a\t" + email (or name ""))) + ((string= form "quoted") + (string-append + "\"" + (escape-special-chars + (string-append + (if name + (format #f "\"~a\" " name) + "") + (format #f "<~a>" email)) + "\"" #\\) + "\"")) + (else (error "Unsupported format"))))) + +;; message parts + + +(define-class <mu:part> () + (msgpath #:init-value #f #:init-keyword #:msgpath) + (index #:init-value #f #:init-keyword #:index) + (name #:init-value #f #:getter mu:name #:init-keyword #:name) + (mime-type #:init-value #f #:getter mu:mime-type #:init-keyword #:mime-type) + (size #:init-value 0 #:getter mu:size #:init-keyword #:size)) + +(define-method (get-parts (msg <mu:message>) (files-only <boolean>)) + "Get the part for MSG as a list of <mu:part> objects; if FILES-ONLY is #t, +only get the part with file names." + (map (lambda (part) + (make <mu:part> + #:msgpath (list-ref part 0) + #:index (list-ref part 1) + #:name (list-ref part 2) + #:mime-type (list-ref part 3) + #:size (list-ref part 4))) + (mu:c:get-parts (slot-ref msg 'msg) files-only))) + +(define-method (mu:attachments (msg <mu:message>)) + "Get the attachments for MSG as a list of <mu:part> objects." + (get-parts msg #t)) + +(define-method (mu:parts (msg <mu:message>)) + "Get the MIME-parts for MSG as a list of <mu-part> objects." + (get-parts msg #f)) + +;; (define-method (mu:save (part <mu:part>)) +;; "Save PART to a temporary file, and return the file name. If the +;; part had a filename, the temporary file's file name will be just that; +;; otherwise a name is made up." +;; (mu:save-part (slot-ref part 'msgpath) (slot-ref part 'index))) + +;; (define-method (mu:save-as (part <mu:part>) (filepath <string>)) +;; "Save message-part PART to file system path PATH." +;; (copy-file (save part) filepath)) diff --git a/guile/mu/Makefile.am b/guile/mu/Makefile.am new file mode 100644 index 0000000..f531822 --- /dev/null +++ b/guile/mu/Makefile.am @@ -0,0 +1,28 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +# FIXME: GUILE_SITEDIR would be better, but that +# breaks 'make distcheck' +scmdir=${prefix}/share/guile/site/2.2/mu/ + +scm_DATA= \ + stats.scm \ + plot.scm \ + script.scm + +EXTRA_DIST=$(scm_DATA) diff --git a/guile/mu/README b/guile/mu/README new file mode 100644 index 0000000..634ad8b --- /dev/null +++ b/guile/mu/README @@ -0,0 +1,207 @@ +* OUTDATED * + +* README + +** What is muile? + + `muile' is a little experiment/toy using the equally experimental mu guile + bindings, to be found in libmuguile/ in the top-level source directory. + + `guile'[1] is an interpreter/library for the Scheme programming language[2], + specifically meant for extending other programs. It is, in fact, the + official GNU language for doing so. 'muile' requires guile 2.x to get the full + support. + + Older versions may not support e.g. the 'mu-stats.scm' things discussed below. + + The combination of mu + guile is called `muile', and allows you to write + little Scheme-programs to query the mu-database, or inspect individual + messages. It is still in an experimental stage, but useful already. + +** How do I get it? + + The git-version and the future 0.9.7 version of mu will automatically build + muile if you have guile. I've been using guile 2.x from git, but installing + the 'guile-1.8-dev' package (Ubuntu/Debian) should do the trick. (I only did + very minimal testing with guile 1.8 though). + + Then, configure mu. The configure output should tell you about whether guile + was found (and where). If it's found, build mu, and toys/muile should be + created, as well. + +** What can I do with it? + + Go to toys/muile and start muile. You'll end up with a guile-shell where you + can type scheme [1], it looks something like this (for guile 2.x): + + ,---- + | scheme@(guile-user)> + `---- + + Now, let's load a message (of course, replace with a message on your system): + + ,---- + | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S")) + `---- + + This defines a variable 'msg', which holds some message on your file + system. It's now easy to inspect this message: + + ,---- + | scheme@(guile-user)> (define msg (mu:msg:make-from-file "/home/djcb/Maildir/cur/12131e7b20a2:2,S")) + `---- + + Now, we can inspect this message a bit: + ,---- + | scheme@(guile-user)> (mu:msg:subject msg) + | $1 = "See me in bikini :-)" + | scheme@(guile-user)> (mu:msg:flags msg) + | $2 = (mu:attach mu:unread) + `---- + + and so on. Note, it's probably easiest to explore the various mu: methods + using autocompletion; to enable that make sure you have + + + ,---- + | (use-modules (ice-9 readline)) + | (activate-readline) + `---- + + in your ~/.guile configuration. + +** does this tool have some parameters? + + Yes, there is --muhome to set a non-default place for the message database + (see the documentation on --muhome in the mu-find manpage). + + And there is --msg=<path> where you specify some particular message file; + it will be available as 'mu:current-msg' in the guile (muile) environment. For + example: + + ,---- + | ./muile --msg=~/Maildir/inbox/cur/1311310172_1234:2,S + | [...] + | scheme@(guile-user)> mu:current-msg + | $1 = #<msg /home/djcb/Maildir/inbox/cur/1311310172_1234:2,S> + | scheme@(guile-user)> (mu:msg:size mu:current-msg) + | $2 = 7206 + `---- + +** What about searching messages in the database? + + That's easy, too - it does require a little more scheme knowledge. For + searching messages there is the mu:store:for-each function, which takes two + arguments; the first argument is a function that will be called for each + message found. The optional second argument is the search expression (following + 'mu find' syntax); if don't provide the argument, all messages match. + + So how does this work in practice? Let's see I want to see the subject and + sender for messages about milk: + + ,---- + | (mu:store:for-each (lambda(msg) (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) "milk") + `---- + + or slightly more readable: + + ,---- + | (mu:store:for-each + | (lambda(msg) + | (format #t "~s ~s\n" (mu:msg:from msg) (mu:msg:subject msg))) + | "milk") + `---- + + As you can see, I provide an anonymous ('lambda') function which will be + called for each message matching 'milk'. Admittedly, this requires a bit of + Scheme-knowledge... but this time is good as any to learn this nice + language. + + +** Can I do some statistics on my messages? + + Yes you can. In fact, it's pretty easy. If you load (in the muile/ directory) + the file 'mu-stats.scm': + + ,---- + | (load "mu-stats.scm") + `---- + + you'll get a bunch of functions (with names starting with 'mu:stats') to make + this very easy. Let's see, suppose I want to see how many messages I get per + weekday: + + ,---- + | scheme@(guile-user)> (mu:stats:per-weekday) + | $1 = ((0 . 2255) (1 . 2788) (2 . 2868) (3 . 2599) (4 . 2629) (5 . 2287) (6 . 1851)) + `---- + + Note, Sunday=0, Monday=1 and so on. Apparently, I get/send most of e-mail on + Tuesdays, and least on Saturday. + + And note that mu:stats:per-weekdays takes an optional search expression + argument, to limit the results to messages matching that, e.g., to only + consider messages related to emacs during this year: + + ,---- + | scheme@(guile-user)> (mu:stats:per-weekday "emacs date:2011..now") + | $8 = ((0 . 54) (1 . 22) (2 . 46) (3 . 47) (4 . 39) (5 . 54) (6 . 50)) + `---- + + There's also 'mu:stats:per-month', 'mu:stats:per-year', 'mu:stats:per-hour'. + I learnt that during 3-4am I sent/receive only about a third of what I sent + during 11-12pm. + +** What about getting the top-10 people in the To:-field? + + Easy. + + ,---- + | scheme@(guile-user)> (mu:stats:top-n-to) + | $1 = ((("Abc" "myself@example.com") . 4465) (("Def" "somebodyelse@example.com") . 2114) + | (and so on) + `---- + + I've changed the names a bit to protect the innocent, but what the function + does is return a list of pairs of + + (<name> <email>) . <frequency> + + descending in order of frequency. Note, 'mu:stats:top-n-to' takes two + optional arguments - first the 'n' in top-n (default is 10), and seconds as + search expression to limit the messages considered. + + There are also the functions 'mu:stats:top-n-subject' and + 'mu:stats:top-n-from' which do the same, mutatis mutandis, and it's quite + easy to add your own (see the mu-stats.scm for examples) + +** What about showing the results in a table? + + Even easier. Try: + + ,---- + | (mu:stats:table (mu:stats:top-n-to)) + `---- + + or + + ,---- + | (mu:stats:table (mu:stats:per-weekday)) + `---- + + You can also export the table: + + ,---- + | (mu:stats:export (mu:stats:per-weekday)) + `---- + + which will create a temporary file with the results, for further processing + in e.g. 'R' or 'gnuplot'. + + +[1] http://www.gnu.org/s/guile/ +[2] http://en.wikipedia.org/wiki/Scheme_(programming_language) + +# Local Variables: +# mode: org; org-startup-folded: nil +# End: diff --git a/guile/mu/contact.scm b/guile/mu/contact.scm new file mode 100644 index 0000000..843d9c4 --- /dev/null +++ b/guile/mu/contact.scm @@ -0,0 +1,4 @@ +(define-module (mu contact) :use-module(mu)) +(display "(mu contact) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/message.scm b/guile/mu/message.scm new file mode 100644 index 0000000..bc9b27a --- /dev/null +++ b/guile/mu/message.scm @@ -0,0 +1,4 @@ +(define-module (mu message) :use-module (mu)) +(display "(mu message) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/part.scm b/guile/mu/part.scm new file mode 100644 index 0000000..f9b9cd3 --- /dev/null +++ b/guile/mu/part.scm @@ -0,0 +1,4 @@ +(define-module (mu part) :use-module (mu)) +(display "(mu part) is deprecated, please remove from (use-modules ...)") +(newline) + diff --git a/guile/mu/plot.scm b/guile/mu/plot.scm new file mode 100644 index 0000000..adeb80f --- /dev/null +++ b/guile/mu/plot.scm @@ -0,0 +1,80 @@ +;; +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu plot) + :use-module (mu) + :use-module (ice-9 popen) + :export ( mu:plot ;; alias for mu:plot-histogram + mu:plot-histogram + )) + +(define (export-pairs pairs) + "Write a temporary file with the list of PAIRS in table format, and +return the file name." + (let* ((datafile (tmpnam)) + (output (open datafile (logior O_CREAT O_WRONLY) #O0600))) + (for-each + (lambda(pair) + (display (format #f "~a ~a\n" (car pair) (cdr pair)) output)) + pairs) + (close output) + datafile)) + +(define (find-program-in-path prog) + "Find exutable program PROG in PATH; return the full path, or #f if +not found." + (let* ((path (parse-path (getenv "PATH"))) + (progpath (search-path path prog))) + (if (not progpath) + #f + (if (access? progpath X_OK) ;; is + progpath + #f)))) + +(define* (mu:plot-histogram data title x-label y-label + #:optional (output "dumb") (extra-gnuplot-opts '())) + "Plot DATA with TITLE, X-LABEL and X-LABEL using the gnuplot +program. DATA is a list of cons-pairs (X . Y). + + OUTPUT is a string +that determines the type of output that gnuplot produces, depending on +the system. Which options are available depends on the particulars for +the gnuplot installation, but typical examples would be \"dumb\" for +text-only display, \"wxterm\" to write to a graphical window, or +\"png\" to write a PNG-image to stdout. + +EXTRA-GNUPLOT-OPTS is a list +of any additional options for gnuplot." + (if (not (find-program-in-path "gnuplot")) + (error "cannot find 'gnuplot' in path")) + (let ((datafile (export-pairs data)) + (gnuplot (open-pipe "gnuplot -p" OPEN_WRITE))) + (display (string-append + "reset\n" + "set term " (or output "dumb") "\n" + "set title \"" title "\"\n" + "set xlabel \"" x-label "\"\n" + "set ylabel \"" y-label "\"\n" + "set boxwidth 0.9\n" + (string-join extra-gnuplot-opts "\n") + "plot \"" datafile "\" using 2:xticlabels(1) with boxes fs solid\n") + gnuplot) + (close-pipe gnuplot))) + +;; backward compatibility +(define mu:plot mu:plot-histogram) diff --git a/guile/mu/script.scm b/guile/mu/script.scm new file mode 100644 index 0000000..3a62948 --- /dev/null +++ b/guile/mu/script.scm @@ -0,0 +1,57 @@ +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(define-module (mu script) + :export (mu:run-stats)) + +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats) (mu plot)) + +(define (help-and-exit) + "Show some help." + (display + (string-append "usage: script [--help] [--textonly] " + "[--muhome=<muhome>] [--query=<query>") + (newline)) + (exit 0)) + +(define (mu:run-stats args func) + "Run some statistics function. +Interpret argument-list ARGS (like command-line +arguments). Possible arguments are: + --help (show some help and exit) + --muhome (path to alternative mu home directory) + --output (a string describing the output, e.g. \"dumb\", \"png\" \"wxt\") + searchexpr (a search query) +then call FUNC with args SEARCHEXPR and OUTPUT." + (setlocale LC_ALL "") + (let* ((optionspec '((muhome (value #t)) + (query (value #t)) + (output (value #f)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (query (option-ref options 'query #f)) + (help (option-ref options 'help #f)) + (output (option-ref options 'output #f)) + (muhome (option-ref options 'muhome #f)) + (restargs (option-ref options '() #f))) + (if help (help-and-exit)) + (mu:initialize muhome) + (func (or query "") output))) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/mu/stats.scm b/guile/mu/stats.scm new file mode 100644 index 0000000..90ab836 --- /dev/null +++ b/guile/mu/stats.scm @@ -0,0 +1,165 @@ +;; +;; Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +(define-module (mu stats) + :use-module (oop goops) + :use-module (mu) + :use-module (srfi srfi-1) + :use-module (ice-9 i18n) + :use-module (ice-9 r5rs) + :export ( mu:tabulate + mu:top-n-most-frequent + mu:count + mu:average + mu:stddev + mu:correl + mu:max + mu:min + mu:weekday-numbers->names + mu:month-numbers->names)) + + +(define* (mu:tabulate func #:optional (expr #t)) + "Execute FUNC for each message matching EXPR, and return an alist +with maps each result of FUNC to its frequency. If the result of FUNC +is a list, add each of its values separately. + FUNC is a function takes a <mu-message> instance as its argument. For +example, to tabulate messages by weekday, one could use: + (mu:tabulate (lambda(msg) (tm:wday (localtime (date msg))))), and +get back a list like + ((1 . 2) (2 . 5)(3 . 4)(4 . 4)(5 . 12)(6 . 7)(7. 2))." + (let* ((table '()) + ;; func to add a value to our table + (update-table + (lambda (val) + (let ((old-freq (or (assoc-ref table val) 0))) + (set! table (assoc-set! table val (1+ old-freq))))))) + (mu:for-each-message + (lambda(msg) + (let ((val (func msg))) + (if (list? val) + (for-each update-table val) + (update-table val)))) + expr) + table)) + +(define* (top-n func less n #:optional (expr #t)) + "Take the results of (mu:tabulate FUNC EXPR), sort using LESS (a +function taking two arguments A and B (cons cells, (VAL . FREQ)), and +returns #t if A < B, #f otherwise), and then take the first N." + (take (sort (mu:tabulate func expr) less) n)) + +(define* (mu:top-n-most-frequent func n #:optional (expr #t)) + "Take the results of (mu:tabulate FUNC EXPR), and return the N items with the highest frequency." + (top-n func (lambda (a b) (> (cdr a) (cdr b))) n expr)) + +(define* (mu:count #:optional (expr #t)) + "Count the number of messages matching EXPR. If EXPR is not +provided, match /all/ messages." + (let ((num 0)) + (mu:for-each-message + (lambda (msg) (set! num (1+ num))) + expr) + num)) + +(define (average lst) + "Calculate the average of a list LST of numbers, or #f if undefined." + (if (null? lst) + #f + (/ (apply + lst) (length lst)))) + +(define (stddev lst) + "Calculate the standard deviation of a list LST of numbers or #f if +undefined." + (let* ((avg (average lst)) + (sosq (if avg + (apply + (map (lambda (x)(* (- x avg) (- x avg))) lst))))) + (if sosq + (sqrt (/ sosq (length lst)))))) + + +(define* (mu:average func #:optional (expr #t)) + "Get the average value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (average (map func (mu:message-list expr)))) + +(define* (mu:stddev func #:optional (expr #t)) + "Get the standard deviation the the values of FUNC applied to all +messages matching EXPR (or #t for all). This is the 'population' stddev, not the 'sample' stddev. Returns #f if undefined." + (stddev (map func (mu:message-list expr)))) + +(define* (mu:max func #:optional (expr #t)) + "Get the maximum value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (apply max (map func (mu:message-list expr)))) + +(define* (mu:min func #:optional (expr #t)) + "Get the minimum value of FUNC applied to all messages matching +EXPR (or #t for all). Returns #f if undefined." + (apply min (map func (mu:message-list expr)))) + + +(define (correl lst) + "Calculate Pearson's correlation coefficient for a list LST of cons +pair, where the car and cdr of the pairs are values from data set 1 +and 2, respectively." + (let ((n (length lst)) + (sx (apply + (map car lst))) + (sy (apply + (map cdr lst))) + (sxy (apply + (map (lambda (cell) (* (car cell) (cdr cell))) lst))) + (sxx (apply + (map (lambda (cell) (* (car cell) (car cell))) lst))) + (syy (apply + (map (lambda (cell) (* (cdr cell) (cdr cell))) lst)))) + (/ (- (* n sxy) (* sx sy)) + (sqrt (* (- (* n sxx) (* sx sx)) (- (* n syy) (* sy sy))))))) + +(define* (mu:correl func1 func2 #:optional (expr #t)) + "Determine Pearson's correlation coefficient between the value for +functions FUNC1 and FUNC2 to all messages matching EXPR (or #t for +all). Returns #f if undefined." + (let ((data + (map (lambda (msg) + (cons (func1 msg) (func2 msg))) + (mu:message-list expr)))) + (if data (correl data) #f))) + + +;; a list of abbreviated, localized day names +(define day-names + (map locale-day-short (iota 7 1))) + +(define (mu:weekday-numbers->names table) + "Convert a list of pairs with the car denoting a day number (0-6) +into a list of pairs with the car replaced by the corresponding day +name (abbreviated) for the current locale." + (map + (lambda (pair) + (cons (list-ref day-names (car pair)) (cdr pair))) + table)) + +;; a list of abbreviated, localized month names +(define month-names + (map locale-month-short (iota 12 1))) + +(define (mu:month-numbers->names table) + "Convert a list of pairs with the car denoting a month number (0-11) +into a list of pairs with the car replaced by the corresponding day +name (abbreviated)." + (map + (lambda (pair) + (cons (list-ref month-names (car pair)) (cdr pair))) + table)) diff --git a/guile/scripts/Makefile.am b/guile/scripts/Makefile.am new file mode 100644 index 0000000..c846596 --- /dev/null +++ b/guile/scripts/Makefile.am @@ -0,0 +1,29 @@ +## Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + msgs-count.scm \ + msgs-per-year.scm \ + msgs-per-hour.scm \ + msgs-per-month.scm \ + msgs-per-day.scm \ + msgs-per-year-month.scm \ + find-dups.scm + +muguiledistscriptdir = $(pkgdatadir)/scripts/ +muguiledistscript_SCRIPTS = $(EXTRA_DIST) diff --git a/guile/scripts/find-dups.scm b/guile/scripts/find-dups.scm new file mode 100755 index 0000000..778acfe --- /dev/null +++ b/guile/scripts/find-dups.scm @@ -0,0 +1,119 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2013-2015 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: find duplicate messages +;; INFO: options: +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --delete: delete all but the first one (experimental, be careful!) + +(use-modules (mu) (mu script) (mu stats)) +(use-modules (ice-9 getopt-long) (ice-9 optargs) + (ice-9 popen) (ice-9 format) (ice-9 rdelim)) + +(define (md5sum path) + (let* ((port (open-pipe* OPEN_READ "md5sum" path)) + (md5 (read-delimited " " port))) + (close-pipe port) + md5)) + +(define (find-dups delete expr) + (let ((id-table (make-hash-table 20000))) + ;; fill the hash with <msgid-size> => <list of paths> + (mu:for-each-message + (lambda (msg) + (let* ((id (format #f "~a-~d" (mu:message-id msg) + (mu:size msg))) + (lst (hash-ref id-table id))) + (if lst + (set! lst (cons (mu:path msg) lst)) + (set! lst (list (mu:path msg)))) + (hash-set! id-table id lst))) + expr) + ;; list all the paths with multiple elements; check the md5sum to + ;; make 100%-minus-ε sure they are really the same file. + (hash-for-each + (lambda (id paths) + (if (> (length paths) 1) + (let ((hash (make-hash-table 10))) + (for-each + (lambda (path) + (when (file-exists? path) + (let* ((md5 (md5sum path)) (lst (hash-ref hash md5))) + (if lst + (set! lst (cons path lst)) + (set! lst (list path))) + (hash-set! hash md5 lst)))) + paths) + ;; hash now maps the md5sum to the messages... + (hash-for-each + (lambda (md5 mpaths) + (if (> (length mpaths) 1) + (begin + ;;(format #t "md5sum: ~a:\n" md5) + (let ((num 1)) + (for-each + (lambda (path) + (if (equal? num 1) + (format #t "~a\n" path) + (begin + (format #t "~a: ~a\n" (if delete "deleting" "dup") path) + (if delete (delete-file path)))) + (set! num (+ 1 num))) + mpaths))))) + hash)))) + id-table))) + + + +(define (main args) + "Find duplicate messages and, potentially, delete the dups. + Be careful with that! +Interpret argument-list ARGS (like command-line +arguments). Possible arguments are: + --muhome (path to alternative mu home directory). + --delete (delete all but the first one). Run mu index afterwards. + --expr (expression to constrain search)." + (setlocale LC_ALL "") + (let* ((optionspec '( (muhome (value #t)) + (delete (value #f)) + (expr (value #t)) + (help (single-char #\h) (value #f)))) + (options (getopt-long args optionspec)) + (help (option-ref options 'help #f)) + (delete (option-ref options 'delete #f)) + (expr (option-ref options 'expr #t)) + (muhome (option-ref options 'muhome #f))) + (mu:initialize muhome) + (find-dups delete expr))) + + +;; Local Variables: +;; mode: scheme +;; End: + + + + + + + + + diff --git a/guile/scripts/msgs-count.scm b/guile/scripts/msgs-count.scm new file mode 100755 index 0000000..923e3a5 --- /dev/null +++ b/guile/scripts/msgs-count.scm @@ -0,0 +1,40 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir + +(use-modules (mu) (mu script) (mu stats)) + +(define (count expr output) + "Print the total number of messages matching the query EXPR. +OUTPUT is ignored." + (display (mu:count expr)) + (newline)) + +(define (main args) + (mu:run-stats args count)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-day.scm b/guile/scripts/msgs-per-day.scm new file mode 100755 index 0000000..824f556 --- /dev/null +++ b/guile/scripts/msgs-per-day.scm @@ -0,0 +1,49 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-day expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (mu:weekday-numbers->names + (sort (mu:tabulate + (lambda (msg) + (tm:wday (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per weekday matching ~a" expr) + "Day" "Messages" output)) + +(define (main args) + (mu:run-stats args per-day)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-hour.scm b/guile/scripts/msgs-per-hour.scm new file mode 100755 index 0000000..d96f9b6 --- /dev/null +++ b/guile/scripts/msgs-per-hour.scm @@ -0,0 +1,49 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-hour expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort + (mu:tabulate + (lambda (msg) + (tm:hour (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per hour matching ~a" expr) + "Hour" "Messages" output)) + +(define (main args) + (mu:run-stats args per-hour)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-month.scm b/guile/scripts/msgs-per-month.scm new file mode 100755 index 0000000..50dbbed --- /dev/null +++ b/guile/scripts/msgs-per-month.scm @@ -0,0 +1,50 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-month expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (mu:month-numbers->names + (sort + (mu:tabulate + (lambda (msg) + (tm:mon (localtime (mu:date msg)))) expr) + (lambda (x y) (< (car x) (car y))))) + (format #f "Messages per month matching ~a" expr) + "Month" "Messages" output)) + +(define (main args) + (mu:run-stats args per-month)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-year-month.scm b/guile/scripts/msgs-per-year-month.scm new file mode 100755 index 0000000..33b1447 --- /dev/null +++ b/guile/scripts/msgs-per-year-month.scm @@ -0,0 +1,52 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-year-month expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort (mu:tabulate + (lambda (msg) + (string->number + (format #f "~d~2'0d" + (+ 1900 (tm:year (localtime (mu:date msg)))) + (tm:mon (localtime (mu:date msg)))))) + expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year/month matching ~a" expr) + "Year/Month" "Messages" output)) + +(define (main args) + (mu:run-stats args per-year-month)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/scripts/msgs-per-year.scm b/guile/scripts/msgs-per-year.scm new file mode 100755 index 0000000..242e299 --- /dev/null +++ b/guile/scripts/msgs-per-year.scm @@ -0,0 +1,48 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# +;; +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +;; INFO: graph the number of messages per day (using gnuplot) +;; INFO: options: +;; INFO: --query=<query>: limit to messages matching query +;; INFO: --muhome=<muhome>: path to mu home dir +;; INFO: --output: the output format, such as "png", "wxt" +;; INFO: (depending on the environment) + +(use-modules (mu) (mu script) (mu stats) (mu plot)) + +(define (per-year expr output) + "Count the total number of messages for each weekday (0-6 for +Sun..Sat) that match EXPR. OUTPUT corresponds to the output format, as +per gnuplot's 'set terminal'." + (mu:plot-histogram + (sort (mu:tabulate + (lambda (msg) + (+ 1900 (tm:year (localtime (mu:date msg))))) expr) + (lambda (x y) (< (car x) (car y)))) + (format #f "Messages per year matching ~a" expr) + "Year" "Messages" output)) + +(define (main args) + (mu:run-stats args per-year)) + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/tests/Makefile.am b/guile/tests/Makefile.am new file mode 100644 index 0000000..8df87fb --- /dev/null +++ b/guile/tests/Makefile.am @@ -0,0 +1,52 @@ +# Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +## +## This program is free software; you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by the +## Free Software Foundation; either version 3, or (at your option) any +## later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS=$(XAPIAN_CXXFLAGS) \ + $(GMIME_CFLAGS) \ + $(GLIB_CFLAGS) \ + -I${top_srcdir} \ + -I${top_srcdir}/lib \ + -DMU_TESTMAILDIR=\"${top_srcdir}/lib/testdir\" \ + -DMU_TESTMAILDIR2=\"${top_srcdir}/lib/testdir2\" \ + -DMU_TESTMAILDIR3=\"${top_srcdir}/lib/testdir3\" \ + -DMU_PROGRAM=\"${abs_top_builddir}/mu/mu\" \ + -DMU_GUILE_MODULE_PATH=\"${abs_top_srcdir}/guile/\" \ + -DMU_GUILE_LIBRARY_PATH=\"${abs_top_builddir}/guile/.libs\" \ + -DABS_CURDIR=\"${abs_builddir}\" \ + -DABS_SRCDIR=\"${abs_srcdir}\" + +# don't use -Werror, as it might break on other compilers +# use -Wno-unused-parameters, because some callbacks may not +# really need all the params they get +AM_CFLAGS=$(ASAN_CFLAGS) ${WARN_CFLAGS} +AM_CXXFLAGS=$(ASAN_CXXFLAGS) ${WARN_CXXFLAGS} +AM_LDFLAGS=$(ASAN_LDFLAGS) + +noinst_PROGRAMS= $(TEST_PROGS) + +TEST_PROGS += test-mu-guile +test_mu_guile_SOURCES= test-mu-guile.c dummy.cc +test_mu_guile_LDADD=${top_builddir}/lib/libtestmucommon.la + +# we need to use dummy.cc to enforce c++ linking... +BUILT_SOURCES= \ + dummy.cc +dummy.cc: + touch dummy.cc + +EXTRA_DIST=test-mu-guile.scm diff --git a/guile/tests/test-mu-guile.c b/guile/tests/test-mu-guile.c new file mode 100644 index 0000000..50199eb --- /dev/null +++ b/guile/tests/test-mu-guile.c @@ -0,0 +1,134 @@ +/* +** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <glib/gstdio.h> + +#include <lib/mu-query.h> +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +#include "test-mu-common.h" +#include <lib/mu-store.hh> + + +/* tests for the command line interface, uses testdir2 */ + +static gchar* +fill_database (void) +{ + gchar *cmdline, *tmpdir; + GError *err; + + tmpdir = test_mu_common_get_random_tmpdir(); + cmdline = g_strdup_printf ( + "/bin/sh -c '" + "%s init --muhome=%s --maildir=%s --quiet; " + "%s index --muhome=%s --quiet'", + MU_PROGRAM, tmpdir, MU_TESTMAILDIR2, + MU_PROGRAM, tmpdir); + + if (g_test_verbose()) + g_print ("%s\n", cmdline); + + err = NULL; + if (!g_spawn_command_line_sync (cmdline, NULL, NULL, + NULL, &err)) { + g_printerr ("Error: %s\n", err ? err->message : "?"); + g_assert (0); + } + + g_free (cmdline); + return tmpdir; +} + + +static void +test_something (const char *what) +{ + char *dir, *cmdline; + gint result; + + dir = fill_database (); + cmdline = g_strdup_printf ( + "LD_LIBRARY_PATH=%s %s -q -L %s -e main %s/test-mu-guile.scm " + "--muhome=%s --test=%s", + MU_GUILE_LIBRARY_PATH, + GUILE_BINARY, + MU_GUILE_MODULE_PATH, + ABS_SRCDIR, + dir, + what); + + if (g_test_verbose ()) + g_print ("cmdline: %s\n", cmdline); + + result = system (cmdline); + g_assert (result == 0); + + g_free (dir); + g_free (cmdline); +} + +static void +test_mu_guile_queries (void) +{ + test_something ("queries"); +} + +static void +test_mu_guile_messages (void) +{ + test_something ("message"); +} + +static void +test_mu_guile_stats (void) +{ + test_something ("stats"); +} + + +int +main (int argc, char *argv[]) +{ + int rv; + g_test_init (&argc, &argv, NULL); + + if (!set_en_us_utf8_locale()) + return 0; /* don't error out... */ + + g_test_add_func ("/guile/queries", test_mu_guile_queries); + g_test_add_func ("/guile/message", test_mu_guile_messages); + g_test_add_func ("/guile/stats", test_mu_guile_stats); + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_LEVEL_WARNING| + G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + rv = g_test_run (); + + return rv; +} diff --git a/guile/tests/test-mu-guile.scm b/guile/tests/test-mu-guile.scm new file mode 100755 index 0000000..8281518 --- /dev/null +++ b/guile/tests/test-mu-guile.scm @@ -0,0 +1,124 @@ +#!/bin/sh +exec guile -e main -s $0 $@ +!# + +;; Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +;; +;; This program is free software; you can redistribute it and/or modify it +;; under the terms of the GNU General Public License as published by the +;; Free Software Foundation; either version 3, or (at your option) any +;; later version. +;; +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. +;; + +;; You should have received a copy of the GNU General Public License +;; along with this program; if not, write to the Free Software Foundation, +;; Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +(setlocale LC_ALL "") + +(use-modules (srfi srfi-1)) +(use-modules (ice-9 getopt-long) (ice-9 optargs) (ice-9 popen) (ice-9 format)) +(use-modules (mu) (mu stats)) + +(define (n-results-or-exit query n) + "Run QUERY, and exit 1 if the number of results != N." + (let ((lst (mu:message-list query))) + (if (not (= (length lst) n)) + (begin + (simple-format (current-error-port) "Query: \"~A\"; expected ~A, got ~A\n" + query n (length lst)) + (exit 1))))) + +(define (test-queries) + "Test a bunch of queries (or die trying)." + (n-results-or-exit "hello" 1) + (n-results-or-exit "f:john fruit" 1) + (n-results-or-exit "f:soc@example.com" 1) + (n-results-or-exit "t:alki@example.com" 1) + (n-results-or-exit "t:alcibiades" 1) + (n-results-or-exit "f:soc@example.com OR f:john" 2) + (n-results-or-exit "f:soc@example.com OR f:john OR t:edmond" 3) + (n-results-or-exit "t:julius" 1) + (n-results-or-exit "s:dude" 1) + (n-results-or-exit "t:dantès" 1) + (n-results-or-exit "file:sittingbull.jpg" 1) + (n-results-or-exit "file:custer.jpg" 1) + (n-results-or-exit "file:custer.*" 1) + (n-results-or-exit "j:sit*" 1) + (n-results-or-exit "mime:image/jpeg" 1) + (n-results-or-exit "mime:text/plain" 13) + (n-results-or-exit "y:text*" 13) + (n-results-or-exit "y:image*" 1) + (n-results-or-exit "mime:message/rfc822" 2)) + +(define (error-exit msg . args) + "Print error and exit." + (let ((msg (apply format #f msg args))) + (simple-format (current-error-port) "*ERROR*: ~A\n" msg) + (exit 1))) + +(define (str-equal-or-exit got exp) + "S1 == S2 or exit 1." + ;; (format #t "'~A' <=> '~A'\n" s1 s2) + (if (not (string= exp got)) + (error-exit "Expected \"~A\", got \"~A\"\n" exp got))) + +(define (test-message) + "Test functions for a particular message." + + (let ((msg (car (mu:message-list "hello")))) + (str-equal-or-exit (mu:subject msg) "Fwd: rfc822") + (str-equal-or-exit (mu:to msg) "martin") + (str-equal-or-exit (mu:from msg) "foobar <foo@example.com>") + (str-equal-or-exit (mu:header msg "X-Mailer") "Ximian Evolution 1.4.5") + + (if (not (equal? (mu:priority msg) mu:prio:normal)) + (error-exit "Expected ~A, got ~A" (mu:priority msg) mu:prio:normal))) + + (let ((msg (car (mu:message-list "atoms")))) + (str-equal-or-exit (mu:subject msg) "atoms") + (str-equal-or-exit (mu:to msg) "Democritus <demo@example.com>") + (str-equal-or-exit (mu:from msg) "\"Richard P. Feynman\" <rpf@example.com>") + ;;(str-equal-or-exit (mu:header msg "Content-transfer-encoding") "7BIT") + + (if (not (equal? (mu:priority msg) mu:prio:high)) + (error-exit "Expected ~a, got ~a" (mu:priority msg) mu:prio:high)))) + +(define (num-equal-or-exit got exp) + "S1 == S2 or exit 1." + ;; (format #t "'~A' <=> '~A'\n" s1 s2) + (if (not (= exp got)) + (error-exit "Expected \"~S\", got \"~S\"\n" exp got))) + +(define (test-stats) + "Test statistical functions." + ;; average + (num-equal-or-exit (mu:average mu:size) 82152/13) + (num-equal-or-exit (floor (mu:stddev mu:size)) 13020.0) + (num-equal-or-exit (mu:max mu:size) 46308) + (num-equal-or-exit (mu:min mu:size) 111)) + +(define (main args) + (let* ((optionspec '((muhome (value #t)) + (test (value #t)))) + (options (getopt-long args optionspec)) + (muhome (option-ref options 'muhome #f)) + (test (option-ref options 'test #f))) + + (mu:initialize muhome) + + (if test + (cond + ((string= test "queries") (test-queries)) + ((string= test "message") (test-message)) + ((string= test "stats") (test-stats)) + (#t (exit 1)))))) + + +;; Local Variables: +;; mode: scheme +;; End: diff --git a/guile/texi.texi.in b/guile/texi.texi.in new file mode 100644 index 0000000..716d9a9 --- /dev/null +++ b/guile/texi.texi.in @@ -0,0 +1,3 @@ +@c the version for mu for including in texinfo docs +@set mu-version @VERSION@ + diff --git a/lib/Makefile.am b/lib/Makefile.am new file mode 100644 index 0000000..405506e --- /dev/null +++ b/lib/Makefile.am @@ -0,0 +1,285 @@ +## Copyright (C) 2010-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +# enforce compiling guile (optionally) first,then this dir first +# before descending into tests/ +include $(top_srcdir)/gtest.mk + +SUBDIRS= utils query + +if HAVE_JSON_GLIB +json_srcs= \ + mu-msg-json.c +json_flag="-DHAVE_JSON_GLIB" +endif + +AM_CFLAGS= \ + $(WARN_CFLAGS) \ + $(GMIME_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(GUILE_CFLAGS) \ + $(JSON_GLIB_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(json_flag) \ + $(CODE_COVERAGE_CFLAGS) \ + -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_srcdir}/testdir2\" \ + -DMU_TESTMAILDIR3=\"${abs_srcdir}/testdir3\" \ + -DMU_TESTMAILDIR4=\"${abs_srcdir}/testdir4\" \ + -DABS_CURDIR=\"${abs_builddir}\" \ + -DABS_SRCDIR=\"${abs_srcdir}\" \ + -Wno-format-nonliteral \ + -Wno-switch-enum \ + -Wno-deprecated-declarations \ + -Wno-inline + +AM_CXXFLAGS= \ + $(GMIME_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(GUILE_CFLAGS) \ + $(JSON_GLIB_CFLAGS) \ + $(json_flag) \ + $(WARN_CXXFLAGS) \ + $(XAPIAN_CXXFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -DMU_TESTMAILDIR=\"${abs_srcdir}/testdir\" + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +# don't use -Werror, as it might break on other compilers +# use -Wno-unused-parameters, because some callbacks may not +# really need all the params they get +# AM_CFLAGS=-Wall -Wextra -Wno-unused-parameter \ +# -Wdeclaration-after-statement -Wno-variadic-macros +# AM_CXXFLAGS=-Wall -Wextra -Wno-unused-parameter + +noinst_LTLIBRARIES= \ + libmu.la + +libmu_la_SOURCES= \ + mu-bookmarks.c \ + mu-bookmarks.h \ + mu-contacts.cc \ + mu-contacts.hh \ + mu-container.c \ + mu-container.h \ + mu-flags.h \ + mu-flags.c \ + mu-index.c \ + mu-index.h \ + mu-maildir.c \ + mu-maildir.h \ + mu-msg-crypto.c \ + mu-msg-doc.cc \ + mu-msg-doc.h \ + mu-msg-fields.c \ + mu-msg-fields.h \ + mu-msg-file.c \ + mu-msg-file.h \ + mu-msg-iter.cc \ + mu-msg-iter.h \ + $(json_srcs) \ + mu-msg-part.c \ + mu-msg-part.h \ + mu-msg-prio.c \ + mu-msg-prio.h \ + mu-msg-priv.h \ + mu-msg-sexp.c \ + mu-msg.c \ + mu-msg.h \ + mu-msg.h \ + mu-query.cc \ + mu-query.h \ + mu-runtime.cc \ + mu-runtime.h \ + mu-script.c \ + mu-script.h \ + mu-store.cc \ + mu-store.hh \ + mu-threader.c \ + mu-threader.h + +libmu_la_LIBADD= \ + $(XAPIAN_LIBS) \ + $(GMIME_LIBS) \ + $(GLIB_LIBS) \ + $(GUILE_LIBS) \ + $(JSON_GLIB_LIBS) \ + ${builddir}/utils/libmu-utils.la \ + ${builddir}/query/libmu-query.la \ + $(CODE_COVERAGE_LIBS) + +libmu_la_LDFLAGS= \ + $(ASAN_LDFLAGS) + +EXTRA_DIST= \ + mu-msg-crypto.c \ + doxyfile.in + +noinst_PROGRAMS= $(TEST_PROGS) + +noinst_LTLIBRARIES+= \ + libtestmucommon.la + +TEST_PROGS += test-mu-maildir +test_mu_maildir_SOURCES= test-mu-maildir.c dummy.cc +test_mu_maildir_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-msg-fields +test_mu_msg_fields_SOURCES= test-mu-msg-fields.c dummy.cc +test_mu_msg_fields_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-msg +test_mu_msg_SOURCES= test-mu-msg.c dummy.cc +test_mu_msg_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-store +test_mu_store_SOURCES= test-mu-store.c dummy.cc +test_mu_store_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-flags +test_mu_flags_SOURCES= test-mu-flags.c dummy.cc +test_mu_flags_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-container +test_mu_container_SOURCES= test-mu-container.c dummy.cc +test_mu_container_LDADD= libtestmucommon.la + +TEST_PROGS += test-mu-contacts +test_mu_contacts_SOURCES= test-mu-contacts.cc +test_mu_contacts_LDADD= libtestmucommon.la + +# we need to use dummy.cc to enforce c++ linking... +BUILT_SOURCES= \ + dummy.cc + +dummy.cc: + touch dummy.cc + +libtestmucommon_la_SOURCES= \ + test-mu-common.c \ + test-mu-common.h +libtestmucommon_la_LIBADD= \ + libmu.la + +# note the question marks; make does not like files with ':', so we +# use the (also supported) version with '!' instead. We could escape +# the : with \: but automake does not recognize that.... + +# test messages, the '.ignore' message should be ignored +# when indexing +EXTRA_DIST+= \ + testdir/tmp/1220863087.12663.ignore \ + testdir/new/1220863087.12663_9.mindcrime \ + testdir/new/1220863087.12663_25.mindcrime \ + testdir/new/1220863087.12663_21.mindcrime \ + testdir/new/1220863087.12663_23.mindcrime \ + testdir/cur/1220863087.12663_5.mindcrime!2,S \ + testdir/cur/1220863087.12663_7.mindcrime!2,RS \ + testdir/cur/1220863087.12663_15.mindcrime!2,PS \ + testdir/cur/1220863087.12663_19.mindcrime!2,S \ + testdir/cur/1220863042.12663_1.mindcrime!2,S \ + testdir/cur/1220863060.12663_3.mindcrime!2,S \ + testdir/cur/1283599333.1840_11.cthulhu!2, \ + testdir/cur/1305664394.2171_402.cthulhu!2, \ + testdir/cur/1252168370_3.14675.cthulhu!2,S \ + testdir/cur/encrypted!2,S \ + testdir/cur/multimime!2,FS \ + testdir/cur/signed!2,S \ + testdir/cur/signed-encrypted!2,S \ + testdir/cur/special!2,Sabc \ + testdir/cur/multirecip!2,S \ + testdir2/bar/cur/mail1 \ + testdir2/bar/cur/mail2 \ + testdir2/bar/cur/mail3 \ + testdir2/bar/cur/mail4 \ + testdir2/bar/cur/mail5 \ + testdir2/bar/cur/181736.eml \ + testdir2/bar/cur/mail6 \ + testdir2/bar/tmp/.noindex \ + testdir2/bar/new/.noindex \ + testdir2/Foo/cur/mail5 \ + testdir2/Foo/cur/arto.eml \ + testdir2/Foo/cur/fraiche.eml \ + testdir2/Foo/tmp/.noindex \ + testdir2/Foo/new/.noindex \ + testdir2/wom_bat/cur/atomic \ + testdir2/wom_bat/cur/rfc822.1 \ + testdir2/wom_bat/cur/rfc822.2 \ + testdir3/cycle \ + testdir3/cycle/new/.noindex \ + testdir3/cycle/cur/rogue0 \ + testdir3/cycle/cur/cycle0 \ + testdir3/cycle/cur/cycle0.0 \ + testdir3/cycle/cur/cycle0.0.0 \ + testdir3/cycle/tmp/.noindex \ + testdir3/tree/new/.noindex \ + testdir3/tree/cur/child0.0 \ + testdir3/tree/cur/child4.0 \ + testdir3/tree/cur/root2 \ + testdir3/tree/cur/root1 \ + testdir3/tree/cur/child3.0.0.0.0 \ + testdir3/tree/cur/root0 \ + testdir3/tree/cur/child2.0.0 \ + testdir3/tree/cur/child0.1 \ + testdir3/tree/cur/child0.1.0 \ + testdir3/tree/cur/child4.1 \ + testdir3/tree/tmp/.noindex \ + testdir3/sort/1st-child-promotes-thread/cur/A \ + testdir3/sort/1st-child-promotes-thread/cur/B \ + testdir3/sort/1st-child-promotes-thread/cur/C \ + testdir3/sort/1st-child-promotes-thread/cur/D \ + testdir3/sort/2nd-child-promotes-thread/cur/A \ + testdir3/sort/2nd-child-promotes-thread/cur/B \ + testdir3/sort/2nd-child-promotes-thread/cur/C \ + testdir3/sort/2nd-child-promotes-thread/cur/D \ + testdir3/sort/2nd-child-promotes-thread/cur/E \ + testdir3/sort/child-does-not-promote-thread/cur/A \ + testdir3/sort/child-does-not-promote-thread/cur/X \ + testdir3/sort/child-does-not-promote-thread/cur/Y \ + testdir3/sort/child-does-not-promote-thread/cur/Z \ + testdir3/sort/grandchild-promotes-only-subthread/cur/A \ + testdir3/sort/grandchild-promotes-only-subthread/cur/B \ + testdir3/sort/grandchild-promotes-only-subthread/cur/C \ + testdir3/sort/grandchild-promotes-only-subthread/cur/D \ + testdir3/sort/grandchild-promotes-only-subthread/cur/E \ + testdir3/sort/grandchild-promotes-only-subthread/cur/F \ + testdir3/sort/grandchild-promotes-only-subthread/cur/G \ + testdir3/sort/grandchild-promotes-thread/cur/A \ + testdir3/sort/grandchild-promotes-thread/cur/B \ + testdir3/sort/grandchild-promotes-thread/cur/C \ + testdir3/sort/grandchild-promotes-thread/cur/D \ + testdir3/sort/grandchild-promotes-thread/cur/E \ + testdir4/1220863087.12663_19.mindcrime!2,S \ + testdir4/1220863042.12663_1.mindcrime!2,S \ + testdir4/1283599333.1840_11.cthulhu!2, \ + testdir4/1305664394.2171_402.cthulhu!2, \ + testdir4/1252168370_3.14675.cthulhu!2,S \ + testdir4/mail1 \ + testdir4/mail5 \ + testdir4/181736.eml \ + testdir4/encrypted!2,S \ + testdir4/multimime!2,FS \ + testdir4/signed!2,S \ + testdir4/signed-bad!2,S \ + testdir4/signed-encrypted!2,S \ + testdir4/special!2,Sabc + +TESTS=$(TEST_PROGS) + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/doxyfile.in b/lib/doxyfile.in new file mode 100644 index 0000000..6ad94ac --- /dev/null +++ b/lib/doxyfile.in @@ -0,0 +1,181 @@ +# Doxyfile 0.1 + +#--------------------------------------------------------------------------- +# General configuration options +#--------------------------------------------------------------------------- +PROJECT_NAME = mu +PROJECT_NUMBER = @VERSION@ +OUTPUT_DIRECTORY = apidocs +OUTPUT_LANGUAGE = English +EXTRACT_ALL = NO +EXTRACT_PRIVATE = NO +EXTRACT_STATIC = NO +HIDE_UNDOC_MEMBERS = NO +HIDE_UNDOC_CLASSES = NO +BRIEF_MEMBER_DESC = YES +REPEAT_BRIEF = YES +ALWAYS_DETAILED_SEC = NO +FULL_PATH_NAMES = NO +STRIP_FROM_PATH = +INTERNAL_DOCS = NO +STRIP_CODE_COMMENTS = YES +CASE_SENSE_NAMES = YES +SHORT_NAMES = NO +HIDE_SCOPE_NAMES = NO +VERBATIM_HEADERS = YES +SHOW_INCLUDE_FILES = YES +JAVADOC_AUTOBRIEF = YES +INHERIT_DOCS = YES +INLINE_INFO = YES +SORT_MEMBER_DOCS = YES +DISTRIBUTE_GROUP_DOC = NO +TAB_SIZE = 8 +GENERATE_TODOLIST = YES +GENERATE_TESTLIST = YES +GENERATE_BUGLIST = YES +ALIASES = +ENABLED_SECTIONS = +MAX_INITIALIZER_LINES = 30 +OPTIMIZE_OUTPUT_FOR_C = YES +SHOW_USED_FILES = YES +#--------------------------------------------------------------------------- +# configuration options related to warning and progress messages +#--------------------------------------------------------------------------- +QUIET = YES +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_FORMAT = +WARN_LOGFILE = +#--------------------------------------------------------------------------- +# configuration options related to the input files +#--------------------------------------------------------------------------- +INPUT = @top_srcdir@/lib/ +FILE_PATTERNS = *.c *.h +RECURSIVE = YES +EXCLUDE = tests + +# If the value of the INPUT tag contains directories, you can use the +# EXCLUDE_PATTERNS tag to specify one or more wildcard patterns to exclude +# certain files from those directories. + +EXCLUDE_PATTERNS = Makefile.* ChangeLog CHANGES CHANGES.* README \ + README.* *.png AUTHORS DESIGN DESIGN.* *.desktop \ + DESKTOP* COMMENTS HOWTO magic NOTES TODO THANKS + +# The EXAMPLE_PATH tag can be used to specify one or more files or +# directories that contain example code fragments that are included (see +# the \include command). + +EXAMPLE_PATH = +EXAMPLE_PATTERNS = +EXAMPLE_RECURSIVE = NO +IMAGE_PATH = +INPUT_FILTER = +FILTER_SOURCE_FILES = NO +#--------------------------------------------------------------------------- +# configuration options related to source browsing +#--------------------------------------------------------------------------- +SOURCE_BROWSER = YES +INLINE_SOURCES = NO +REFERENCED_BY_RELATION = YES +REFERENCES_RELATION = YES +#--------------------------------------------------------------------------- +# configuration options related to the alphabetical class index +#--------------------------------------------------------------------------- +ALPHABETICAL_INDEX = NO +COLS_IN_ALPHA_INDEX = 5 +IGNORE_PREFIX = +#--------------------------------------------------------------------------- +# configuration options related to the HTML output +#--------------------------------------------------------------------------- +GENERATE_HTML = YES +HTML_OUTPUT = +HTML_HEADER = +HTML_FOOTER = +HTML_STYLESHEET = +HTML_ALIGN_MEMBERS = YES +GENERATE_HTMLHELP = NO +GENERATE_CHI = NO +BINARY_TOC = NO +TOC_EXPAND = NO +DISABLE_INDEX = NO +ENUM_VALUES_PER_LINE = 4 +GENERATE_TREEVIEW = NO +TREEVIEW_WIDTH = 250 +#--------------------------------------------------------------------------- +# configuration options related to the LaTeX output +#--------------------------------------------------------------------------- +GENERATE_LATEX = NO +LATEX_OUTPUT = +COMPACT_LATEX = NO +PAPER_TYPE = a4wide +EXTRA_PACKAGES = +LATEX_HEADER = +PDF_HYPERLINKS = NO +USE_PDFLATEX = NO +LATEX_BATCHMODE = NO +#--------------------------------------------------------------------------- +# configuration options related to the RTF output +#--------------------------------------------------------------------------- +GENERATE_RTF = NO +RTF_OUTPUT = +COMPACT_RTF = NO +RTF_HYPERLINKS = NO +RTF_STYLESHEET_FILE = +RTF_EXTENSIONS_FILE = +#--------------------------------------------------------------------------- +# configuration options related to the man page output +#--------------------------------------------------------------------------- +GENERATE_MAN = YES +MAN_OUTPUT = man +MAN_EXTENSION = .3mu +MAN_LINKS = YES +#--------------------------------------------------------------------------- +# configuration options related to the XML output +#--------------------------------------------------------------------------- +GENERATE_XML = YES +#--------------------------------------------------------------------------- +# Configuration options related to the preprocessor +#--------------------------------------------------------------------------- +ENABLE_PREPROCESSING = YES +MACRO_EXPANSION = YES +EXPAND_ONLY_PREDEF = YES +SEARCH_INCLUDES = YES +INCLUDE_PATH = +INCLUDE_FILE_PATTERNS = +PREDEFINED = "G_BEGIN_DECLS=" \ + "G_END_DECLS=" +# "DOXYGEN_SHOULD_SKIP_THIS" \ +# "DBUS_GNUC_DEPRECATED=" \ +# "_DBUS_DEFINE_GLOBAL_LOCK(name)=" \ +# "_DBUS_GNUC_PRINTF(from,to)=" +SKIP_FUNCTION_MACROS = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to external references +#--------------------------------------------------------------------------- +TAGFILES = +GENERATE_TAGFILE = +ALLEXTERNALS = NO +PERL_PATH = +#--------------------------------------------------------------------------- +# Configuration options related to the dot tool +#--------------------------------------------------------------------------- +CLASS_DIAGRAMS = YES +HAVE_DOT = NO +CLASS_GRAPH = YES +COLLABORATION_GRAPH = YES +TEMPLATE_RELATIONS = YES +HIDE_UNDOC_RELATIONS = YES +INCLUDE_GRAPH = YES +INCLUDED_BY_GRAPH = YES +GRAPHICAL_HIERARCHY = YES +DOT_PATH = +DOTFILE_DIRS = +MAX_DOT_GRAPH_WIDTH = 640 +MAX_DOT_GRAPH_HEIGHT = 1024 +GENERATE_LEGEND = YES +DOT_CLEANUP = YES +#--------------------------------------------------------------------------- +# Configuration::addtions related to the search engine +#--------------------------------------------------------------------------- +SEARCHENGINE = NO diff --git a/lib/mu-bookmarks.c b/lib/mu-bookmarks.c new file mode 100644 index 0000000..2f1ebac --- /dev/null +++ b/lib/mu-bookmarks.c @@ -0,0 +1,146 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2010-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <glib.h> +#include "mu-bookmarks.h" + +#define MU_BOOKMARK_GROUP "mu" + +struct _MuBookmarks { + char *_bmpath; + GHashTable *_hash; +}; + + +static void +fill_hash (GHashTable *hash, GKeyFile *kfile) +{ + gchar **keys, **cur; + + keys = g_key_file_get_keys (kfile, MU_BOOKMARK_GROUP, NULL, NULL); + if (!keys) + return; + + for (cur = keys; *cur; ++cur) { + gchar *val; + val = g_key_file_get_string (kfile, MU_BOOKMARK_GROUP, + *cur, NULL); + if (val) + g_hash_table_insert (hash, *cur, val); + } + + /* don't use g_strfreev, because we put them in the hash table; + * only free the gchar** itself */ + g_free (keys); +} + +static GHashTable* +create_hash_from_key_file (const gchar *bmpath) +{ + GKeyFile *kfile; + GHashTable *hash; + + kfile = g_key_file_new (); + + if (!g_key_file_load_from_file (kfile, bmpath, G_KEY_FILE_NONE, NULL)) { + g_key_file_free (kfile); + return NULL; + } + + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + fill_hash (hash, kfile); + + g_key_file_free (kfile); + + return hash; +} + + + +MuBookmarks* +mu_bookmarks_new (const gchar *bmpath) +{ + MuBookmarks *bookmarks; + GHashTable *hash; + + g_return_val_if_fail (bmpath, NULL); + + hash = create_hash_from_key_file (bmpath); + if (!hash) + return NULL; + + bookmarks = g_new (MuBookmarks, 1); + + bookmarks->_bmpath = g_strdup (bmpath); + bookmarks->_hash = hash; + + return bookmarks; +} + + + +void +mu_bookmarks_destroy (MuBookmarks *bm) +{ + if (!bm) + return; + + g_free (bm->_bmpath); + g_hash_table_destroy (bm->_hash); + g_free (bm); +} + +const gchar* +mu_bookmarks_lookup (MuBookmarks *bm, const gchar *name) +{ + g_return_val_if_fail (bm, NULL); + g_return_val_if_fail (name, NULL); + + return g_hash_table_lookup (bm->_hash, name); +} + +struct _BMData { + MuBookmarksForeachFunc _func; + gpointer _user_data; +}; +typedef struct _BMData BMData; + + +static void +each_bookmark (const gchar* key, const gchar *val, BMData *bmdata) +{ + bmdata->_func (key, val, bmdata->_user_data); +} + + +void +mu_bookmarks_foreach (MuBookmarks *bm, MuBookmarksForeachFunc func, + gpointer user_data) +{ + BMData bmdata; + + g_return_if_fail (bm); + g_return_if_fail (func); + + bmdata._func = func; + bmdata._user_data = user_data; + + g_hash_table_foreach (bm->_hash, (GHFunc)each_bookmark, &bmdata); +} diff --git a/lib/mu-bookmarks.h b/lib/mu-bookmarks.h new file mode 100644 index 0000000..2f1d99f --- /dev/null +++ b/lib/mu-bookmarks.h @@ -0,0 +1,86 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_BOOKMARKS_H__ +#define __MU_BOOKMARKS_H__ + +#include <glib.h> + +G_BEGIN_DECLS +/** + * @addtogroup MuBookmarks + * Functions for dealing with bookmarks + * @{ + */ + +struct _MuBookmarks; +/*! \struct MuBookmarks + * \brief Opaque structure representing a sequence of bookmarks + */ +typedef struct _MuBookmarks MuBookmarks; + + +/** + * create a new bookmarks object. when it's no longer needed, use + * mu_bookmarks_destroy + * + * @param bmpath path to the bookmarks file + * + * @return a new BookMarks object, or NULL in case of error + */ +MuBookmarks *mu_bookmarks_new (const gchar *bmpath) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * destroy a bookmarks object + * + * @param bm a bookmarks object, or NULL + */ +void mu_bookmarks_destroy (MuBookmarks *bm); + + +/** + * get the value for some bookmark + * + * @param bm a valid bookmarks object + * @param name name of the bookmark to retrieve + * + * @return the value of the bookmark or NULL in case in error, e.g. if + * the bookmark was not found + */ +const gchar* mu_bookmarks_lookup (MuBookmarks *bm, const gchar *name); + +typedef void (*MuBookmarksForeachFunc) (const gchar *key, const gchar *val, + gpointer user_data); + +/** + * call a function for each bookmark + * + * @param bm a valid bookmarks object + * @param func a callback function to be called for each bookmarks + * @param user_data a user pointer passed to the callback + */ +void mu_bookmarks_foreach (MuBookmarks *bm, MuBookmarksForeachFunc func, + gpointer user_data); + +/** @} */ + +G_END_DECLS + +#endif /*__MU_BOOKMARKS_H__*/ diff --git a/lib/mu-contacts.cc b/lib/mu-contacts.cc new file mode 100644 index 0000000..e5bb21f --- /dev/null +++ b/lib/mu-contacts.cc @@ -0,0 +1,299 @@ +/* +** Copyright (C) 2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-contacts.hh" + +#include <mutex> +#include <unordered_map> +#include <set> +#include <sstream> +#include <functional> +#include <algorithm> + +#include <utils/mu-utils.hh> +#include <glib.h> + +using namespace Mu; + +ContactInfo::ContactInfo (const std::string& _full_address, + const std::string& _email, + const std::string& _name, + bool _personal, time_t _last_seen, size_t _freq): + full_address{_full_address}, + email{_email}, + name{_name}, + personal{_personal}, + last_seen{_last_seen}, + freq{_freq}, + tstamp{g_get_monotonic_time()} {} + + +struct EmailHash { + std::size_t operator()(const std::string& email) const { + std::size_t djb = 5381; // djb hash + for (const auto c : email) + djb = ((djb << 5) + djb) + g_ascii_tolower(c); + return djb; + } +}; + +struct EmailEqual { + bool operator()(const std::string& email1, const std::string& email2) const { + return g_ascii_strcasecmp(email1.c_str(), email2.c_str()) == 0; + } +}; + +struct ContactInfoHash { + std::size_t operator()(const ContactInfo& ci) const { + std::size_t djb = 5381; // djb hash + for (const auto c : ci.email) + djb = ((djb << 5) + djb) + g_ascii_tolower(c); + return djb; + } +}; + +struct ContactInfoEqual { + bool operator()(const Mu::ContactInfo& ci1, const Mu::ContactInfo& ci2) const { + return g_ascii_strcasecmp(ci1.email.c_str(), ci2.email.c_str()) == 0; + } +}; + +struct ContactInfoLessThan { + bool operator()(const Mu::ContactInfo& ci1, const Mu::ContactInfo& ci2) const { + + if (ci1.personal != ci2.personal) + return ci1.personal; // personal comes first + + if (ci1.last_seen != ci2.last_seen) // more recent comes first + return ci1.last_seen > ci2.last_seen; + + if (ci1.freq != ci2.freq) // more frequent comes first + return ci1.freq > ci2.freq; + + return g_ascii_strcasecmp(ci1.email.c_str(), ci2.email.c_str()) < 0; + } +}; + +using ContactUMap = std::unordered_map<const std::string, ContactInfo, EmailHash, EmailEqual>; +//using ContactUSet = std::unordered_set<ContactInfo, ContactInfoHash, ContactInfoEqual>; +using ContactSet = std::set<std::reference_wrapper<const ContactInfo>, ContactInfoLessThan>; + +struct Contacts::Private { + Private(const std::string& serialized): + contacts_{deserialize(serialized)} + {} + + ContactUMap deserialize(const std::string&) const; + std::string serialize() const; + + ContactUMap contacts_; + std::mutex mtx_; +}; + +constexpr auto Separator = "\xff"; // Invalid in UTF-8 + +ContactUMap +Contacts::Private::deserialize(const std::string& serialized) const +{ + ContactUMap contacts; + std::stringstream ss{serialized, std::ios_base::in}; + std::string line; + + while (getline (ss, line)) { + + const auto parts = Mu::split (line, Separator); + if (G_UNLIKELY(parts.size() != 6)) { + g_warning ("error: '%s'", line.c_str()); + continue; + } + + ContactInfo ci(std::move(parts[0]), // full address + parts[1], // email + std::move(parts[2]), // name + parts[3][0] == '1' ? true : false, // personal + (time_t)g_ascii_strtoll(parts[4].c_str(), NULL, 10), // last_seen + (std::size_t)g_ascii_strtoll(parts[5].c_str(), NULL, 10)); // freq + + contacts.emplace(std::move(parts[1]), std::move(ci)); + + } + + return contacts; +} + + +Contacts::Contacts (const std::string& serialized) : + priv_{std::make_unique<Private>(serialized)} +{} + +Contacts::~Contacts() = default; + +std::string +Contacts::serialize() const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + std::string s; + + for (auto& item: priv_->contacts_) { + const auto& ci{item.second}; + s += Mu::format("%s%s" + "%s%s" + "%s%s" + "%d%s" + "%" G_GINT64_FORMAT "%s" + "%" G_GINT64_FORMAT "\n", + ci.full_address.c_str(), Separator, + ci.email.c_str(), Separator, + ci.name.c_str(), Separator, + ci.personal ? 1 : 0, Separator, + (gint64)ci.last_seen, Separator, + (gint64)ci.freq); + } + + return s; +} + + +// for now, we only care about _not_ having newlines. +static void +wash (std::string& str) +{ + str.erase(std::remove(str.begin(), str.end(), '\n'), str.end()); +} + + +void +Contacts::add (ContactInfo&& ci) +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + auto down = g_ascii_strdown (ci.email.c_str(), -1); + std::string email{down}; + g_free(down); + + auto it = priv_->contacts_.find(email); + if (it != priv_->contacts_.end()) { + auto& ci2 = it->second; + ++ci2.freq; + if (ci.last_seen > ci2.last_seen) { + ci2.last_seen = ci.last_seen; + wash(ci.email); + ci2.email = std::move(ci.email); + if (!ci.name.empty()) { + wash(ci.name); + ci2.name = std::move(ci.name); + } + } + } + + wash(ci.name); + wash(ci.email); + wash(ci.full_address); + + priv_->contacts_.emplace( + ContactUMap::value_type(std::move(email), std::move(ci))); +} + + +const ContactInfo* +Contacts::_find (const std::string& email) const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + ContactInfo ci{"", email, "", false, 0}; + const auto it = priv_->contacts_.find(ci.email); + if (it == priv_->contacts_.end()) + return {}; + else + return &it->second; +} + + +void +Contacts::clear() +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + priv_->contacts_.clear(); +} + + +std::size_t +Contacts::size() const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + return priv_->contacts_.size(); +} + + +void +Contacts::for_each(const EachContactFunc& each_contact) const +{ + std::lock_guard<std::mutex> l_{priv_->mtx_}; + + if (!each_contact) + return; // nothing to do + + // first sort them for 'rank' + ContactSet sorted; + for (const auto& item: priv_->contacts_) + sorted.emplace(item.second); + + for (const auto& ci: sorted) + each_contact (ci); +} + +/// C binding + +size_t +mu_contacts_count (const MuContacts *self) +{ + g_return_val_if_fail (self, 0); + + auto myself = reinterpret_cast<const Mu::Contacts*>(self); + + return myself->size(); +} + +gboolean +mu_contacts_foreach (const MuContacts *self, MuContactsForeachFunc func, + gpointer user_data) +{ + g_return_val_if_fail (self, FALSE); + g_return_val_if_fail (func, FALSE); + + auto myself = reinterpret_cast<const Mu::Contacts*>(self); + + myself->for_each([&](const ContactInfo& ci) { + g_return_if_fail (!ci.email.empty()); + func(ci.full_address.c_str(), + ci.email.c_str(), + ci.name.empty() ? NULL : ci.name.c_str(), + ci.personal, + ci.last_seen, + ci.freq, + ci.tstamp, + user_data); + }); + + return TRUE; +} + +struct _MuContacts : public Mu::Contacts {}; /**< c-compat */ diff --git a/lib/mu-contacts.hh b/lib/mu-contacts.hh new file mode 100644 index 0000000..7873cd6 --- /dev/null +++ b/lib/mu-contacts.hh @@ -0,0 +1,207 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_CONTACTS_HH__ +#define __MU_CONTACTS_HH__ + +#include <glib.h> +#include <time.h> + +struct _MuContacts; +typedef struct _MuContacts MuContacts; + +#ifdef __cplusplus + +#include <memory> +#include <functional> +#include <chrono> +#include <string> +#include <time.h> +#include <inttypes.h> + +namespace Mu { + +/// Data-structure representing information about some contact. + +struct ContactInfo { + /** + * Construct a new ContactInfo + * + * @param _full_address the full email address + name. + * @param _email email address + * @param _name name or empty + * @param _personal is this a personal contact? + * @param _last_seen when was this contact last seen? + * @param _freq how often was this contact seen? + * + * @return + */ + ContactInfo (const std::string& _full_address, + const std::string& _email, + const std::string& _name, + bool _personal, time_t _last_seen, size_t _freq=1); + + std::string full_address; /**< Full name <email> */ + std::string email; /**< email address */ + std::string name; /**< name (or empty) */ + bool personal; /**< is this a personal contact? */ + time_t last_seen; /**< when was this contact last seen? */ + std::size_t freq; /**< how often was this contact seen? */ + + int64_t tstamp; /**< Time-stamp, as per g_get_monotonic_time */ +}; + +/// All contacts +class Contacts { +public: + /** + * Construct a new contacts objects + * + * @param serialized serialized contacts + */ + Contacts (const std::string& serialized = ""); + + /** + * DTOR + * + */ + ~Contacts (); + + /** + * Add a contact + * + * @param ci A contact-info object + */ + void add(ContactInfo&& ci); + + /** + * Clear all contacts + * + */ + void clear(); + + /** + * Get the number of contacts + * + + * @return number of contacts + */ + std::size_t size() const; + + /** + * Are there no contacts? + * + * @return true or false + */ + bool empty() const { return size() == 0; } + + /** + * Get the contacts, serialized. + * + * @return serialized contacts + */ + std::string serialize() const; + + /** + * Find a contact based on the email address. This is not safe, since + * the returned ptr can be invalidated at any time; only for unit-tests. + * + * @param email email address + * + * @return contact info, or {} if not found + */ + const ContactInfo* _find (const std::string& email) const; + + /** + * Prototype for a callable that receives a contact + * + * @param contact some contact + */ + using EachContactFunc = std::function<void (const ContactInfo& contact_info)>; + + /** + * Invoke some callable for each contact, in order of rank. + * + * @param each_contact + */ + void for_each (const EachContactFunc& each_contact) const; + + /** + * For C compatiblityy + * + * @return a MuContacts* referring to this. + */ + const MuContacts* mu_contacts() const { + return reinterpret_cast<const MuContacts*>(this); + } + + + +private: + struct Private; + std::unique_ptr<Private> priv_; +}; + +} // namespace Mu + +#endif /*__cplusplus*/ + +G_BEGIN_DECLS + + +/** + * return the number of contacts + * + * @param self a contacts object + * + * @return the number of contacts + */ +size_t mu_contacts_count (const MuContacts *self); + +/** + * Function called for mu_contacts_foreach; returns the e-mail address, name + * (which may be NULL) , whether the message is 'personal', the timestamp for + * the address (when it was last seen), and the frequency (in how many message + * did this contact participate) and the tstamp (last modification) + * + */ +typedef void (*MuContactsForeachFunc) (const char *full_address, + const char *email, const char *name, + gboolean personal, + time_t last_seen, unsigned freq, + gint64 tstamp, gpointer user_data); + +/** + * call a function for either each contact, or each contact satisfying + * a regular expression, + * + * @param self contacts object + * @param func callback function to be called for each + * @param user_data user data to pass to the callback + * + * @return TRUE if the function succeeded, or FALSE if the provide regular + * expression was invalid (and not NULL) + */ +gboolean mu_contacts_foreach (const MuContacts *self, + MuContactsForeachFunc func, + gpointer user_data); + +G_END_DECLS + +#endif /* __MU_CONTACTS_HH__ */ diff --git a/lib/mu-container.c b/lib/mu-container.c new file mode 100644 index 0000000..a7e07e3 --- /dev/null +++ b/lib/mu-container.c @@ -0,0 +1,691 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> /* for memset */ +#include <math.h> /* for log, ceil */ + +#include "mu-container.h" +#include "mu-msg.h" +#include "mu-msg-iter.h" + + +/* + * path data structure, to determine the thread paths mentioned above; + * the path is filled as we're traversing the tree of MuContainers + * (messages) + */ +struct _Path { + int *_data; + guint _len; +}; +typedef struct _Path Path; + +static Path* path_new (guint initial); +static void path_destroy (Path *p); +static void path_inc (Path *p, guint index); +static gchar* path_to_string (Path *p, const char* frmt); + +MuContainer* +mu_container_new (MuMsg *msg, guint docid, const char *msgid) +{ + MuContainer *c; + + g_return_val_if_fail (!msg || docid != 0, NULL); + + c = g_slice_new0 (MuContainer); + if (msg) + c->msg = mu_msg_ref (msg); + + c->leader = c; + c->docid = docid; + c->msgid = msgid; + + return c; +} + +void +mu_container_destroy (MuContainer *c) +{ + if (!c) + return; + + if (c->msg) + mu_msg_unref (c->msg); + + g_slice_free (MuContainer, c); +} + + +static void +set_parent (MuContainer *c, MuContainer *parent) +{ + while (c) { + c->parent = parent; + c = c->next; + } +} + + +G_GNUC_UNUSED static gboolean +check_dup (MuContainer *c, GHashTable *hash) +{ + if (g_hash_table_lookup (hash, c)) { + g_warning ("ALREADY!!"); + mu_container_dump (c, TRUE); + g_assert (0); + } else + g_hash_table_insert (hash, c, GUINT_TO_POINTER(TRUE)); + + return TRUE; +} + + +G_GNUC_UNUSED static void +assert_no_duplicates (MuContainer *c) +{ + GHashTable *hash; + + hash = g_hash_table_new (g_direct_hash, g_direct_equal); + + mu_container_foreach (c, + (MuContainerForeachFunc)check_dup, + hash); + + g_hash_table_destroy (hash); +} + + +MuContainer* +mu_container_append_siblings (MuContainer *c, MuContainer *sibling) +{ + g_assert (c); + + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (sibling, NULL); + g_return_val_if_fail (c != sibling, NULL); + + /* assert_no_duplicates (c); */ + + set_parent (sibling, c->parent); + + /* find the last sibling and append; first we try our cache + * 'last', otherwise we need to walk the chain. We use a + * cached last as to avoid walking the chain (which is + * O(n*n)) */ + if (c->last) + c->last->next = sibling; + else { + /* no 'last' cached, so walk the chain */ + MuContainer *c2; + for (c2 = c; c2 && c2->next; c2 = c2->next); + c2->next = sibling; + } + /* update the cached last */ + c->last = sibling->last ? sibling->last : sibling; + + /* assert_no_duplicates (c); */ + + return c; +} + +MuContainer* +mu_container_remove_sibling (MuContainer *c, MuContainer *sibling) +{ + MuContainer *cur, *prev; + + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (sibling, NULL); + + for (prev = NULL, cur = c; cur; cur = cur->next) { + + if (cur == sibling) { + if (!prev) + c = cur->next; + else + prev->next = cur->next; + break; + } + prev = cur; + } + + /* unset the cached last; it's not valid anymore + * + * TODO: we could actually do a better job updating last + * rather than invalidating it. */ + if (c) + c->last = NULL; + + return c; +} + +MuContainer* +mu_container_append_children (MuContainer *c, MuContainer *child) +{ + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (child, NULL); + g_return_val_if_fail (c != child, NULL); + + /* assert_no_duplicates (c); */ + + set_parent (child, c); + if (!c->child) + c->child = child; + else + c->child = mu_container_append_siblings (c->child, child); + + /* assert_no_duplicates (c->child); */ + + return c; +} + + +MuContainer* +mu_container_remove_child (MuContainer *c, MuContainer *child) +{ + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (child, NULL); + + /* g_assert (!child->child); */ + /* g_return_val_if_fail (!child->child, NULL); */ + g_return_val_if_fail (c != child, NULL); + + c->child = mu_container_remove_sibling (c->child, child); + + return c; +} + +typedef void (*MuContainerPathForeachFunc) (MuContainer*, gpointer, Path*); + +static void +mu_container_path_foreach_real (MuContainer *c, guint level, Path *path, + MuContainerPathForeachFunc func, + gpointer user_data) +{ + if (!c) + return; + + path_inc (path, level); + func (c, user_data, path); + + /* children */ + mu_container_path_foreach_real (c->child, level + 1, path, + func, user_data); + + /* siblings */ + mu_container_path_foreach_real (c->next, level, path, func, user_data); +} + +static void +mu_container_path_foreach (MuContainer *c, MuContainerPathForeachFunc func, + gpointer user_data) +{ + Path *path; + + path = path_new (100); + + mu_container_path_foreach_real (c, 0, path, func, user_data); + + path_destroy (path); +} + + +gboolean +mu_container_foreach (MuContainer *c, MuContainerForeachFunc func, + gpointer user_data) +{ + g_return_val_if_fail (func, FALSE); + + if (!c) + return TRUE; + + if (!mu_container_foreach (c->child, func, user_data)) + return FALSE; /* recurse into children */ + + /* recurse into siblings */ + if (!mu_container_foreach (c->next, func, user_data)) + return FALSE; + + return func (c, user_data); +} + +MuContainer* +mu_container_splice_children (MuContainer *c, MuContainer *sibling) +{ + MuContainer *children; + + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (sibling, NULL); + + children = sibling->child; + sibling->child = NULL; + + return mu_container_append_siblings (c, children); +} + +MuContainer* +mu_container_splice_grandchildren (MuContainer *parent, MuContainer *child) +{ + MuContainer *newchild; + + g_return_val_if_fail (parent, NULL); + g_return_val_if_fail (child, NULL); + g_return_val_if_fail (parent != child, NULL); + + newchild = child->child; + child->child=NULL; + + return mu_container_append_children (parent, newchild); +} + + +static GSList* +mu_container_to_list (MuContainer *c) +{ + GSList *lst; + + for (lst = NULL; c; c = c->next) + lst = g_slist_prepend (lst, c); + + return lst; +} + +static gpointer +list_last_data (GSList *lst) +{ + GSList *tail; + + tail = g_slist_last (lst); + + return tail->data; +} + +static MuContainer* +mu_container_from_list (GSList *lst) +{ + MuContainer *c, *cur, *tail; + + if (!lst) + return NULL; + + tail = list_last_data (lst); + for (c = cur = (MuContainer*)lst->data; cur; lst = g_slist_next(lst)) { + cur->next = lst ? (MuContainer*)lst->data : NULL; + cur->last = tail; + cur=cur->next; + } + + return c; +} + +struct _SortFuncData { + MuMsgFieldId mfid; + gboolean descending; + gpointer user_data; +}; +typedef struct _SortFuncData SortFuncData; + +static int +container_cmp (MuContainer *a, MuContainer *b, MuMsgFieldId mfid) +{ + if (a == b) + return 0; + else if (!a->msg) + return -1; + else if (!b->msg) + return 1; + + return mu_msg_cmp (a->msg, b->msg, mfid); +} + +static int +sort_func_root (MuContainer *a, MuContainer *b, SortFuncData *data) +{ + if (data->descending) + return container_cmp (b->leader, a->leader, data->mfid); + else + return container_cmp (a->leader, b->leader, data->mfid); +} + +static int +sort_func_child (MuContainer *a, MuContainer *b, SortFuncData *data) +{ + return container_cmp (a, b, data->mfid); +} + +static MuContainer* +container_sort(MuContainer *c, GCompareDataFunc func, SortFuncData *sfdata) +{ + GSList *lst; + + lst = mu_container_to_list (c); + lst = g_slist_sort_with_data (lst, func, sfdata); + c = mu_container_from_list (lst); + g_slist_free (lst); + + return c; +} + +static MuContainer* +container_sort_child (MuContainer *c, SortFuncData *sfdata) +{ + MuContainer *cur, *leader; + + if (!c) + return NULL; + + /* find leader */ + leader = c->leader; + for (cur = c; cur; cur = cur->next) { + if (cur->child) + cur->child = container_sort_child (cur->child, sfdata); + if (container_cmp (cur->leader, leader, sfdata->mfid) > 0) + leader = cur->leader; + } + + c = container_sort(c, (GCompareDataFunc)sort_func_child, sfdata); + + /* set parent's leader to the one found */ + c->parent->leader = leader; + + return c; +} + +static MuContainer* +container_sort_root (MuContainer *c, SortFuncData *sfdata) +{ + MuContainer *cur; + + if (!c) + return NULL; + + for (cur = c; cur; cur = cur->next) { + if (cur->child) + cur->child = container_sort_child (cur->child, sfdata); + } + + return container_sort (c, (GCompareDataFunc)sort_func_root, sfdata); +} + +MuContainer* +mu_container_sort (MuContainer *c, MuMsgFieldId mfid, gboolean descending, + gpointer user_data) +{ + SortFuncData sfdata; + + sfdata.mfid = mfid; + sfdata.descending = descending; + sfdata.user_data = user_data; + + g_return_val_if_fail (c, NULL); + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL); + + return container_sort_root (c, &sfdata); +} + + +static gboolean +unequal (MuContainer *a, MuContainer *b) +{ + return a == b ? FALSE : TRUE; +} + + +gboolean +mu_container_reachable (MuContainer *haystack, MuContainer *needle) +{ + g_return_val_if_fail (haystack, FALSE); + g_return_val_if_fail (needle, FALSE); + + if (!mu_container_foreach + (haystack, (MuContainerForeachFunc)unequal, needle)) + return TRUE; + + return FALSE; +} + + +static gboolean +dump_container (MuContainer *c) +{ + const gchar* subject; + + if (!c) { + g_print ("<empty>\n"); + return TRUE; + } + + subject = (c->msg) ? mu_msg_get_subject (c->msg) : "<none>"; + + g_print ("[%s][%s m:%p p:%p docid:%u %s]\n",c->msgid, subject, (void*)c, + (void*)c->parent, c->docid, + c->msg ? mu_msg_get_path (c->msg) : ""); + + return TRUE; +} + + +void +mu_container_dump (MuContainer *c, gboolean recursive) +{ + g_return_if_fail (c); + + if (!recursive) + dump_container (c); + else + mu_container_foreach + (c, + (MuContainerForeachFunc)dump_container, + NULL); +} + + + +static Path* +path_new (guint initial) +{ + Path *p; + + p = g_slice_new0 (Path); + + p->_data = g_new0 (int, initial); + p->_len = initial; + + return p; +} + +static void +path_destroy (Path *p) +{ + if (!p) + return; + + g_free (p->_data); + g_slice_free (Path, p); +} + +static void +path_inc (Path *p, guint index) +{ + if (index + 1 >= p->_len) { + p->_data = g_renew (int, p->_data, 2 * p->_len); + memset (&p->_data[p->_len], 0, p->_len); + p->_len *= 2; + } + + ++p->_data[index]; + p->_data[index + 1] = 0; +} + + +static gchar* +path_to_string (Path *p, const char* frmt) +{ + char *str; + guint u; + + if (!p->_data) + return NULL; + + for (u = 0, str = NULL; p->_data[u] != 0; ++u) { + + char segm[16]; + g_snprintf (segm, sizeof(segm), frmt, p->_data[u] - 1); + + if (!str) + str = g_strdup (segm); + else { + gchar *tmp; + tmp = g_strdup_printf ("%s:%s", str, segm); + g_free (str); + str = tmp; + } + } + + return str; +} + +static unsigned +count_colons (const char *str) +{ + unsigned num; + + num = 0; + while (str++ && *str) + if (*str == ':') + ++num; + + return num; +} + + + +static MuMsgIterThreadInfo* +thread_info_new (gchar *threadpath, gboolean root, gboolean first_child, + gboolean last_child, gboolean empty_parent, + gboolean has_child, gboolean is_dup) +{ + MuMsgIterThreadInfo *ti; + + ti = g_slice_new (MuMsgIterThreadInfo); + ti->threadpath = threadpath; + ti->level = count_colons (threadpath); /* hacky... */ + + ti->prop = MU_MSG_ITER_THREAD_PROP_NONE; + ti->prop |= root ? MU_MSG_ITER_THREAD_PROP_ROOT : 0; + ti->prop |= first_child ? MU_MSG_ITER_THREAD_PROP_FIRST_CHILD : 0; + ti->prop |= last_child ? MU_MSG_ITER_THREAD_PROP_LAST_CHILD : 0; + ti->prop |= empty_parent ? MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT : 0; + ti->prop |= is_dup ? MU_MSG_ITER_THREAD_PROP_DUP : 0; + ti->prop |= has_child ? MU_MSG_ITER_THREAD_PROP_HAS_CHILD : 0; + + return ti; +} + +static void +thread_info_destroy (MuMsgIterThreadInfo *ti) +{ + if (ti) { + g_free (ti->threadpath); + g_slice_free (MuMsgIterThreadInfo, ti); + } +} + + +struct _ThreadInfo { + GHashTable *hash; + const char *format; +}; +typedef struct _ThreadInfo ThreadInfo; + + +static void +add_to_thread_info_hash (GHashTable *thread_info_hash, MuContainer *c, + char *threadpath) +{ + gboolean is_root, first_child, last_child, empty_parent, is_dup, has_child; + + /* 'root' means we're a child of the dummy root-container */ + is_root = (c->parent == NULL); + + first_child = is_root ? FALSE : (c->parent->child == c); + last_child = is_root ? FALSE : (c->next == NULL); + empty_parent = is_root ? FALSE : (!c->parent->msg); + is_dup = c->flags & MU_CONTAINER_FLAG_DUP; + has_child = c->child ? TRUE : FALSE; + + g_hash_table_insert (thread_info_hash, + GUINT_TO_POINTER(c->docid), + thread_info_new (threadpath, + is_root, + first_child, + last_child, + empty_parent, + has_child, + is_dup)); +} + +/* device a format string that is the minimum size to fit up to + * matchnum matches -- returns static memory */ +static const char* +thread_segment_format_string (size_t matchnum) +{ + unsigned digitnum; + static char frmt[16]; + + /* get the number of digits needed in a hex-representation of + * matchnum */ + digitnum = (unsigned) (ceil (log(matchnum)/log(16))); + g_snprintf (frmt, sizeof(frmt), "%%0%ux", digitnum); + + return frmt; +} + +static gboolean +add_thread_info (MuContainer *c, ThreadInfo *ti, Path *path) +{ + gchar *pathstr; + + pathstr = path_to_string (path, ti->format); + add_to_thread_info_hash (ti->hash, c, pathstr); + + return TRUE; +} + + +GHashTable* +mu_container_thread_info_hash_new (MuContainer *root_set, size_t matchnum) +{ + ThreadInfo ti; + + g_return_val_if_fail (root_set, NULL); + g_return_val_if_fail (matchnum > 0, NULL); + + /* create hash docid => thread-info */ + ti.hash = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, + (GDestroyNotify)thread_info_destroy); + + ti.format = thread_segment_format_string (matchnum); + + mu_container_path_foreach (root_set, + (MuContainerPathForeachFunc)add_thread_info, + &ti); + + return ti.hash; +} diff --git a/lib/mu-container.h b/lib/mu-container.h new file mode 100644 index 0000000..69c1950 --- /dev/null +++ b/lib/mu-container.h @@ -0,0 +1,224 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_CONTAINER_H__ +#define __MU_CONTAINER_H__ + +#include <glib.h> +#include <mu-msg.h> + +enum _MuContainerFlag { + MU_CONTAINER_FLAG_NONE = 0, + MU_CONTAINER_FLAG_DELETE = 1 << 0, + MU_CONTAINER_FLAG_SPLICE = 1 << 1, + MU_CONTAINER_FLAG_DUP = 1 << 2 +}; +typedef enum _MuContainerFlag MuContainerFlag; + +/* + * MuContainer data structure, as seen in JWZs document: + * http://www.jwz.org/doc/threading.html + */ +struct _MuContainer { + struct _MuContainer *parent, *child, *next; + + /* note: we cache the last of the string of next->next->... + * `mu_container_append_siblings' shows up high in the + * profiles since it needs to walk to the end, and this give + * O(n*n) behavior. + * */ + struct _MuContainer *last; + + /* Node in the subtree rooted at this node which comes first + * in the descending sort order, e.g. the latest message if + * sorting by date. We compare the leaders when ordering + * subtrees. */ + struct _MuContainer *leader; + + MuMsg *msg; + const char *msgid; + + unsigned docid; + MuContainerFlag flags; + +}; +typedef struct _MuContainer MuContainer; + + +/** + * create a new Container object + * + * @param msg a MuMsg, or NULL; when it's NULL, docid should be 0 + * @param docid a Xapian docid, or 0 + * @param msgid a message id, or NULL + * + * @return a new Container instance, or NULL in case of error; free + * with mu_container_destroy + */ +MuContainer* mu_container_new (MuMsg *msg, guint docid, const char* msgid); + + +/** + * free a Container object + * + * @param c a Container object, or NULL + */ +void mu_container_destroy (MuContainer *c); + + + +/** + * append new child(ren) to this container; the child(ren) container's + * parent pointer will point to this one + * + * @param c a Container instance + * @param child a child + * + * @return the Container instance with a child added + */ +MuContainer* mu_container_append_children (MuContainer *c, MuContainer *child); + +/** + * append a new sibling to this (list of) containers; all the siblings + * will get the same parent that @c has + * + * @param c a container instance + * @param sibling a sibling + * + * @return the container (list) with the sibling(s) appended + */ +MuContainer* mu_container_append_siblings (MuContainer *c, MuContainer *sibling); + +/** + * remove a _single_ child container from a container + * + * @param c a container instance + * @param child the child container to remove + * + * @return the container with the child removed; if the container did + * have this child, nothing changes + */ +MuContainer* mu_container_remove_child (MuContainer *c, MuContainer *child); + +/** + * remove a _single_ sibling container from a container + * + * @param c a container instance + * @param sibling the sibling container to remove + * + * @return the container with the sibling removed; if the container did + * have this sibling, nothing changes + */ +MuContainer* mu_container_remove_sibling (MuContainer *c, MuContainer *sibling); + +/** + * promote sibling's children to be this container's siblings + * + * @param c a container instance + * @param sibling a sibling of this container + * + * @return the container with the sibling's children promoted + */ + +MuContainer* mu_container_splice_children (MuContainer *c, + MuContainer *sibling); + +/** + * promote child's children to be parent's children + * + * @param parent a container instance + * @param child a child of this container + * + * @return the new container with it's children's children promoted + */ +MuContainer* mu_container_splice_grandchildren (MuContainer *parent, + MuContainer *child); + +typedef gboolean (*MuContainerForeachFunc) (MuContainer*, gpointer); + +/** + * execute some function on all siblings an children of some container + * (recursively) until all children have been visited or the callback + * function returns FALSE + * + * @param c a container + * @param func a function to call for each container + * @param user_data a pointer to pass to the callback function + * + * @return + */ +gboolean mu_container_foreach (MuContainer *c, + MuContainerForeachFunc func, + gpointer user_data); + +/** + * check whether container needle is a child or sibling (recursively) + * of container haystack + * + * @param haystack a container + * @param needle a container + * + * @return TRUE if needle is reachable from haystack, FALSE otherwise + */ +gboolean mu_container_reachable (MuContainer *haystack, MuContainer *needle); + + +/** + * dump the container to stdout (for debugging) + * + * @param c a container + * @param recursive whether to include siblings, children + */ +void mu_container_dump (MuContainer *c, gboolean recursive); + + +typedef int (*MuContainerCmpFunc) (MuContainer *c1, MuContainer *c2, + gpointer user_data); + +/** + * sort the tree of MuContainers, recursively; ie. each of the list of + * siblings (children) will be sorted according to @func; if the + * container is empty, the first non-empty 'leftmost' child is used. + * + * @param c a container + * @param mfid the field to sort by + * @param revert if TRUE, revert the sorting order * + * @param user_data a user pointer to pass to the sorting function + * + * @return a sorted container + */ +MuContainer* mu_container_sort (MuContainer *c, MuMsgFieldId mfid, + gboolean revert, + gpointer user_data); + + +/** + * create a hashtable with maps document-ids to information about them, + * ie. Xapian docid => MuMsgIterThreadInfo + * + * @param root_set the containers @param matchnum the number of + * matches in the list (this is needed to determine the shortest + * possible collation keys ('threadpaths') for the messages + * + * @return a hash; free with g_hash_table_destroy + */ +GHashTable* mu_container_thread_info_hash_new (MuContainer *root_set, + size_t matchnum); + +#endif /*__MU_CONTAINER_H__*/ diff --git a/lib/mu-flags.c b/lib/mu-flags.c new file mode 100644 index 0000000..330fa31 --- /dev/null +++ b/lib/mu-flags.c @@ -0,0 +1,241 @@ +/* +** Copyright (C) 2011-2012 <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> +#include "mu-flags.h" + +struct _FlagInfo { + MuFlags flag; + char kar; + const char *name; + MuFlagType flag_type; +}; +typedef struct _FlagInfo FlagInfo; + +static const FlagInfo FLAG_INFO[] = { + + /* NOTE: order of this is significant, due to optimizations + * below */ + + { MU_FLAG_DRAFT, 'D', "draft", MU_FLAG_TYPE_MAILFILE }, + { MU_FLAG_FLAGGED, 'F', "flagged", MU_FLAG_TYPE_MAILFILE }, + { MU_FLAG_PASSED, 'P', "passed", MU_FLAG_TYPE_MAILFILE }, + { MU_FLAG_REPLIED, 'R', "replied", MU_FLAG_TYPE_MAILFILE }, + { MU_FLAG_SEEN, 'S', "seen", MU_FLAG_TYPE_MAILFILE }, + { MU_FLAG_TRASHED, 'T', "trashed", MU_FLAG_TYPE_MAILFILE }, + + { MU_FLAG_NEW, 'N', "new", MU_FLAG_TYPE_MAILDIR }, + + { MU_FLAG_SIGNED, 'z', "signed", MU_FLAG_TYPE_CONTENT }, + { MU_FLAG_ENCRYPTED, 'x', "encrypted", MU_FLAG_TYPE_CONTENT }, + { MU_FLAG_HAS_ATTACH, 'a', "attach", MU_FLAG_TYPE_CONTENT }, + { MU_FLAG_LIST, 'l', "list", MU_FLAG_TYPE_CONTENT }, + + + { MU_FLAG_UNREAD, 'u', "unread", MU_FLAG_TYPE_PSEUDO } +}; + + +MuFlagType +mu_flag_type (MuFlags flag) +{ + unsigned u; + + for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u) + if (FLAG_INFO[u].flag == flag) + return FLAG_INFO[u].flag_type; + + return MU_FLAG_TYPE_INVALID; +} + + +char +mu_flag_char (MuFlags flag) +{ + unsigned u; + + for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u) + if (FLAG_INFO[u].flag == flag) + return FLAG_INFO[u].kar; + return 0; +} + + +MuFlags +mu_flag_char_from_name (const char *str) +{ + unsigned u; + + g_return_val_if_fail (str, MU_FLAG_INVALID); + + for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u) + if (g_strcmp0(FLAG_INFO[u].name, str) == 0) + return FLAG_INFO[u].kar; + + return 0; +} + + +static MuFlags +mu_flag_from_char (char kar) +{ + unsigned u; + + for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u) + if (FLAG_INFO[u].kar == kar) + return FLAG_INFO[u].flag; + + return MU_FLAG_INVALID; +} + + +const char* +mu_flag_name (MuFlags flag) +{ + unsigned u; + + for (u = 0; u != G_N_ELEMENTS (FLAG_INFO); ++u) + if (FLAG_INFO[u].flag == flag) + return FLAG_INFO[u].name; + + return NULL; +} + + +const char* +mu_flags_to_str_s (MuFlags flags, MuFlagType types) +{ + unsigned u,v; + static char str[sizeof(FLAG_INFO) + 1]; + + for (u = 0, v = 0; u != G_N_ELEMENTS(FLAG_INFO); ++u) + if (flags & FLAG_INFO[u].flag && + types & FLAG_INFO[u].flag_type) + str[v++] = FLAG_INFO[u].kar; + str[v] = '\0'; + + return str; +} + + +MuFlags +mu_flags_from_str (const char *str, MuFlagType types, + gboolean ignore_invalid) +{ + const char *cur; + MuFlags flag; + + g_return_val_if_fail (str, MU_FLAG_INVALID); + + for (cur = str, flag = MU_FLAG_NONE; *cur; ++cur) { + + MuFlags f; + + f = mu_flag_from_char (*cur); + + if (f == MU_FLAG_INVALID) { + if (ignore_invalid) + continue; + return MU_FLAG_INVALID; + } + + if (mu_flag_type (f) & types) + flag |= f; + } + + return flag; +} + + + +char* +mu_flags_custom_from_str (const char *str) +{ + char *custom; + const char* cur; + unsigned u; + + g_return_val_if_fail (str, NULL); + + for (cur = str, u = 0, custom = NULL; *cur; ++cur) { + + MuFlags flag; + flag = mu_flag_from_char (*cur); + + /* if it's a valid file flag, ignore it */ + if (flag != MU_FLAG_INVALID && + mu_flag_type (flag) == MU_FLAG_TYPE_MAILFILE) + continue; + + /* otherwise, add it to our custom string */ + if (!custom) + custom = g_new0 (char, strlen(str) + 1); + custom[u++] = *cur; + } + + return custom; +} + + + +void +mu_flags_foreach (MuFlagsForeachFunc func, gpointer user_data) +{ + unsigned u; + + g_return_if_fail (func); + + for (u = 0; u != G_N_ELEMENTS(FLAG_INFO); ++u) + func (FLAG_INFO[u].flag, user_data); +} + + +MuFlags +mu_flags_from_str_delta (const char *str, MuFlags oldflags, + MuFlagType types) +{ + const char *cur; + MuFlags newflags; + + g_return_val_if_fail (str, MU_FLAG_INVALID); + + for (cur = str, newflags = oldflags; *cur; ++cur) { + + MuFlags f; + if (*cur == '+' || *cur == '-') { + f = mu_flag_from_char (cur[1]); + if (f == 0) + goto error; + if (*cur == '+') + newflags |= f; + else + newflags &= ~f; + ++cur; + continue; + } + + goto error; + } + + return newflags; +error: + g_warning ("invalid flag string"); + return MU_FLAG_INVALID; + +} diff --git a/lib/mu-flags.h b/lib/mu-flags.h new file mode 100644 index 0000000..9d892f3 --- /dev/null +++ b/lib/mu-flags.h @@ -0,0 +1,184 @@ +/* +** Copyright (C) 2011-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#ifndef __MU_FLAGS_H__ +#define __MU_FLAGS_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +enum _MuFlags { + MU_FLAG_NONE = 0, + + /* next 6 are seen in the file-info part of maildir message + * file names, ie., in a name like "1234345346:2,<fileinfo>", + * <fileinfo> consists of zero or more of the following + * characters (in ascii order) */ + MU_FLAG_DRAFT = 1 << 0, + MU_FLAG_FLAGGED = 1 << 1, + MU_FLAG_PASSED = 1 << 2, + MU_FLAG_REPLIED = 1 << 3, + MU_FLAG_SEEN = 1 << 4, + MU_FLAG_TRASHED = 1 << 5, + + /* decides on cur/ or new/ in the maildir */ + MU_FLAG_NEW = 1 << 6, + + /* content flags -- not visible in the filename, but used for + * searching */ + MU_FLAG_SIGNED = 1 << 7, + MU_FLAG_ENCRYPTED = 1 << 8, + MU_FLAG_HAS_ATTACH = 1 << 9, + + /* pseudo-flag, only for queries, so we can search for + * flag:unread, which is equivalent to 'flag:new OR NOT + * flag:seen' */ + MU_FLAG_UNREAD = 1 << 10, + + /* other content flags */ + MU_FLAG_LIST = 1 << 11 +}; +typedef enum _MuFlags MuFlags; + +#define MU_FLAG_INVALID ((MuFlags)-1) + +enum _MuFlagType { + MU_FLAG_TYPE_MAILFILE = 1 << 0, + MU_FLAG_TYPE_MAILDIR = 1 << 1, + MU_FLAG_TYPE_CONTENT = 1 << 2, + MU_FLAG_TYPE_PSEUDO = 1 << 3 +}; +typedef enum _MuFlagType MuFlagType; + +#define MU_FLAG_TYPE_ANY ((MuFlagType)-1) +#define MU_FLAG_TYPE_INVALID ((MuFlagType)-1) + + +/** + * Get the type of flag (mailfile, maildir, pseudo or content) + * + * @param flag a MuFlag + * + * @return the flag type or MU_FLAG_TYPE_INVALID in case of error + */ +MuFlagType mu_flag_type (MuFlags flag) G_GNUC_CONST; + + +/** + * Get the flag character + * + * @param flag a MuFlag (single) + * + * @return the character, or 0 if it's not a valid flag + */ +char mu_flag_char (MuFlags flag) G_GNUC_CONST; + + +/** + * Get the flag name + * + * @param flag a single MuFlag + * + * @return the name (don't free) as string or NULL in case of error + */ +const char* mu_flag_name (MuFlags flag) G_GNUC_CONST; + + +/** + * Get the string representation of an OR'ed set of flags + * + * @param flags MuFlag (OR'ed) + * @param types allowable types (OR'ed) for the result; the rest is ignored + * + * @return The string representation (static, don't free), or NULL in + * case of error + */ +const char* mu_flags_to_str_s (MuFlags flags, MuFlagType types); + + +/** + * Get the (OR'ed) flags corresponding to a string representation + * + * @param str the file info string + * @param types the flag types to accept (other will be ignored) + * @param ignore invalid if TRUE, ignore invalid flags, otherwise return + * MU_FLAG_INVALID if an invalid flag is encountered + * + * @return the (OR'ed) flags + */ +MuFlags mu_flags_from_str (const char *str, MuFlagType types, + gboolean ignore_invalid); + + + + +/** + * Get the MuFlag char for some flag name + * + * @param str a flag name + * + * @return a flag character, or 0 + */ +MuFlags mu_flag_char_from_name (const char *str); + + +/** + * return the concatenation of all non-standard file flags in str + * (ie., characters other than DFPRST) as a newly allocated string. + * + * @param str the file info string + * + * @return concatenation of all non-standard flags, as a string; free + * with g_free when done. If there are no such flags, return NULL. + */ +char* mu_flags_custom_from_str (const char *str) G_GNUC_WARN_UNUSED_RESULT; + + +/** + * Update #oldflags with the flags in #str, where #str consists of the + * the normal flag characters, but prefixed with either '+' or '-', + * which means resp. "add this flag" or "remove this flag" from + * oldflags. So, e.g. "-N+S" would unset the NEW flag and set the + * SEEN flag, without affecting other flags. + * + * @param str the string representation + * @param old flags to update + * @param types the flag types to accept (other will be ignored) + * + * @return + */ +MuFlags mu_flags_from_str_delta (const char *str, MuFlags oldflags, + MuFlagType types); + + +typedef void (*MuFlagsForeachFunc) (MuFlags flag, gpointer user_data); + +/** + * call a function for each available flag + * + * @param func a function to call + * @param user_data a user pointer to pass to the function + */ +void mu_flags_foreach (MuFlagsForeachFunc func, gpointer user_data); + +G_END_DECLS + +#endif /*__MU_FLAGS_H__*/ diff --git a/lib/mu-index.c b/lib/mu-index.c new file mode 100644 index 0000000..33aaffe --- /dev/null +++ b/lib/mu-index.c @@ -0,0 +1,475 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +1** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-index.h" + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <glib.h> +#include <glib/gstdio.h> +#include <errno.h> + +#include "mu-maildir.h" + +#define MU_LAST_USED_MAILDIR_KEY "last_used_maildir" +#define MU_INDEX_MAX_FILE_SIZE (500*1000*1000) /* 500 Mb */ +/* apparently, people are getting really big mails, so let us index those (by + * default)*/ + +struct _MuIndex { + MuStore *_store; + gboolean _needs_reindex; + guint _max_filesize; +}; + +MuIndex* +mu_index_new (MuStore *store, GError **err) +{ + MuIndex *index; + unsigned count; + + g_return_val_if_fail (store, NULL); + g_return_val_if_fail (!mu_store_is_read_only(store), NULL); + + index = g_new0 (MuIndex, 1); + + index->_store = mu_store_ref (store); + + /* set the default max file size */ + index->_max_filesize = MU_INDEX_MAX_FILE_SIZE; + + count = mu_store_count (store, err); + if (count == (unsigned)-1) + return NULL; + else if (count == 0) + index->_needs_reindex = TRUE; + + return index; +} + +void +mu_index_destroy (MuIndex *index) +{ + if (!index) + return; + + mu_store_unref (index->_store); + g_free (index); +} + + +struct _MuIndexCallbackData { + MuIndexMsgCallback _idx_msg_cb; + MuIndexDirCallback _idx_dir_cb; + MuStore* _store; + void* _user_data; + MuIndexStats* _stats; + gboolean _reindex; + gboolean _lazy_check; + time_t _dirstamp; + guint _max_filesize; +}; +typedef struct _MuIndexCallbackData MuIndexCallbackData; + + +/* checks to determine if we need to (re)index this message note: + * simply checking timestamps is not good enough because message may + * be moved from other dirs (e.g. from 'new' to 'cur') and the time + * stamps won't change. */ +static inline gboolean +needs_index (MuIndexCallbackData *data, const char *fullpath, + time_t filestamp) +{ + /* unconditionally reindex */ + if (data->_reindex) + return TRUE; + + /* it's not in the database yet */ + if (!mu_store_contains_message (data->_store, fullpath)) + return TRUE; + + /* it's there, but it's not up to date */ + if ((unsigned)filestamp >= (unsigned)data->_dirstamp) + return TRUE; + + return FALSE; /* index not needed */ +} + + +static MuError +insert_or_update_maybe (const char *fullpath, const char *mdir, + time_t filestamp, MuIndexCallbackData *data, + gboolean *updated) +{ + MuMsg *msg; + GError *err; + gboolean rv; + + *updated = FALSE; + if (!needs_index (data, fullpath, filestamp)) + return MU_OK; /* nothing to do for this one */ + + err = NULL; + msg = mu_msg_new_from_file (fullpath, mdir, &err); + if (!msg) { + if (!err) + g_warning ("error creating message object: %s", + fullpath); + else { + g_warning ("%s", err->message); + g_clear_error (&err); + } + /* warn, then simply continue */ + return MU_OK; + } + + /* we got a valid id; scan the message contents as well */ + rv = mu_store_add_msg (data->_store, msg, &err); + mu_msg_unref (msg); + + if (!rv) { + g_warning ("error storing message object: %s", + err ? err->message : "cause unknown"); + g_clear_error (&err); + return MU_ERROR; + } + + *updated = TRUE; + return MU_OK; +} + + +static MuError +run_msg_callback_maybe (MuIndexCallbackData *data) +{ + MuError result; + + if (!data || !data->_idx_msg_cb) + return MU_OK; + + result = data->_idx_msg_cb (data->_stats, data->_user_data); + if (G_UNLIKELY(result != MU_OK && result != MU_STOP)) + g_warning ("error in callback"); + + return result; +} + + +static MuError +on_run_maildir_msg (const char *fullpath, const char *mdir, + struct stat *statbuf, MuIndexCallbackData *data) +{ + MuError result; + gboolean updated; + + /* protect against too big messages */ + if (G_UNLIKELY(statbuf->st_size > data->_max_filesize)) { + g_warning ("ignoring because bigger than %u bytes: %s", + data->_max_filesize, fullpath); + return MU_OK; /* not an error */ + } + + result = run_msg_callback_maybe (data); + if (result != MU_OK) + return result; + + /* see if we need to update/insert anything... + * use the ctime, so any status change will be visible (perms, + * filename etc.)*/ + result = insert_or_update_maybe (fullpath, mdir, statbuf->st_ctime, + data, &updated); + + if (result == MU_OK && data && data->_stats) { /* update statistics */ + ++data->_stats->_processed; + updated ? ++data->_stats->_updated : ++data->_stats->_uptodate; + } + + return result; +} + +static time_t +get_dir_timestamp (const char *path) +{ + struct stat statbuf; + + if (stat (path, &statbuf) != 0) { + g_warning ("failed to stat %s: %s", + path, strerror(errno)); + return 0; + } + + return statbuf.st_ctime; +} + +static MuError +on_run_maildir_dir (const char* fullpath, gboolean enter, + MuIndexCallbackData *data) +{ + GError *err; + + err = NULL; + + /* xapian stores a per-dir timestamp; we use this timestamp to determine + * whether a message is up-to-date + */ + if (enter) { + data->_dirstamp = + mu_store_get_dirstamp (data->_store, fullpath, &err); + /* in 'lazy' mode, we only check the dir timestamp, and if it's + * up to date, we don't bother with this dir. This fails to + * account for messages below this dir that have merely + * _changed_ though */ + if (data->_lazy_check && mu_maildir_is_leaf_dir(fullpath)) { + time_t dirstamp; + dirstamp = get_dir_timestamp (fullpath); + if (dirstamp <= data->_dirstamp) { + g_debug ("ignore %s (up-to-date)", fullpath); + return MU_IGNORE; + } + } + g_debug ("entering %s", fullpath); + } else { + mu_store_set_dirstamp (data->_store, fullpath, + time(NULL), &err); + g_debug ("leaving %s", fullpath); + } + + if (data->_idx_dir_cb) + return data->_idx_dir_cb (fullpath, enter, + data->_user_data); + + if (err) { + MU_WRITE_LOG ("%s: %s", __func__, err->message); + g_clear_error(&err); + } + + return MU_OK; +} + +static gboolean +check_path (const char *path) +{ + g_return_val_if_fail (path, FALSE); + + if (!g_path_is_absolute (path)) { + g_warning ("%s: not an absolute path: '%s'", __func__, path); + return FALSE; + } + + if (access (path, R_OK) != 0) { + g_warning ("%s: cannot open '%s': %s", + __func__, path, strerror (errno)); + return FALSE; + } + + return TRUE; +} + +static void +init_cb_data (MuIndexCallbackData *cb_data, MuStore *xapian, + gboolean reindex, gboolean lazycheck, + guint max_filesize, MuIndexStats *stats, + MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb, + void *user_data) +{ + cb_data->_idx_msg_cb = msg_cb; + cb_data->_idx_dir_cb = dir_cb; + + cb_data->_user_data = user_data; + cb_data->_store = xapian; + + cb_data->_reindex = reindex; + cb_data->_lazy_check = lazycheck; + cb_data->_dirstamp = 0; + cb_data->_max_filesize = max_filesize; + + cb_data->_stats = stats; + if (cb_data->_stats) + memset (cb_data->_stats, 0, sizeof(MuIndexStats)); +} + + +void +mu_index_set_max_msg_size (MuIndex *index, guint max_size) +{ + g_return_if_fail (index); + + if (max_size == 0) + index->_max_filesize = MU_INDEX_MAX_FILE_SIZE; + else + index->_max_filesize = max_size; +} + + +MuError +mu_index_run (MuIndex *index, gboolean reindex, gboolean lazycheck, + MuIndexStats *stats, + MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb, + void *user_data) +{ + MuIndexCallbackData cb_data; + MuError rv; + const char *path; + + g_return_val_if_fail (index && index->_store, MU_ERROR); + g_return_val_if_fail (msg_cb, MU_ERROR); + + path = mu_store_root_maildir (index->_store); + if (!check_path (path)) + return MU_ERROR; + + if (index->_needs_reindex) + reindex = TRUE; + + init_cb_data (&cb_data, index->_store, reindex, lazycheck, + index->_max_filesize, stats, + msg_cb, dir_cb, user_data); + + rv = mu_maildir_walk (path, + (MuMaildirWalkMsgCallback)on_run_maildir_msg, + (MuMaildirWalkDirCallback)on_run_maildir_dir, + reindex, /* re-index, ie. do a full update */ + &cb_data); + + mu_store_flush (index->_store); + + return rv; +} + +static MuError +on_stats_maildir_file (const char *fullpath, const char *mdir, + struct stat *statbuf, + MuIndexCallbackData *cb_data) +{ + MuError result; + + if (cb_data && cb_data->_idx_msg_cb) + result = cb_data->_idx_msg_cb (cb_data->_stats, + cb_data->_user_data); + else + result = MU_OK; + + if (result == MU_OK) { + if (cb_data->_stats) + ++cb_data->_stats->_processed; + return MU_OK; + } + + return result; /* MU_STOP or MU_OK */ +} + + +MuError +mu_index_stats (MuIndex *index, + MuIndexStats *stats, MuIndexMsgCallback cb_msg, + MuIndexDirCallback cb_dir, void *user_data) +{ + const char *path; + MuIndexCallbackData cb_data; + + g_return_val_if_fail (index, MU_ERROR); + g_return_val_if_fail (cb_msg, MU_ERROR); + + path = mu_store_root_maildir (index->_store); + if (!check_path (path)) + return MU_ERROR; + + if (stats) + memset (stats, 0, sizeof(MuIndexStats)); + + cb_data._idx_msg_cb = cb_msg; + cb_data._idx_dir_cb = cb_dir; + + cb_data._stats = stats; + cb_data._user_data = user_data; + + cb_data._dirstamp = 0; + + return mu_maildir_walk (path, + (MuMaildirWalkMsgCallback)on_stats_maildir_file, + NULL, FALSE, &cb_data); +} + +struct _CleanupData { + MuStore *_store; + MuIndexStats *_stats; + MuIndexCleanupDeleteCallback _cb; + void *_user_data; + +}; +typedef struct _CleanupData CleanupData; + + +static MuError +foreach_doc_cb (const char* path, CleanupData *cudata) +{ + if (access (path, R_OK) != 0) { + if (errno != EACCES) + g_debug ("cannot access %s: %s", path, strerror(errno)); + if (!mu_store_remove_path (cudata->_store, path)) + return MU_ERROR; /* something went wrong... bail out */ + if (cudata->_stats) + ++cudata->_stats->_cleaned_up; + } + + if (cudata->_stats) + ++cudata->_stats->_processed; + + if (!cudata->_cb) + return MU_OK; + + return cudata->_cb (cudata->_stats, cudata->_user_data); +} + + +MuError +mu_index_cleanup (MuIndex *index, MuIndexStats *stats, + MuIndexCleanupDeleteCallback cb, + void *user_data, GError **err) +{ + MuError rv; + CleanupData cudata; + + g_return_val_if_fail (index, MU_ERROR); + + cudata._store = index->_store; + cudata._stats = stats; + cudata._cb = cb; + cudata._user_data = user_data; + + rv = mu_store_foreach (index->_store, + (MuStoreForeachFunc)foreach_doc_cb, + &cudata, err); + + mu_store_flush (index->_store); + + return rv; +} + +gboolean +mu_index_stats_clear (MuIndexStats *stats) +{ + if (!stats) + return FALSE; + + memset (stats, 0, sizeof(MuIndexStats)); + return TRUE; +} diff --git a/lib/mu-index.h b/lib/mu-index.h new file mode 100644 index 0000000..c0faba8 --- /dev/null +++ b/lib/mu-index.h @@ -0,0 +1,193 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_INDEX_H__ +#define __MU_INDEX_H__ + +#include <stdlib.h> +#include <glib.h> +#include <utils/mu-util.h> +#include <mu-store.hh> + +G_BEGIN_DECLS + +/* opaque structure */ +struct _MuIndex; +typedef struct _MuIndex MuIndex; + +struct _MuIndexStats { + unsigned _processed; /* number of msgs processed or counted */ + unsigned _updated; /* number of msgs new or updated */ + unsigned _cleaned_up; /* number of msgs cleaned up */ + unsigned _uptodate; /* number of msgs already up-to-date */ +}; +typedef struct _MuIndexStats MuIndexStats; + +/** + * create a new MuIndex instance. NOTE: the database does not have + * to exist yet, but the directory must already exist; NOTE(2): before + * doing anything with the returned Index object, make sure you haved + * called mu_msg_init somewhere in your code. + * + * @param store a writable MuStore object + * @param err to receive error or NULL; there are only errors when this + * function returns NULL. Possible errors: see mu-error.h + * + * @return a new MuIndex instance, or NULL in case of error + */ +MuIndex* mu_index_new (MuStore *store, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * destroy the index instance + * + * @param index a MuIndex instance, or NULL + */ +void mu_index_destroy (MuIndex *index); + + +/** + * change the maximum file size that mu-index considers from its + * default (MU_INDEX_MAX_FILE_SIZE). Note that the maximum size is a + * protection against mu (or the libraries it uses) allocating too + * much memory, which can lead to problems + * + * @param index a mu index object + * @param max_size the maximum msg size, or 0 to reset to the default + */ +void mu_index_set_max_msg_size (MuIndex *index, guint max_size); + + +/** + * callback function for mu_index_(run|stats|cleanup), for each message + * + * @param stats pointer to structure to receive statistics data + * @param user_data pointer to user data + * + * @return MU_OK to continue, MU_STOP to stop, or MU_ERROR in + * case of some error. + */ +typedef MuError (*MuIndexMsgCallback) (MuIndexStats* stats, void *user_data); + + +/** + * callback function for mu_index_(run|stats|cleanup), for each dir enter/leave + * + * @param path dirpath we just entered / left + * @param enter did we enter (TRUE) or leave(FALSE) the dir? + * @param user_data pointer to user data + * + * @return MU_OK to continue, MU_STOP to stopd or MU_ERROR in + * case of some error. + */ +typedef MuError (*MuIndexDirCallback) (const char* path, gboolean enter, + void *user_data); + +/** + * start the indexing process + * + * @param index a valid MuIndex instance + * @param force if != 0, force re-indexing already index messages; this is + * obviously a lot slower than only indexing new/changed messages + * @param lazycheck whether ignore subdirectoryies that have up-to-date + * timestamps. + * @param stats a structure with some statistics about the results; + * note that this function does *not* reset the struct values to allow + * for cumulative stats from multiple calls. If needed, you can use + * @mu_index_stats_clear before calling this function + * @param cb_msg a callback function called for every msg indexed; + * @param cb_dir a callback function called for every dir entered/left or NULL + * @param user_data a user pointer that will be passed to the callback function + * + * @return MU_OK if the stats gathering was completed successfully, + * MU_STOP if the user stopped or MU_ERROR in + * case of some error. + */ +MuError mu_index_run (MuIndex *index, gboolean force, + gboolean lazycheck, MuIndexStats *stats, + MuIndexMsgCallback msg_cb, + MuIndexDirCallback dir_cb, void *user_data); + +/** + * gather some statistics about the Maildir; this is usually much faster than + * mu_index_run, and can thus be used to provide some information to the user + * note though that the statistics may be different from the reality that + * mu_index_run sees, when there are updates in the Maildir + * + * @param index a valid MuIndex instance + * @param stats a structure with some statistics about the results; + * note that this function does *not* reset the struct values to allow + * for cumulative stats from multiple calls. If needed, you can use + * @mu_index_stats_clear before calling this function + * @param msg_cb a callback function which will be called for every msg; + * @param dir_cb a callback function which will be called for every dir or NULL + * @param user_data a user pointer that will be passed to the callback function + * xb + * @return MU_OK if the stats gathering was completed successfully, + * MU_STOP if the user stopped or MU_ERROR in + * case of some error. + */ +MuError mu_index_stats (MuIndex *index, MuIndexStats *stats, + MuIndexMsgCallback msg_cb, MuIndexDirCallback dir_cb, + void *user_data); + +/** + * callback function called for each message + * + * @param MuIndexCleanupCallback + * + * @return a MuResult + */ +typedef MuError (*MuIndexCleanupDeleteCallback) (MuIndexStats *stats, + void *user_data); + +/** + * cleanup the database; ie. remove entries for which no longer a corresponding + * file exists in the maildir + * + * @param index a valid MuIndex instance + * @param stats a structure with some statistics about the results; + * note that this function does *not* reset the struct values to allow + * for cumulative stats from multiple calls. If needed, you can use + * @mu_index_stats_clear before calling this function + * @param cb a callback function which will be called for every msg; + * @param user_data a user pointer that will be passed to the callback function + * @param err to receive error info or NULL. err->code is MuError value + * + * @return MU_OK if the stats gathering was completed successfully, + * MU_STOP if the user stopped or MU_ERROR in + * case of some error. + */ +MuError mu_index_cleanup (MuIndex *index, MuIndexStats *stats, + MuIndexCleanupDeleteCallback cb, + void *user_data, GError **err); + +/** + * clear the stats structure + * + * @param stats a MuIndexStats object + * + * @return TRUE if stats != NULL, FALSE otherwise + */ +gboolean mu_index_stats_clear (MuIndexStats *stats); + +G_END_DECLS + +#endif /*__MU_INDEX_H__*/ diff --git a/lib/mu-maildir.c b/lib/mu-maildir.c new file mode 100644 index 0000000..c8c74f4 --- /dev/null +++ b/lib/mu-maildir.c @@ -0,0 +1,943 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2016 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to 59the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <stdlib.h> + +#include <string.h> +#include <errno.h> +#include <glib/gprintf.h> + +#include "mu-maildir.h" +#include "utils/mu-str.h" + +#define MU_MAILDIR_NOINDEX_FILE ".noindex" +#define MU_MAILDIR_NOUPDATE_FILE ".noupdate" + + +/* On Linux (and some BSD), we have entry->d_type, but some file + * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN. + * On other OSs, notably Solaris, entry->d_type is not present at all. + * For these cases, we use lstat (in get_dtype) as a slower fallback, + * and return it in the d_type parameter + */ +#ifdef HAVE_STRUCT_DIRENT_D_TYPE +#define GET_DTYPE(DE,FP) \ + ((DE)->d_type == DT_UNKNOWN ? mu_util_get_dtype_with_lstat((FP)) : \ + (DE)->d_type) +#else +#define GET_DTYPE(DE,FP) \ + mu_util_get_dtype_with_lstat((FP)) +#endif /*HAVE_STRUCT_DIRENT_D_TYPE*/ + + +static gboolean +create_maildir (const char *path, mode_t mode, GError **err) +{ + int i; + const gchar* subdirs[] = {"new", "cur", "tmp"}; + + for (i = 0; i != G_N_ELEMENTS(subdirs); ++i) { + + const char *fullpath; + int rv; + + /* static buffer */ + fullpath = mu_str_fullpath_s (path, subdirs[i]); + + /* if subdir already exists, don't try to re-create + * it */ + if (mu_util_check_dir (fullpath, TRUE, TRUE)) + continue; + + rv = g_mkdir_with_parents (fullpath, (int)mode); + + /* note, g_mkdir_with_parents won't detect an error if + * there's already such a dir, but with the wrong + * permissions; so we need to check */ + if (rv != 0 || !mu_util_check_dir(fullpath, TRUE, TRUE)) + return mu_util_g_set_error + (err,MU_ERROR_FILE_CANNOT_MKDIR, + "creating dir failed for %s: %s", + fullpath, strerror (errno)); + } + + return TRUE; +} + +static gboolean +create_noindex (const char *path, GError **err) +{ + /* create a noindex file if requested */ + int fd; + const char *noindexpath; + + /* static buffer */ + noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE); + + fd = creat (noindexpath, 0644); + + /* note, if the 'close' failed, creation may still have + * succeeded...*/ + if (fd < 0 || close (fd) != 0) + return mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_CREATE, + "error in create_noindex: %s", + strerror (errno)); + return TRUE; +} + +gboolean +mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, GError **err) +{ + g_return_val_if_fail (path, FALSE); + + MU_WRITE_LOG ("%s (%s, %o, %s)", __func__, + path, mode, noindex ? "TRUE" : "FALSE"); + + if (!create_maildir (path, mode, err)) + return FALSE; + + if (noindex && !create_noindex (path, err)) + return FALSE; + + return TRUE; +} + +/* determine whether the source message is in 'new' or in 'cur'; + * we ignore messages in 'tmp' for obvious reasons */ +static gboolean +check_subdir (const char *src, gboolean *in_cur, GError **err) +{ + gboolean rv; + gchar *srcpath; + + srcpath = g_path_get_dirname (src); + *in_cur = FALSE; + rv = TRUE; + + if (g_str_has_suffix (srcpath, "cur")) + *in_cur = TRUE; + else if (!g_str_has_suffix (srcpath, "new")) + rv = mu_util_g_set_error (err, + MU_ERROR_FILE_INVALID_SOURCE, + "invalid source message '%s'", + src); + g_free (srcpath); + return rv; +} + +static gchar* +get_target_fullpath (const char* src, const gchar *targetpath, GError **err) +{ + gchar *targetfullpath, *srcfile; + gboolean in_cur; + + if (!check_subdir (src, &in_cur, err)) + return NULL; + + srcfile = g_path_get_basename (src); + + /* create targetpath; note: make the filename *cough* unique + * by including a hash of the srcname in the targetname. This + * helps if there are copies of a message (which all have the + * same basename) + */ + targetfullpath = g_strdup_printf ("%s%c%s%c%u_%s", + targetpath, + G_DIR_SEPARATOR, + in_cur ? "cur" : "new", + G_DIR_SEPARATOR, + g_str_hash(src), + srcfile); + g_free (srcfile); + + return targetfullpath; +} + + +gboolean +mu_maildir_link (const char* src, const char *targetpath, GError **err) +{ + gchar *targetfullpath; + int rv; + + g_return_val_if_fail (src, FALSE); + g_return_val_if_fail (targetpath, FALSE); + + targetfullpath = get_target_fullpath (src, targetpath, err); + if (!targetfullpath) + return FALSE; + + rv = symlink (src, targetfullpath); + + if (rv != 0) + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_LINK, + "error creating link %s => %s: %s", + targetfullpath, src, strerror (errno)); + g_free (targetfullpath); + + return rv == 0 ? TRUE: FALSE; +} + + +static MuError +process_dir (const char* path, const gchar *mdir, + MuMaildirWalkMsgCallback msg_cb, + MuMaildirWalkDirCallback dir_cb, gboolean full, + void *data); + +static MuError +process_file (const char* fullpath, const gchar* mdir, + MuMaildirWalkMsgCallback msg_cb, void *data) +{ + MuError result; + struct stat statbuf; + + if (!msg_cb) + return MU_OK; + + if (G_UNLIKELY(access(fullpath, R_OK) != 0)) { + g_warning ("cannot access %s: %s", fullpath, + strerror(errno)); + return MU_ERROR; + } + + if (G_UNLIKELY(stat (fullpath, &statbuf) != 0)) { + g_warning ("cannot stat %s: %s", fullpath, strerror(errno)); + return MU_ERROR; + } + + result = (msg_cb)(fullpath, mdir, &statbuf, data); + if (result == MU_STOP) + g_debug ("callback said 'MU_STOP' for %s", fullpath); + else if (result == MU_ERROR) + g_warning ("%s: error in callback (%s)", + __func__, fullpath); + + return result; +} + + +/* + * determine if path is a maildir leaf-dir; ie. if it's 'cur' or 'new' + * (we're skipping 'tmp' for obvious reasons) + */ +gboolean +mu_maildir_is_leaf_dir (const char *path) +{ + size_t len; + + /* path is the full path; it cannot possibly be shorter + * than 4 for a maildir (/cur or /new) */ + len = path ? strlen (path) : 0; + if (G_UNLIKELY(len < 4)) + return FALSE; + + /* optimization; one further idea would be cast the 4 bytes to an + * integer and compare that -- need to think about alignment, + * endianness */ + + if (path[len - 4] == G_DIR_SEPARATOR && + path[len - 3] == 'c' && + path[len - 2] == 'u' && + path[len - 1] == 'r') + return TRUE; + + if (path[len - 4] == G_DIR_SEPARATOR && + path[len - 3] == 'n' && + path[len - 2] == 'e' && + path[len - 1] == 'w') + return TRUE; + + return FALSE; +} + + +/* check if there path contains file; used for checking if there is + * MU_MAILDIR_NOINDEX_FILE or MU_MAILDIR_NOUPDATE_FILE in this + * dir; */ +static gboolean +dir_contains_file (const char *path, const char *file) +{ + const char* fullpath; + + /* static buffer */ + fullpath = mu_str_fullpath_s (path, file); + + if (access (fullpath, F_OK) == 0) + return TRUE; + else if (G_UNLIKELY(errno != ENOENT && errno != EACCES)) + g_warning ("error testing for %s/%s: %s", + fullpath, file, strerror(errno)); + return FALSE; +} + +static gboolean +is_dotdir_to_ignore (const char* dir) +{ + int i; + const char* ignore[] = { + ".notmuch", + ".nnmaildir", + ".#evolution" + }; /* when adding names, check the optimization below */ + + if (dir[0] != '.') + return FALSE; /* not a dotdir */ + + if (dir[1] == '\0' || (dir[1] == '.' && dir[2] == '\0')) + return TRUE; /* ignore '.' and '..' */ + + /* optimization: special dirs have 'n' or '#' in pos 1 */ + if (dir[1] != 'n' && dir[1] != '#') + return FALSE; /* not special: don't ignore */ + + for (i = 0; i != G_N_ELEMENTS(ignore); ++i) + if (strcmp(dir, ignore[i]) == 0) + return TRUE; + + return FALSE; /* don't ignore */ +} + +static gboolean +ignore_dir_entry (struct dirent *entry, unsigned char d_type) +{ + if (G_LIKELY(d_type == DT_REG)) { + + guint u; + + /* ignore emacs tempfiles */ + if (entry->d_name[0] == '#') + return TRUE; + /* ignore dovecot metadata */ + if (entry->d_name[0] == 'd' && + strncmp (entry->d_name, "dovecot", 7) == 0) + return TRUE; + /* ignore special files */ + if (entry->d_name[0] == '.') + return TRUE; + /* ignore core files */ + if (entry->d_name[0] == 'c' && + strncmp (entry->d_name, "core", 4) == 0) + return TRUE; + /* ignore tmp/backup files; find the last char */ + for (u = 0; entry->d_name[u] != '\0'; ++u) { + switch (entry->d_name[u]) { + case '#': + case '~': + /* looks like a backup / tempsave file */ + if (entry->d_name[u + 1] == '\0') + return TRUE; + continue; + default: + continue; + } + } + return FALSE; /* other files: don't ignore */ + + } else if (d_type == DT_DIR) + return is_dotdir_to_ignore (entry->d_name); + else + return TRUE; /* ignore non-normal files, non-dirs */ +} + +/* + * return the maildir value for the the path - this is the directory + * for the message (with the top-level dir as "/"), and without the + * leaf "/cur" or "/new". In other words, contatenate old_mdir + "/" + dir, + * unless dir is either 'new' or 'cur'. The value will be used in queries. + */ +static gchar* +get_mdir_for_path (const gchar *old_mdir, const gchar *dir) +{ + /* if the current dir is not 'new' or 'cur', contatenate + * old_mdir an dir */ + if ((dir[0] == 'n' && strcmp(dir, "new") == 0) || + (dir[0] == 'c' && strcmp(dir, "cur") == 0) || + (dir[0] == 't' && strcmp(dir, "tmp") == 0)) + return strdup (old_mdir ? old_mdir : G_DIR_SEPARATOR_S); + else + return g_strconcat (old_mdir ? old_mdir : "", + G_DIR_SEPARATOR_S, dir, NULL); + +} + + +static MuError +process_dir_entry (const char* path, const char* mdir, struct dirent *entry, + MuMaildirWalkMsgCallback cb_msg, + MuMaildirWalkDirCallback cb_dir, + gboolean full, void *data) +{ + const char *fp; + char* fullpath; + unsigned char d_type; + + /* we have to copy the buffer from fullpath_s, because it + * returns a static buffer, and we maybe called reentrantly */ + fp = mu_str_fullpath_s (path, entry->d_name); + fullpath = g_newa (char, strlen(fp) + 1); + strcpy (fullpath, fp); + + d_type = GET_DTYPE(entry, fullpath); + + /* ignore special files/dirs */ + if (ignore_dir_entry (entry, d_type)) { + /* g_debug ("ignoring %s\n", entry->d_name); */ + return MU_OK; + } + + switch (d_type) { + case DT_REG: /* we only want files in cur/ and new/ */ + if (!mu_maildir_is_leaf_dir (path)) + return MU_OK; + + return process_file (fullpath, mdir, cb_msg, data); + + case DT_DIR: { + char *my_mdir; + MuError rv; + /* my_mdir is the search maildir (the dir starting + * with the top-level maildir as /, and without the + * /tmp, /cur, /new */ + my_mdir = get_mdir_for_path (mdir, entry->d_name); + rv = process_dir (fullpath, my_mdir, cb_msg, cb_dir, full, data); + g_free (my_mdir); + + return rv; + } + + default: + return MU_OK; /* ignore other types */ + } +} + + +static const size_t DIRENT_ALLOC_SIZE = + offsetof (struct dirent, d_name) + PATH_MAX; + +static struct dirent* +dirent_new (void) +{ + return (struct dirent*) g_slice_alloc (DIRENT_ALLOC_SIZE); +} + + +static void +dirent_destroy (struct dirent *entry) +{ + g_slice_free1 (DIRENT_ALLOC_SIZE, entry); +} + +#ifdef HAVE_STRUCT_DIRENT_D_INO +static int +dirent_cmp (struct dirent *d1, struct dirent *d2) +{ + /* we do it his way instead of a simple d1->d_ino - d2->d_ino + * because this way, we don't need 64-bit numbers for the + * actual sorting */ + if (d1->d_ino < d2->d_ino) + return -1; + else if (d1->d_ino > d2->d_ino) + return 1; + else + return 0; +} +#endif /*HAVE_STRUCT_DIRENT_D_INO*/ + +static MuError +process_dir_entries (DIR *dir, const char* path, const char* mdir, + MuMaildirWalkMsgCallback msg_cb, + MuMaildirWalkDirCallback dir_cb, + gboolean full, void *data) +{ + MuError result; + GSList *lst, *c; + + for (lst = NULL;;) { + int rv; + struct dirent *entry, *res; + entry = dirent_new (); + rv = readdir_r (dir, entry, &res); + if (rv == 0) { + if (res) + lst = g_slist_prepend (lst, entry); + else { + dirent_destroy (entry); + break; /* last direntry reached */ + } + } else { + dirent_destroy (entry); + g_warning ("error scanning dir: %s", strerror(rv)); + return MU_ERROR_FILE; + } + } + + /* we sort by inode; this makes things much faster on + * extfs2,3 */ +#if HAVE_STRUCT_DIRENT_D_INO + c = lst = g_slist_sort (lst, (GCompareFunc)dirent_cmp); +#endif /*HAVE_STRUCT_DIRENT_D_INO*/ + + for (c = lst, result = MU_OK; c && result == MU_OK; c = g_slist_next(c)) + result = process_dir_entry (path, mdir, (struct dirent*)c->data, + msg_cb, dir_cb, full, data); + + g_slist_foreach (lst, (GFunc)dirent_destroy, NULL); + g_slist_free (lst); + + return result; +} + + +static MuError +process_dir (const char* path, const char* mdir, + MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb, + gboolean full, void *data) +{ + MuError result; + DIR* dir; + + /* if it has a noindex file, we ignore this dir */ + if (dir_contains_file (path, MU_MAILDIR_NOINDEX_FILE) || + (!full && dir_contains_file (path, MU_MAILDIR_NOUPDATE_FILE))) { + g_debug ("found noindex/noupdate: ignoring dir %s", path); + return MU_OK; + } + + if (dir_cb) { + MuError rv; + rv = dir_cb (path, TRUE/*enter*/, data); + /* ignore this dir; not necessarily an _error_, dir might + * be up-to-date and return MU_IGNORE */ + if (rv == MU_IGNORE) + return MU_OK; + else if (rv != MU_OK) + return rv; + } + + dir = opendir (path); + if (!dir) { + g_warning ("cannot access %s: %s", path, strerror(errno)); + return MU_OK; + } + + result = process_dir_entries (dir, path, mdir, msg_cb, dir_cb, + full, data); + closedir (dir); + + /* only run dir_cb if it exists and so far, things went ok */ + if (dir_cb && result == MU_OK) + return dir_cb (path, FALSE/*leave*/, data); + + return result; +} + + +MuError +mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg, + MuMaildirWalkDirCallback cb_dir, gboolean full, + void *data) +{ + MuError rv; + char *mypath; + + g_return_val_if_fail (path && cb_msg, MU_ERROR); + g_return_val_if_fail (mu_util_check_dir(path, TRUE, FALSE), MU_ERROR); + + /* strip the final / or \ */ + mypath = g_strdup (path); + if (mypath[strlen(mypath)-1] == G_DIR_SEPARATOR) + mypath[strlen(mypath)-1] = '\0'; + + rv = process_dir (mypath, NULL, cb_msg, cb_dir, full, data); + g_free (mypath); + + return rv; +} + + +static gboolean +clear_links (const char *path, DIR *dir) +{ + gboolean rv; + struct dirent *dentry; + + rv = TRUE; + errno = 0; + + while ((dentry = readdir (dir))) { + + guint8 d_type; + char *fullpath; + + if (dentry->d_name[0] == '.') + continue; /* ignore .,.. other dotdirs */ + + fullpath = g_build_path ("/", path, dentry->d_name, NULL); + d_type = GET_DTYPE (dentry, fullpath); + + if (d_type == DT_LNK) { + if (unlink (fullpath) != 0 ) { + g_warning ("error unlinking %s: %s", + fullpath, strerror(errno)); + rv = FALSE; + } + } else if (d_type == DT_DIR) { + DIR *subdir; + subdir = opendir (fullpath); + if (!subdir) { + g_warning ("failed to open dir %s: %s", + fullpath, strerror(errno)); + rv = FALSE; + goto next; + } + + if (!clear_links (fullpath, subdir)) + rv = FALSE; + + closedir (subdir); + } + + next: + g_free (fullpath); + } + + return rv; +} + +gboolean +mu_maildir_clear_links (const char *path, GError **err) +{ + DIR *dir; + gboolean rv; + + g_return_val_if_fail (path, FALSE); + + dir = opendir (path); + if (!dir) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_OPEN, + "failed to open %s: %s", path, strerror(errno)); + return FALSE; + } + + rv = clear_links (path, dir); + + closedir (dir); + + return rv; +} + + + + +MuFlags +mu_maildir_get_flags_from_path (const char *path) +{ + g_return_val_if_fail (path, MU_FLAG_INVALID); + + /* try to find the info part */ + /* note that we can use either the ':' or '!' as separator; + * the former is the official, but as it does not work on e.g. VFAT + * file systems, some Maildir implementations use the latter instead + * (or both). For example, Tinymail/modest does this. The python + * documentation at http://docs.python.org/lib/mailbox-maildir.html + * mentions the '!' as well as a 'popular choice' + */ + + /* we check the dir -- */ + if (strstr (path, G_DIR_SEPARATOR_S "new" G_DIR_SEPARATOR_S)) { + + char *dir, *dir2; + MuFlags flags; + + dir = g_path_get_dirname (path); + dir2 = g_path_get_basename (dir); + + flags = MU_FLAG_NONE; + + if (g_strcmp0 (dir2, "new") == 0) + flags = MU_FLAG_NEW; + + g_free (dir); + g_free (dir2); + + /* NOTE: new/ message should not have :2,-stuff, as + * per http://cr.yp.to/proto/maildir.html. If they, do + * we ignore it + */ + if (flags == MU_FLAG_NEW) + return flags; + } + + /* get the file flags */ + { + char *info; + + info = strrchr (path, '2'); + if (!info || info == path || + (info[-1] != ':' && info[-1] != '!') || + (info[1] != ',')) + return MU_FLAG_NONE; + else + return mu_flags_from_str + (&info[2], MU_FLAG_TYPE_MAILFILE, + TRUE /*ignore invalid */); + } +} + + +/* + * take an existing message path, and return a new path, based on + * whether it should be in 'new' or 'cur'; ie. + * + * /home/user/Maildir/foo/bar/cur/abc:2,F and flags == MU_FLAG_NEW + * => /home/user/Maildir/foo/bar/new + * and + * /home/user/Maildir/foo/bar/new/abc and flags == MU_FLAG_REPLIED + * => /home/user/Maildir/foo/bar/cur + * + * so the difference is whether MU_FLAG_NEW is set or not; and in the + * latter case, no other flags are allowed. + * + */ +static gchar* +get_new_path (const char *mdir, const char *mfile, MuFlags flags, + const char* custom_flags) +{ + if (flags & MU_FLAG_NEW) + return g_strdup_printf ("%s%cnew%c%s", + mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR, + mfile); + else { + const char *flagstr; + flagstr = mu_flags_to_str_s (flags, MU_FLAG_TYPE_MAILFILE); + + return g_strdup_printf ("%s%ccur%c%s:2,%s%s", + mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR, + mfile, flagstr, + custom_flags ? custom_flags : ""); + } +} + + +char* +mu_maildir_get_maildir_from_path (const char* path) +{ + gchar *mdir; + + /* determine the maildir */ + mdir = g_path_get_dirname (path); + if (!g_str_has_suffix (mdir, "cur") && + !g_str_has_suffix (mdir, "new")) { + g_warning ("%s: not a valid maildir path: %s", + __func__, path); + g_free (mdir); + return NULL; + } + + /* remove the 'cur' or 'new' */ + mdir[strlen(mdir) - 4] = '\0'; + + return mdir; +} + + +static char* +get_new_basename (void) +{ + return g_strdup_printf ("%u.%08x%08x.%s", + (guint)time(NULL), + g_random_int(), + (gint32)g_get_monotonic_time (), + g_get_host_name ()); +} + + +char* +mu_maildir_get_new_path (const char *oldpath, const char *new_mdir, + MuFlags newflags, gboolean new_name) +{ + char *mfile, *mdir, *custom_flags, *newpath; + + g_return_val_if_fail (oldpath, NULL); + + mfile = newpath = custom_flags = NULL; + + /* determine the maildir */ + mdir = mu_maildir_get_maildir_from_path (oldpath); + if (!mdir) + return NULL; + + if (new_name) + mfile = get_new_basename (); + else { + /* determine the name of the mailfile, stripped of its flags, as + * well as any custom (non-standard) flags */ + char *cur; + mfile = g_path_get_basename (oldpath); + for (cur = &mfile[strlen(mfile)-1]; cur > mfile; --cur) { + if ((*cur == ':' || *cur == '!') && + (cur[1] == '2' && cur[2] == ',')) { + /* get the custom flags (if any) */ + custom_flags = + mu_flags_custom_from_str (cur + 3); + cur[0] = '\0'; /* strip the flags */ + break; + } + } + } + + newpath = get_new_path (new_mdir ? new_mdir : mdir, + mfile, newflags, custom_flags); + g_free (mfile); + g_free (mdir); + g_free (custom_flags); + + return newpath; +} + + +static gint64 +get_file_size (const char* path) +{ + int rv; + struct stat statbuf; + + rv = stat (path, &statbuf); + if (rv != 0) { + /* g_warning ("error: %s", strerror (errno)); */ + return -1; + } + + return (gint64)statbuf.st_size; +} + + +static gboolean +msg_move_check_pre (const gchar *src, const gchar *dst, GError **err) +{ + gint size1, size2; + + if (!g_path_is_absolute(src)) + return mu_util_g_set_error + (err, MU_ERROR_FILE, + "source is not an absolute path: '%s'", src); + + if (!g_path_is_absolute(dst)) + return mu_util_g_set_error + (err, MU_ERROR_FILE, + "target is not an absolute path: '%s'", dst); + + if (access (src, R_OK) != 0) + return mu_util_g_set_error (err, MU_ERROR_FILE, + "cannot read %s", src); + + if (access (dst, F_OK) != 0) + return TRUE; + + /* target exist; we simply overwrite it, unless target has a different + * size. ignore the exceedingly rare case where have duplicate message + * file names with different content yet the same length. (md5 etc. is a + * bit slow) */ + size1 = get_file_size (src); + size2 = get_file_size (dst); + if (size1 != size2) + return mu_util_g_set_error (err, MU_ERROR_FILE, + "%s already exists", dst); + + return TRUE; +} + +static gboolean +msg_move_check_post (const char *src, const char *dst, GError **err) +{ + /* double check -- is the target really there? */ + if (access (dst, F_OK) != 0) + return mu_util_g_set_error + (err, MU_ERROR_FILE, "can't find target (%s)", dst); + + if (access (src, F_OK) == 0) + return mu_util_g_set_error + (err, MU_ERROR_FILE, "source still there (%s)", src); + + return TRUE; +} + + +static gboolean +msg_move (const char* src, const char *dst, GError **err) +{ + if (!msg_move_check_pre (src, dst, err)) + return FALSE; + + if (rename (src, dst) != 0) + return mu_util_g_set_error + (err, MU_ERROR_FILE,"error moving %s to %s", src, dst); + + return msg_move_check_post (src, dst, err); +} + +gchar* +mu_maildir_move_message (const char* oldpath, const char* targetmdir, + MuFlags newflags, gboolean ignore_dups, + gboolean new_name, GError **err) +{ + char *newfullpath; + gboolean rv; + gboolean src_is_target; + + g_return_val_if_fail (oldpath, FALSE); + + newfullpath = mu_maildir_get_new_path (oldpath, targetmdir, + newflags, new_name); + if (!newfullpath) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "failed to determine targetpath"); + return NULL; + } + + src_is_target = (g_strcmp0 (oldpath, newfullpath) == 0); + + if (!ignore_dups && src_is_target) { + mu_util_g_set_error (err, MU_ERROR_FILE_TARGET_EQUALS_SOURCE, + "target equals source"); + return NULL; + } + + if (!src_is_target) { + rv = msg_move (oldpath, newfullpath, err); + if (!rv) { + g_free (newfullpath); + return NULL; + } + } + + return newfullpath; +} diff --git a/lib/mu-maildir.h b/lib/mu-maildir.h new file mode 100644 index 0000000..790c345 --- /dev/null +++ b/lib/mu-maildir.h @@ -0,0 +1,223 @@ +/* +** Copyright (C) 2008-2015 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MAILDIR_H__ +#define __MU_MAILDIR_H__ + +#include <glib.h> +#include <time.h> +#include <sys/types.h> /* for mode_t */ +#include <utils/mu-util.h> +#include <mu-flags.h> + + +G_BEGIN_DECLS + +/** + * create a new maildir. if parts of the maildir already exists, those + * will simply be ignored. IOW, if you try to create the same maildir + * twice, the second will simply be a no-op (without any errors). + * Note, if the function fails 'halfway', it will *not* try to remove + * the parts the were created. it *will* create any parent dirs that + * are not yet existent. + * + * + * @param path the path (missing components will be created, as in 'mkdir -p') + * @param mode the file mode (e.g., 0755) + * @param noindex add a .noindex file to the maildir, so it will be excluded + * from indexing by 'mu index' + * @param err if function returns FALSE, receives error + * information. err may be NULL. + * + * @return TRUE if creation succeeded (or already existed), FALSE otherwise + */ +gboolean mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, + GError **err); + + +/** + * create a symbolic link to a mail message + * + * @param src the full path to the source message + * @param targetpath the path to the target maildir; ie., *not* + * MyMaildir/cur, but just MyMaildir/. The function will figure out + * the correct subdir then. + * @param err if function returns FALSE, err may contain extra + * information. if err is NULL, does nothing + * + * @return + */ +gboolean mu_maildir_link (const char* src, const char *targetpath, + GError **err); + +/** + * MuMaildirWalkMsgCallback -- callback function for + * mu_path_walk_maildir; see the documentation there. It will be + * called for each message found, with fullpath containing the full + * path to the message, mdir containing the maildir -- that is, when + * indexing ~/Maildir, a message ~/Maildir/foo/bar/cur/msg would have + * the maildir "foo/bar". Then, the information from 'stat' of this + * file (see stat(3)), and a user_data pointer + */ +typedef MuError (*MuMaildirWalkMsgCallback) + (const char* fullpath, const char* mdir, struct stat *statinfo, + void *user_data); + +/** + * MuPathWalkDirCallback -- callback function for mu_path_walk_maildir; see the + * documentation there. It will be called each time a dir is entered or left, + * with 'enter' being TRUE upon entering, FALSE otherwise + */ +typedef MuError (*MuMaildirWalkDirCallback) + (const char* fullpath, gboolean enter, void *user_data); + +/** + * start a recursive walk of a maildir; for each file found, we call + * callback with the path (with the Maildir path of scanner_new as + * root), the filename, the timestamp (mtime) of the file,and the + * *data pointer, for user data. dot-files are ignored, as well as + * files outside cur/ and new/ dirs and unreadable files; however, + * dotdirs are visited (ie. '.dotdir/cur'), so this enables Maildir++. + * (http://www.inter7.com/courierimap/README.maildirquota.html, search + * for 'Mission statement'). In addition, dirs containing a file named + * '.noindex' are ignored, as are their subdirectories, and dirs + * containing a file called '.noupdate' are ignored, unless @param + * full is TRUE. + * + * mu_walk_maildir stops if the callbacks return something different + * from MU_OK. For example, it can return MU_STOP to stop the scan, or + * some error. + * + * @param path the maildir path to scan + * @param cb_msg the callback function called for each msg + * @param cb_dir the callback function called for each dir + * @param full whether do a full scan, i.e., to ignore .noupdate files + * @param data user data pointer + * + * @return a scanner result; MU_OK if everything went ok, + * MU_STOP if we want to stop, or MU_ERROR in + * case of error + */ +MuError mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg, + MuMaildirWalkDirCallback cb_dir, gboolean full, + void *data); +/** + * recursively delete all the symbolic links in a directory tree + * + * @param dir top dir + * @param err if function returns FALSE, err may contain extra + * information. if err is NULL, does nothing + * + * @return TRUE if it worked, FALSE in case of error + */ +gboolean mu_maildir_clear_links (const gchar* dir, GError **err); + + + +/** + * whether the directory path ends in '/cur/' or '/new/' + * + * @param path some path + */ +gboolean mu_maildir_is_leaf_dir (const char *path); + + +/** + * get the Maildir flags from the full path of a mailfile. The flags + * are as specified in http://cr.yp.to/proto/maildir.html, plus + * MU_MSG_FLAG_NEW for new messages, ie the ones that live in + * new/. The flags are logically OR'ed. Note that the file does not + * have to exist; the flags are based on the path only. + * + * @param pathname of a mailfile; it does not have to refer to an + * actual message + * + * @return the flags, or MU_MSG_FILE_FLAG_UNKNOWN in case of error + */ +MuFlags mu_maildir_get_flags_from_path (const char* pathname); + +/** + * get the new pathname for a message, based on the old path and the + * new flags and (optionally) a new maildir. Note that + * setting/removing the MU_FLAG_NEW will change the directory in which + * a message lives. The flags are as specified in + * http://cr.yp.to/proto/maildir.html, plus MU_FLAG_NEW for new + * messages, ie the ones that live in new/. The flags are logically + * OR'ed. Note that the file does not have to exist; the flags are + * based on the path only. + * + * + * @param oldpath the old (current) full path to the message + * (including the filename) + * @param new_mdir the new maildir for this message, or NULL to keep + * it in the current one. The maildir is the absolute file system + * path, without the 'cur' or 'new' + * @param new_flags the new flags for this message + * @param new_name whether to create a new unique name, or keep the + * old one + * + * @return a new path name; use g_free when done with. NULL in case of + * error. + */ +char* mu_maildir_get_new_path (const char *oldpath, const char *new_mdir, + MuFlags new_flags, gboolean new_name) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * get the maildir for a certain message path, ie, the path *before* + * cur/ or new/ + * + * @param path path for some message + * + * @return the maildir (free with g_free), or NULL in case of error + */ +char* mu_maildir_get_maildir_from_path (const char* path) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * move a message file to another maildir; the function returns the full path to + * the new message. if the target file already exists, it is overwritten. + * + * @param msgpath an absolute file system path to an existing message in an + * actual maildir + * @param targetmdir the target maildir; note that this the base-level + * Maildir, ie. /home/user/Maildir/archive, and must _not_ include the + * 'cur' or 'new' part. Note that the target maildir must be on the + * same filesystem. If you specify NULL for targetmdir, only the flags + * of the message are affected; note that this may still involve a + * moved to another directory (say, from new/ to cur/) + * @param flags to set for the target (influences the filename, path) + * @param ignore_dups whether to silently ignore the src=target case + * (and return TRUE) + * @param new_name whether to create a new unique name, or keep the + * old one + * @param err receives error information + * + * @return return the full path name of the target file (g_free) if + * the move succeeded, NULL otherwise + */ +gchar* mu_maildir_move_message (const char* oldpath, const char* targetmdir, + MuFlags newflags, gboolean ignore_dups, + gboolean new_name, GError **err) + G_GNUC_WARN_UNUSED_RESULT; + +G_END_DECLS + +#endif /*__MU_MAILDIR_H__*/ diff --git a/lib/mu-msg-crypto.c b/lib/mu-msg-crypto.c new file mode 100644 index 0000000..c7ca8ba --- /dev/null +++ b/lib/mu-msg-crypto.c @@ -0,0 +1,374 @@ +/* +** Copyright (C) 2012-2018 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <string.h> + +#include "mu-msg.h" +#include "mu-msg-priv.h" +#include "mu-msg-part.h" +#include "utils/mu-date.h" + +#include <gmime/gmime.h> +#include <gmime/gmime-multipart-signed.h> + + +static const char* +get_pubkey_algo_name (GMimePubKeyAlgo algo) +{ + switch (algo) { + case GMIME_PUBKEY_ALGO_DEFAULT: + return "default"; + case GMIME_PUBKEY_ALGO_RSA: + return "RSA"; + case GMIME_PUBKEY_ALGO_RSA_E: + return "RSA (encryption only)"; + case GMIME_PUBKEY_ALGO_RSA_S: + return "RSA (signing only)"; + case GMIME_PUBKEY_ALGO_ELG_E: + return "ElGamal (encryption only)"; + case GMIME_PUBKEY_ALGO_DSA: + return "DSA"; + case GMIME_PUBKEY_ALGO_ELG: + return "ElGamal"; + default: + return "unknown pubkey algorithm"; + } +} + +static const gchar* +get_digestkey_algo_name (GMimeDigestAlgo algo) +{ + switch (algo) { + case GMIME_DIGEST_ALGO_DEFAULT: + return "default"; + case GMIME_DIGEST_ALGO_MD5: + return "MD5"; + case GMIME_DIGEST_ALGO_SHA1: + return "SHA-1"; + case GMIME_DIGEST_ALGO_RIPEMD160: + return "RIPEMD160"; + case GMIME_DIGEST_ALGO_MD2: + return "MD2"; + case GMIME_DIGEST_ALGO_TIGER192: + return "TIGER-192"; + case GMIME_DIGEST_ALGO_HAVAL5160: + return "HAVAL-5-160"; + case GMIME_DIGEST_ALGO_SHA256: + return "SHA-256"; + case GMIME_DIGEST_ALGO_SHA384: + return "SHA-384"; + case GMIME_DIGEST_ALGO_SHA512: + return "SHA-512"; + case GMIME_DIGEST_ALGO_SHA224: + return "SHA-224"; + case GMIME_DIGEST_ALGO_MD4: + return "MD4"; + default: + return "unknown digest algorithm"; + } +} + + +/* get data from the 'certificate' */ +static char* +get_cert_data (GMimeCertificate *cert) +{ + const char /**email,*/ *name, *digest_algo, *pubkey_algo, + *keyid, *trust; + + /* email = g_mime_certificate_get_email (cert); */ + name = g_mime_certificate_get_name (cert); + keyid = g_mime_certificate_get_key_id (cert); + + digest_algo = get_digestkey_algo_name + (g_mime_certificate_get_digest_algo (cert)); + pubkey_algo = get_pubkey_algo_name + (g_mime_certificate_get_pubkey_algo (cert)); + + switch (g_mime_certificate_get_trust (cert)) { + case GMIME_TRUST_UNKNOWN: trust = "unknown"; break; + case GMIME_TRUST_UNDEFINED: trust = "undefined"; break; + case GMIME_TRUST_NEVER: trust = "never"; break; + case GMIME_TRUST_MARGINAL: trust = "marginal"; break; + case GMIME_TRUST_FULL: trust = "full"; break; + case GMIME_TRUST_ULTIMATE: trust = "ultimate"; break; + default: + g_return_val_if_reached (NULL); + } + + return g_strdup_printf ( + "signer:%s, key:%s (%s,%s), trust:%s", + name ? name : "?", + /* email ? email : "?", */ + keyid, pubkey_algo, digest_algo, + trust); +} + + +static char* +get_signature_status (GMimeSignatureStatus status) +{ + size_t n; + GString *descr; + + struct { + GMimeSignatureStatus status; + const char *name; + } status_info[] = { + { GMIME_SIGNATURE_STATUS_VALID, "valid" }, + { GMIME_SIGNATURE_STATUS_GREEN, "green" }, + { GMIME_SIGNATURE_STATUS_RED, "red" }, + { GMIME_SIGNATURE_STATUS_KEY_REVOKED, "key revoked" }, + { GMIME_SIGNATURE_STATUS_KEY_EXPIRED, "key expired" }, + { GMIME_SIGNATURE_STATUS_SIG_EXPIRED, "signature expired" }, + { GMIME_SIGNATURE_STATUS_KEY_MISSING, "key missing" }, + { GMIME_SIGNATURE_STATUS_CRL_MISSING, "crl missing" }, + { GMIME_SIGNATURE_STATUS_CRL_TOO_OLD, "crl too old" }, + { GMIME_SIGNATURE_STATUS_BAD_POLICY, "bad policy" }, + { GMIME_SIGNATURE_STATUS_SYS_ERROR, "system error" }, + { GMIME_SIGNATURE_STATUS_TOFU_CONFLICT, "tofu conflict " }, + }; + + descr = g_string_new(""); + for (n = 0; n != G_N_ELEMENTS(status_info); ++n) { + + if (!(status & status_info[n].status)) + continue; + + g_string_append_printf (descr, "%s%s", + descr->len > 0 ? ", " : "", + status_info[n].name); + } + + return g_string_free (descr, FALSE); +} + + +/* get a human-readable report about the signature */ +static char* +get_verdict_report (GMimeSignature *msig) +{ + time_t t; + const char *created, *expires; + gchar *certdata, *report, *status; + GMimeSignatureStatus sigstat; + + sigstat = g_mime_signature_get_status (msig); + status = get_signature_status(sigstat); + + t = g_mime_signature_get_created (msig); + created = (t == 0 || t == (time_t)-1) ? "?" : mu_date_str_s ("%x", t); + + t = g_mime_signature_get_expires (msig); + expires = (t == 0 || t == (time_t)-1) ? "?" : mu_date_str_s ("%x", t); + + certdata = get_cert_data (g_mime_signature_get_certificate (msig)); + report = g_strdup_printf ("%s; created:%s, expires:%s, %s", + status, created, expires, + certdata ? certdata : "?"); + g_free (certdata); + g_free (status); + + return report; +} + + +static char* +get_signers (GHashTable *signerhash) +{ + GString *gstr; + GHashTableIter iter; + const char *name; + + if (!signerhash || g_hash_table_size(signerhash) == 0) + return NULL; + + gstr = g_string_new (NULL); + g_hash_table_iter_init (&iter, signerhash); + while (g_hash_table_iter_next (&iter, (gpointer)&name, NULL)) { + if (gstr->len != 0) + g_string_append_c (gstr, ','); + gstr = g_string_append (gstr, name); + } + + return g_string_free (gstr, FALSE); +} + + +static MuMsgPartSigStatusReport* +get_status_report (GMimeSignatureList *sigs) +{ + int i; + MuMsgPartSigStatus status; + MuMsgPartSigStatusReport *status_report; + char *report; + GHashTable *signerhash; + + status = MU_MSG_PART_SIG_STATUS_GOOD; /* let's start positive! */ + signerhash = g_hash_table_new (g_str_hash, g_str_equal); + + for (i = 0, report = NULL; i != g_mime_signature_list_length (sigs); + ++i) { + + GMimeSignature *msig; + GMimeCertificate *cert; + GMimeSignatureStatus sigstat; + gchar *rep; + + msig = g_mime_signature_list_get_signature (sigs, i); + sigstat = g_mime_signature_get_status (msig); + + /* downgrade our expectations */ + if ((sigstat & GMIME_SIGNATURE_STATUS_ERROR_MASK) && + status != MU_MSG_PART_SIG_STATUS_ERROR) + status = MU_MSG_PART_SIG_STATUS_ERROR; + else if ((sigstat & GMIME_SIGNATURE_STATUS_RED) && + status == MU_MSG_PART_SIG_STATUS_GOOD) + status = MU_MSG_PART_SIG_STATUS_BAD; + + rep = get_verdict_report (msig); + report = g_strdup_printf ("%s%s%d: %s", + report ? report : "", + report ? "; " : "", i + 1, + rep); + g_free (rep); + + cert = g_mime_signature_get_certificate (msig); + if (cert && g_mime_certificate_get_name (cert)) + g_hash_table_add ( + signerhash, + (gpointer)g_mime_certificate_get_name (cert)); + } + + status_report = g_slice_new0 (MuMsgPartSigStatusReport); + + status_report->verdict = status; + status_report->report = report; + status_report->signers = get_signers(signerhash); + + g_hash_table_unref (signerhash); + + return status_report; +} + +void +mu_msg_part_sig_status_report_destroy (MuMsgPartSigStatusReport *report) +{ + if (!report) + return; + + g_free ((char*)report->report); + g_free ((char*)report->signers); + + g_slice_free (MuMsgPartSigStatusReport, report); +} + + +static inline void +tag_with_sig_status(GObject *part, + MuMsgPartSigStatusReport *report) +{ + g_object_set_data_full + (part, SIG_STATUS_REPORT, report, + (GDestroyNotify)mu_msg_part_sig_status_report_destroy); +} + + +void +mu_msg_crypto_verify_part (GMimeMultipartSigned *sig, MuMsgOptions opts, + GError **err) +{ + /* the signature status */ + MuMsgPartSigStatusReport *report; + GMimeSignatureList *sigs; + + g_return_if_fail (GMIME_IS_MULTIPART_SIGNED(sig)); + + sigs = g_mime_multipart_signed_verify (sig, GMIME_VERIFY_NONE, err); + if (!sigs) { + if (err && !*err) + mu_util_g_set_error (err, MU_ERROR_CRYPTO, + "verification failed"); + return; + } + + report = get_status_report (sigs); + g_clear_object (&sigs); + + /* tag this part with the signature status check */ + tag_with_sig_status(G_OBJECT(sig), report); +} + + +static inline void +check_decrypt_result(GMimeMultipartEncrypted *part, GMimeDecryptResult *res, + GError **err) +{ + GMimeSignatureList *sigs; + MuMsgPartSigStatusReport *report; + + if (res) { + /* Check if the decrypted part had any embed signatures */ + sigs = res->signatures; + if (sigs) { + report = get_status_report (sigs); + g_mime_signature_list_clear (sigs); + + /* tag this part with the signature status check */ + tag_with_sig_status(G_OBJECT(part), report); + } + else { + if (err && !*err) + mu_util_g_set_error (err, MU_ERROR_CRYPTO, + "verification failed"); + } + g_object_unref (res); + } + +} + + +GMimeObject* /* this is declared in mu-msg-priv.h */ +mu_msg_crypto_decrypt_part (GMimeMultipartEncrypted *enc, MuMsgOptions opts, + MuMsgPartPasswordFunc func, gpointer user_data, + GError **err) +{ + GMimeObject *dec; + GMimeDecryptResult *res; + + g_return_val_if_fail (GMIME_IS_MULTIPART_ENCRYPTED(enc), NULL); + + res = NULL; + dec = g_mime_multipart_encrypted_decrypt (enc, GMIME_DECRYPT_NONE, NULL, + &res, err); + check_decrypt_result(enc, res, err); + + if (!dec) { + if (err && !*err) + mu_util_g_set_error (err, MU_ERROR_CRYPTO, + "decryption failed"); + return NULL; + } + + return dec; +} diff --git a/lib/mu-msg-doc.cc b/lib/mu-msg-doc.cc new file mode 100644 index 0000000..21c3c8b --- /dev/null +++ b/lib/mu-msg-doc.cc @@ -0,0 +1,124 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <stdlib.h> +#include <iostream> +#include <string.h> +#include <errno.h> +#include <xapian.h> + +#include "mu-msg-fields.h" +#include "mu-msg-doc.h" + +#include "utils/mu-util.h" +#include "utils/mu-str.h" +#include "utils/mu-date.h" +#include "utils/mu-utils.hh" + +struct _MuMsgDoc { + + _MuMsgDoc (Xapian::Document *doc): _doc (doc) { } + ~_MuMsgDoc () { delete _doc; } + const Xapian::Document doc() const { return *_doc; } +private: + Xapian::Document *_doc; +}; + + +MuMsgDoc* +mu_msg_doc_new (XapianDocument *doc, GError **err) +{ + g_return_val_if_fail (doc, NULL); + + try { + return new MuMsgDoc ((Xapian::Document*)doc); + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, NULL); + + return FALSE; +} + +void +mu_msg_doc_destroy (MuMsgDoc *self) +{ + try { + delete self; + + } MU_XAPIAN_CATCH_BLOCK; +} + + +gchar* +mu_msg_doc_get_str_field (MuMsgDoc *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL); + + // disable this check: + // g_return_val_if_fail (mu_msg_field_is_string(mfid), NULL); + // because it's useful to get numerical field as strings, + // for example when sorting (which is much faster if don't + // have to convert to numbers first, esp. when it's a date + // time_t) + + try { + const std::string s (self->doc().get_value(mfid)); + return s.empty() ? NULL : g_strdup (s.c_str()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL); +} + + +GSList* +mu_msg_doc_get_str_list_field (MuMsgDoc *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), NULL); + g_return_val_if_fail (mu_msg_field_is_string_list(mfid), NULL); + + try { + /* return a comma-separated string as a GSList */ + const std::string s (self->doc().get_value(mfid)); + return s.empty() ? NULL : mu_str_to_list(s.c_str(),',',TRUE); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL); +} + + +gint64 +mu_msg_doc_get_num_field (MuMsgDoc *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, -1); + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), -1); + g_return_val_if_fail (mu_msg_field_is_numeric(mfid), -1); + + try { + const std::string s (self->doc().get_value(mfid)); + if (s.empty()) + return 0; + else if (mfid == MU_MSG_FIELD_ID_DATE || + mfid == MU_MSG_FIELD_ID_SIZE) + return strtol (s.c_str(), NULL, 10); + else { + return static_cast<gint64> + (Xapian::sortable_unserialise(s)); + } + + } MU_XAPIAN_CATCH_BLOCK_RETURN(-1); +} diff --git a/lib/mu-msg-doc.h b/lib/mu-msg-doc.h new file mode 100644 index 0000000..2f5fe5c --- /dev/null +++ b/lib/mu-msg-doc.h @@ -0,0 +1,96 @@ +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_DOC_H__ +#define __MU_MSG_DOC_H__ + +#include <glib.h> +#include <utils/mu-util.h> + +G_BEGIN_DECLS + +struct _MuMsgDoc; +typedef struct _MuMsgDoc MuMsgDoc; + +/** + * create a new MuMsgDoc instance + * + * @param doc a Xapian::Document* (you'll need to cast the + * Xapian::Document* to XapianDocument*, because only C (not C++) is + * allowed in this header file. MuMsgDoc takes _ownership_ of this pointer; + * don't touch it afterwards + * @param err receives error info, or NULL + * + * @return a new MuMsgDoc instance (free with mu_msg_doc_destroy), or + * NULL in case of error. + */ +MuMsgDoc* mu_msg_doc_new (XapianDocument *doc, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * destroy a MuMsgDoc instance -- free all the resources. Note, after + * destroying, any strings returned from mu_msg_doc_get_str_field with + * do_free==FALSE are no longer valid + * + * @param self a MuMsgDoc instance + */ +void mu_msg_doc_destroy (MuMsgDoc *self); + + +/** + * get a string parameter from the msgdoc + * + * @param self a MuMsgDoc instance + * @param mfid a MuMsgFieldId for a string field + * + * @return a string for the given field (see do_free), or NULL in case of error. + * free with g_free + */ +gchar* mu_msg_doc_get_str_field (MuMsgDoc *self, MuMsgFieldId mfid) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * get a string-list parameter from the msgdoc + * + * @param self a MuMsgDoc instance + * @param mfid a MuMsgFieldId for a string-list field + * + * @return a list for the given field (see do_free), or NULL in case + * of error. free with mu_str_free_list + */ +GSList* mu_msg_doc_get_str_list_field (MuMsgDoc *self, MuMsgFieldId mfid) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * + * get a numeric parameter from the msgdoc + * + * @param self a MuMsgDoc instance + * @param mfid a MuMsgFieldId for a numeric field + * + * @return the numerical value, or -1 in case of error. You'll need to + * cast this value to the actual type (e.g. time_t for MU_MSG_FIELD_ID_DATE) + */ +gint64 mu_msg_doc_get_num_field (MuMsgDoc *self, MuMsgFieldId mfid); + + +G_END_DECLS + +#endif /*__MU_MSG_DOC_H__*/ diff --git a/lib/mu-msg-fields.c b/lib/mu-msg-fields.c new file mode 100644 index 0000000..daf3c2f --- /dev/null +++ b/lib/mu-msg-fields.c @@ -0,0 +1,425 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> +#include "mu-msg-fields.h" + +/* + * note: the differences for our purposes between a xapian field and a + * term: - there is only a single value for some item in per document + * (msg), ie. one value containing the list of To: addresses - there + * can be multiple terms, each containing e.g. one of the To: + * addresses - searching uses terms, but to display some field, it + * must be in the value (at least when using MuMsgIter) + */ +enum _FieldFlags { + FLAG_GMIME = 1 << 0, /* field retrieved through + * gmime */ + FLAG_XAPIAN_INDEX = 1 << 1, /* field is indexed in + * xapian (i.e., the text + * is processed */ + FLAG_XAPIAN_TERM = 1 << 2, /* field stored as term in + * xapian (so it can be searched) */ + FLAG_XAPIAN_VALUE = 1 << 3, /* field stored as value in + * xapian (so the literal + * value can be + * retrieved) */ + FLAG_XAPIAN_CONTACT = 1 << 4, /* field contains one or more + * e-mail-addresses */ + FLAG_XAPIAN_BOOLEAN = 1 << 5, /* use 'add_boolean_prefix' + * for Xapian queries; + * wildcards do NOT WORK + * for such fields */ + FLAG_DONT_CACHE = 1 << 6, /* don't cache this field in + * the MuMsg cache */ + FLAG_RANGE_FIELD = 1 << 7 /* whether this is a range field */ + +}; +typedef enum _FieldFlags FieldFlags; + +/* + * this struct describes the fields of an e-mail + /*/ +struct _MuMsgField { + MuMsgFieldId _id; /* the id of the field */ + MuMsgFieldType _type; /* the type of the field */ + const char *_name; /* the name of the field */ + const char _shortcut; /* the shortcut for use in + * --fields and sorting */ + const char _xprefix; /* the Xapian-prefix */ + FieldFlags _flags; /* the flags that tells us + * what to do */ +}; +typedef struct _MuMsgField MuMsgField; + +/* the name and shortcut fields must be lower case, or they might be + * misinterpreted by the query-preprocesser which turns queries into + * lowercase */ +static const MuMsgField FIELD_DATA[] = { + + { + MU_MSG_FIELD_ID_BCC, + MU_MSG_FIELD_TYPE_STRING, + "bcc" , 'h', 'H', /* 'hidden */ + FLAG_GMIME | FLAG_XAPIAN_CONTACT | + FLAG_XAPIAN_VALUE + }, + + { + MU_MSG_FIELD_ID_BODY_TEXT, + MU_MSG_FIELD_TYPE_STRING, + "body", 'b', 'B', + FLAG_GMIME | FLAG_XAPIAN_INDEX | + FLAG_DONT_CACHE + }, + + { + MU_MSG_FIELD_ID_BODY_HTML, + MU_MSG_FIELD_TYPE_STRING, + "bodyhtml", 0, 0, + FLAG_GMIME | FLAG_DONT_CACHE + }, + + { + MU_MSG_FIELD_ID_CC, + MU_MSG_FIELD_TYPE_STRING, + "cc", 'c', 'C', + FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE + }, + + { + MU_MSG_FIELD_ID_DATE, + MU_MSG_FIELD_TYPE_TIME_T, + "date", 'd', 'D', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE | + FLAG_XAPIAN_BOOLEAN | FLAG_RANGE_FIELD + }, + + { + MU_MSG_FIELD_ID_EMBEDDED_TEXT, + MU_MSG_FIELD_TYPE_STRING, + "embed", 'e', 'E', + FLAG_GMIME | FLAG_XAPIAN_INDEX | FLAG_DONT_CACHE + }, + + { + MU_MSG_FIELD_ID_FILE, + MU_MSG_FIELD_TYPE_STRING, + "file" , 'j', 'J', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_DONT_CACHE + }, + + + { + MU_MSG_FIELD_ID_FLAGS, + MU_MSG_FIELD_TYPE_INT, + "flag", 'g', 'G', /* flaGs */ + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + { + MU_MSG_FIELD_ID_FROM, + MU_MSG_FIELD_TYPE_STRING, + "from", 'f', 'F', + FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_MAILDIR, + MU_MSG_FIELD_TYPE_STRING, + "maildir", 'm', 'M', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_MAILING_LIST, + MU_MSG_FIELD_TYPE_STRING, + "list", 'v', 'V', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_MIME, + MU_MSG_FIELD_TYPE_STRING, + "mime" , 'y', 'Y', + FLAG_XAPIAN_TERM + }, + + + { + MU_MSG_FIELD_ID_MSGID, + MU_MSG_FIELD_TYPE_STRING, + "msgid", 'i', 'I', /* 'i' for Id */ + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_PATH, + MU_MSG_FIELD_TYPE_STRING, + "path", 'l', 'L', /* 'l' for location */ + FLAG_GMIME | FLAG_XAPIAN_VALUE | + FLAG_XAPIAN_BOOLEAN + }, + + + { + MU_MSG_FIELD_ID_PRIO, + MU_MSG_FIELD_TYPE_INT, + "prio", 'p', 'P', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_REFS, + MU_MSG_FIELD_TYPE_STRING_LIST, + "refs", 'r', 'R', + FLAG_GMIME | FLAG_XAPIAN_VALUE + }, + + + { + MU_MSG_FIELD_ID_SIZE, + MU_MSG_FIELD_TYPE_BYTESIZE, + "size", 'z', 'Z', /* siZe */ + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE | + FLAG_RANGE_FIELD + }, + + { + MU_MSG_FIELD_ID_SUBJECT, + MU_MSG_FIELD_TYPE_STRING, + "subject", 's', 'S', + FLAG_GMIME | FLAG_XAPIAN_INDEX | FLAG_XAPIAN_VALUE | + FLAG_XAPIAN_TERM + }, + + { + MU_MSG_FIELD_ID_TAGS, + MU_MSG_FIELD_TYPE_STRING_LIST, + "tag", 'x', 'X', + FLAG_GMIME | FLAG_XAPIAN_TERM | FLAG_XAPIAN_VALUE + }, + + + { /* remember which thread this message is in */ + MU_MSG_FIELD_ID_THREAD_ID, + MU_MSG_FIELD_TYPE_STRING, + "thread", 0, 'W', + FLAG_XAPIAN_TERM + }, + + { + MU_MSG_FIELD_ID_TO, + MU_MSG_FIELD_TYPE_STRING, + "to", 't', 'T', + FLAG_GMIME | FLAG_XAPIAN_CONTACT | FLAG_XAPIAN_VALUE + }, + + { /* special, internal field, to get a unique key */ + MU_MSG_FIELD_ID_UID, + MU_MSG_FIELD_TYPE_STRING, + "uid", 0, 'U', + FLAG_XAPIAN_TERM + } + + /* note, mu-store also use the 'Q' internal prefix for its uids */ +}; + +/* the MsgField data in an array, indexed by the MsgFieldId; + * this allows for O(1) access + */ +static MuMsgField* _msg_field_data[MU_MSG_FIELD_ID_NUM]; +static const MuMsgField* mu_msg_field (MuMsgFieldId id) +{ + static gboolean _initialized = FALSE; + + /* initialize the array, but only once... */ + if (G_UNLIKELY(!_initialized)) { + int i; + for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i) + _msg_field_data[FIELD_DATA[i]._id] = + (MuMsgField*)&FIELD_DATA[i]; + _initialized = TRUE; + } + + return _msg_field_data[id]; +} + + +void +mu_msg_field_foreach (MuMsgFieldForeachFunc func, gconstpointer data) +{ + int i; + for (i = 0; i != MU_MSG_FIELD_ID_NUM; ++i) + func (i, data); +} + + +MuMsgFieldId +mu_msg_field_id_from_name (const char* str, gboolean err) +{ + int i; + + g_return_val_if_fail (str, MU_MSG_FIELD_ID_NONE); + + for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i) + if (g_strcmp0(str, FIELD_DATA[i]._name) == 0) + return FIELD_DATA[i]._id; + if (err) + g_return_val_if_reached (MU_MSG_FIELD_ID_NONE); + + return MU_MSG_FIELD_ID_NONE; +} + + +MuMsgFieldId +mu_msg_field_id_from_shortcut (char kar, gboolean err) +{ + int i; + for (i = 0; i != G_N_ELEMENTS(FIELD_DATA); ++i) + if (kar == FIELD_DATA[i]._shortcut) + return FIELD_DATA[i]._id; + + if (err) + g_return_val_if_reached (MU_MSG_FIELD_ID_NONE); + + return MU_MSG_FIELD_ID_NONE; +} + + +gboolean +mu_msg_field_gmime (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_GMIME ? TRUE: FALSE; +} + + +gboolean +mu_msg_field_xapian_index (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & + (FLAG_XAPIAN_INDEX | FLAG_XAPIAN_CONTACT) ? TRUE: FALSE; +} + +gboolean +mu_msg_field_xapian_value (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_XAPIAN_VALUE ? TRUE: FALSE; +} + +gboolean +mu_msg_field_xapian_term (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_XAPIAN_TERM ? TRUE: FALSE; +} + + +gboolean +mu_msg_field_is_range_field (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_RANGE_FIELD ? TRUE: FALSE; +} + + + +gboolean +mu_msg_field_uses_boolean_prefix (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_XAPIAN_BOOLEAN ? TRUE:FALSE; +} + + + +gboolean +mu_msg_field_is_cacheable (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + /* note the FALSE: TRUE */ + return mu_msg_field(id)->_flags & FLAG_DONT_CACHE ? FALSE : TRUE; +} + +gboolean +mu_msg_field_xapian_contact (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),FALSE); + return mu_msg_field(id)->_flags & FLAG_XAPIAN_CONTACT ? TRUE: FALSE; +} + + + +gboolean +mu_msg_field_is_numeric (MuMsgFieldId mfid) +{ + MuMsgFieldType type; + + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid),FALSE); + + type = mu_msg_field_type (mfid); + + return type == MU_MSG_FIELD_TYPE_BYTESIZE || + type == MU_MSG_FIELD_TYPE_TIME_T || + type == MU_MSG_FIELD_TYPE_INT; +} + +const char* +mu_msg_field_name (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),NULL); + return mu_msg_field(id)->_name; +} + + +char +mu_msg_field_shortcut (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),0); + return mu_msg_field(id)->_shortcut; +} + + +char +mu_msg_field_xapian_prefix (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id),0); + return mu_msg_field(id)->_xprefix; +} + + + + +MuMsgFieldType +mu_msg_field_type (MuMsgFieldId id) +{ + g_return_val_if_fail (mu_msg_field_id_is_valid(id), + MU_MSG_FIELD_TYPE_NONE); + return mu_msg_field(id)->_type; +} diff --git a/lib/mu-msg-fields.h b/lib/mu-msg-fields.h new file mode 100644 index 0000000..08bfe60 --- /dev/null +++ b/lib/mu-msg-fields.h @@ -0,0 +1,292 @@ +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_FIELDS_H__ +#define __MU_MSG_FIELDS_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +/* don't change the order, add new types at the end, as these numbers + * are used in the database */ +enum _MuMsgFieldId { + + /* first all the string-based ones */ + MU_MSG_FIELD_ID_BCC = 0, + MU_MSG_FIELD_ID_BODY_HTML, + MU_MSG_FIELD_ID_BODY_TEXT, + MU_MSG_FIELD_ID_CC, + MU_MSG_FIELD_ID_EMBEDDED_TEXT, + MU_MSG_FIELD_ID_FILE, + MU_MSG_FIELD_ID_FROM, + MU_MSG_FIELD_ID_MAILDIR, + MU_MSG_FIELD_ID_MIME, /* mime-type */ + MU_MSG_FIELD_ID_MSGID, + MU_MSG_FIELD_ID_PATH, + MU_MSG_FIELD_ID_SUBJECT, + MU_MSG_FIELD_ID_TO, + + MU_MSG_FIELD_ID_UID, /* special, generated from path */ + + /* string list items... */ + MU_MSG_FIELD_ID_REFS, + MU_MSG_FIELD_ID_TAGS, + + /* then the numerical ones */ + MU_MSG_FIELD_ID_DATE, + MU_MSG_FIELD_ID_FLAGS, + MU_MSG_FIELD_ID_PRIO, + MU_MSG_FIELD_ID_SIZE, + + /* add new ones here... */ + MU_MSG_FIELD_ID_MAILING_LIST, /* mailing list */ + MU_MSG_FIELD_ID_THREAD_ID, + + MU_MSG_FIELD_ID_NUM +}; +typedef guint8 MuMsgFieldId; + +/* some specials... */ +static const MuMsgFieldId MU_MSG_FIELD_ID_NONE = (MuMsgFieldId)-1; +#define MU_MSG_STRING_FIELD_ID_NUM (MU_MSG_FIELD_ID_UID + 1) + +/* this is a shortcut for To/From/Cc/Bcc in queries; handled specially + * in mu-query.cc and mu-str.c */ +#define MU_MSG_FIELD_PSEUDO_CONTACT "contact" + +/* this is a shortcut for To/Cc/Bcc in queries; handled specially in + * mu-query.cc and mu-str.c */ +#define MU_MSG_FIELD_PSEUDO_RECIP "recip" + +#define mu_msg_field_id_is_valid(MFID) \ + ((MFID) < MU_MSG_FIELD_ID_NUM) + +/* don't change the order, add new types at the end (before _NUM)*/ +enum _MuMsgFieldType { + MU_MSG_FIELD_TYPE_STRING, + MU_MSG_FIELD_TYPE_STRING_LIST, + + MU_MSG_FIELD_TYPE_BYTESIZE, + MU_MSG_FIELD_TYPE_TIME_T, + MU_MSG_FIELD_TYPE_INT, + + MU_MSG_FIELD_TYPE_NUM +}; +typedef guint8 MuMsgFieldType; +static const MuMsgFieldType MU_MSG_FIELD_TYPE_NONE = (MuMsgFieldType)-1; + +typedef void (*MuMsgFieldForeachFunc) (MuMsgFieldId id, + gconstpointer data); + +/** + * iterator over all possible message fields + * + * @param func a function called for each field + * @param data a user data pointer passed the callback function + */ +void mu_msg_field_foreach (MuMsgFieldForeachFunc func, gconstpointer data); + + +/** + * get the name of the field -- this a name that can be use in queries, + * ie. 'subject:foo', with 'subject' being the name + * + * @param id a MuMsgFieldId + * + * @return the name of the field as a constant string, or + * NULL if the field is unknown + */ +const char* mu_msg_field_name (MuMsgFieldId id) G_GNUC_PURE; + +/** + * get the shortcut of the field -- this a shortcut that can be use in + * queries, ie. 's:foo', with 's' meaning 'subject' being the name + * + * @param id a MuMsgFieldId + * + * @return the shortcut character, or 0 if the field is unknown + */ +char mu_msg_field_shortcut (MuMsgFieldId id) G_GNUC_PURE; + +/** + * get the xapian prefix of the field -- that is, the prefix used in + * the Xapian database to identify the field + * + * @param id a MuMsgFieldId + * + * @return the xapian prefix char or 0 if the field is unknown + */ +char mu_msg_field_xapian_prefix (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * get the type of the field (string, size, time etc.) + * + * @param field a MuMsgField + * + * @return the type of the field (a #MuMsgFieldType), or + * MU_MSG_FIELD_TYPE_NONE if it is not found + */ +MuMsgFieldType mu_msg_field_type (MuMsgFieldId id) G_GNUC_PURE; + + + +/** + * is the field a string? + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field a string, FALSE otherwise + */ +#define mu_msg_field_is_string(MFID)\ + (mu_msg_field_type((MFID))==MU_MSG_FIELD_TYPE_STRING?TRUE:FALSE) + + + +/** + * is the field a string-list? + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field a string-list, FALSE otherwise + */ +#define mu_msg_field_is_string_list(MFID)\ + (mu_msg_field_type((MFID))==MU_MSG_FIELD_TYPE_STRING_LIST?TRUE:FALSE) + +/** + * is the field numeric (has type MU_MSG_FIELD_TYPE_(BYTESIZE|TIME_T|INT))? + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field is numeric, FALSE otherwise + */ +gboolean mu_msg_field_is_numeric (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * whether the field value should be cached (in MuMsg) -- we cache + * values so we can use the MuMsg without needing to keep the + * underlying data source (the GMimeMessage or the database ptr) alive + * in practice, the fields we *don't* cache are the message body + * (html, txt), because they take too much memory + */ +gboolean mu_msg_field_is_cacheable (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * is the field Xapian-indexable? That is, should this field be + * indexed in the Xapian database, so we can use the all the + * phrasing, stemming etc. magic + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field is Xapian-enabled, FALSE otherwise + */ +gboolean mu_msg_field_xapian_index (MuMsgFieldId id) G_GNUC_PURE; + +/** + * should this field be stored as a xapian term? + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field is Xapian-enabled, FALSE otherwise + */ +gboolean mu_msg_field_xapian_term (MuMsgFieldId id) G_GNUC_PURE; + +/** + * should this field be stored as a xapian value? + * + * @param field a MuMsgField + * + * @return TRUE if the field is Xapian-enabled, FALSE otherwise + */ +gboolean mu_msg_field_xapian_value (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * whether we should use add_boolean_prefix (see Xapian documentation) + * for this field in queries. Used in mu-query.cc + * + * @param id a MuMsgFieldId + * + * @return TRUE if this field wants add_boolean_prefix, FALSE + * otherwise + */ +gboolean mu_msg_field_uses_boolean_prefix (MuMsgFieldId id) G_GNUC_PURE; + +/** + * is this a range-field? ie. date, or size + * + * @param id a MuMsgField + * + * @return TRUE if this field is a range field, FALSE otherwise + */ +gboolean mu_msg_field_is_range_field (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * should this field be stored as contact information? This means that + * e-mail address will be stored as terms, and names will be indexed + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field should be stored as contact information, + * FALSE otherwise + */ +gboolean mu_msg_field_xapian_contact (MuMsgFieldId id) G_GNUC_PURE; + +/** + * is the field gmime-enabled? That is, can be field be retrieved + * using GMime? + * + * @param id a MuMsgFieldId + * + * @return TRUE if the field is Gmime-enabled, FALSE otherwise + */ +gboolean mu_msg_field_gmime (MuMsgFieldId id) G_GNUC_PURE; + + +/** + * get the corresponding MuMsgField for a name (as in mu_msg_field_name) + * + * @param str a name + * @param err, if TRUE, when the shortcut is not found, will issue a + * g_critical warning + * + * @return a MuMsgField, or NULL if it could not be found + */ +MuMsgFieldId mu_msg_field_id_from_name (const char* str, + gboolean err) G_GNUC_PURE; + + +/** + * get the corresponding MuMsgField for a shortcut (as in mu_msg_field_shortcut) + * + * @param kar a shortcut character + * @param err, if TRUE, when the shortcut is not found, will issue a + * g_critical warning + * + * @return a MuMsgField, or NULL if it could not be found + */ +MuMsgFieldId mu_msg_field_id_from_shortcut (char kar, + gboolean err) G_GNUC_PURE; +G_END_DECLS + +#endif /*__MU_MSG_FIELDS_H__*/ diff --git a/lib/mu-msg-file.c b/lib/mu-msg-file.c new file mode 100644 index 0000000..f0fa306 --- /dev/null +++ b/lib/mu-msg-file.c @@ -0,0 +1,815 @@ +/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <stdlib.h> +#include <ctype.h> +#include <inttypes.h> + +#include <gmime/gmime.h> +#include "mu-maildir.h" +#include "mu-store.hh" +#include "mu-msg-priv.h" + +#include "utils/mu-util.h" +#include "utils/mu-str.h" + +static gboolean init_file_metadata (MuMsgFile *self, const char* path, + const char *mdir, GError **err); +static gboolean init_mime_msg (MuMsgFile *msg, const char *path, GError **err); + +MuMsgFile* +mu_msg_file_new (const char* filepath, const char *mdir, GError **err) +{ + MuMsgFile *self; + + g_return_val_if_fail (filepath, NULL); + + self = g_slice_new0 (MuMsgFile); + + if (!init_file_metadata (self, filepath, mdir, err)) { + mu_msg_file_destroy (self); + return NULL; + } + + if (!init_mime_msg (self, filepath, err)) { + mu_msg_file_destroy (self); + return NULL; + } + + return self; +} + +void +mu_msg_file_destroy (MuMsgFile *self) +{ + if (!self) + return; + + if (self->_mime_msg) + g_object_unref (self->_mime_msg); + + g_slice_free (MuMsgFile, self); +} + +static gboolean +init_file_metadata (MuMsgFile *self, const char* path, const gchar* mdir, + GError **err) +{ + struct stat statbuf; + + if (access (path, R_OK) != 0) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "cannot read file %s: %s", + path, strerror(errno)); + return FALSE; + } + + if (stat (path, &statbuf) < 0) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "cannot stat %s: %s", + path, strerror(errno)); + return FALSE; + } + + if (!S_ISREG(statbuf.st_mode)) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "not a regular file: %s", path); + return FALSE; + } + + self->_timestamp = statbuf.st_mtime; + self->_size = (size_t)statbuf.st_size; + + /* remove double slashes, relative paths etc. from path & mdir */ + if (!realpath (path, self->_path)) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "could not get realpath for %s: %s", + path, strerror(errno)); + return FALSE; + } + + strncpy (self->_maildir, mdir ? mdir : "", PATH_MAX); + return TRUE; +} + +static GMimeStream* +get_mime_stream (MuMsgFile *self, const char *path, GError **err) +{ + FILE *file; + GMimeStream *stream; + + file = fopen (path, "r"); + if (!file) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, + "cannot open %s: %s", + path, strerror (errno)); + return NULL; + } + + stream = g_mime_stream_file_new (file); + if (!stream) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "cannot create mime stream for %s", + path); + fclose (file); + return NULL; + } + + return stream; +} + +static gboolean +init_mime_msg (MuMsgFile *self, const char* path, GError **err) +{ + GMimeStream *stream; + GMimeParser *parser; + + stream = get_mime_stream (self, path, err); + if (!stream) + return FALSE; + + parser = g_mime_parser_new_with_stream (stream); + g_object_unref (stream); + if (!parser) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "cannot create mime parser for %s", path); + return FALSE; + } + + self->_mime_msg = g_mime_parser_construct_message (parser, NULL); + g_object_unref (parser); + if (!self->_mime_msg) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "message seems invalid, ignoring (%s)", path); + return FALSE; + } + + return TRUE; +} + +static char* +get_recipient (MuMsgFile *self, GMimeAddressType atype) +{ + char *recip; + InternetAddressList *recips; + + recips = g_mime_message_get_addresses (self->_mime_msg, atype); + + /* FALSE --> don't encode */ + recip = (char*)internet_address_list_to_string (recips, NULL, FALSE); + + if (recip && !g_utf8_validate (recip, -1, NULL)) { + g_debug ("invalid recipient in %s\n", self->_path); + mu_str_asciify_in_place (recip); /* ugly... */ + } + + if (mu_str_is_empty(recip)) { + g_free (recip); + return NULL; + } + + if (recip) + mu_str_remove_ctrl_in_place (recip); + + return recip; +} + +/* + * let's try to guess the mailing list from some other + * headers in the mail + */ +static gchar* +get_fake_mailing_list_maybe (MuMsgFile *self) +{ + const char* hdr; + + hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg), + "X-Feed2Imap-Version"); + if (!hdr) + return NULL; + + /* looks like a feed2imap header; guess the source-blog + * from the msgid */ + { + const char *msgid, *e; + msgid = g_mime_message_get_message_id (self->_mime_msg); + if (msgid && (e = strchr (msgid, '-'))) + return g_strndup (msgid, e - msgid); + } + + return NULL; +} + +static gchar* +get_mailing_list (MuMsgFile *self) +{ + char *dechdr, *res; + const char *hdr, *b, *e; + + hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg), + "List-Id"); + if (mu_str_is_empty (hdr)) + return get_fake_mailing_list_maybe (self); + + dechdr = g_mime_utils_header_decode_phrase (NULL, hdr); + if (!dechdr) + return NULL; + + e = NULL; + b = strchr (dechdr, '<'); + if (b) + e = strchr (b, '>'); + + if (b && e) + res = g_strndup (b + 1, e - b - 1); + else + res = g_strdup (dechdr); + + g_free (dechdr); + + return res; +} + +static gboolean +looks_like_attachment (GMimeObject *part) +{ + + GMimeContentDisposition *disp; + GMimeContentType *ctype; + const char *dispstr; + guint u; + const struct { + const char *type; + const char *sub_type; + } att_types[] = { + { "image", "*" }, + { "audio", "*" }, + { "application", "*"}, + { "application", "x-patch"} + }; + + disp = g_mime_object_get_content_disposition (part); + + if (!GMIME_IS_CONTENT_DISPOSITION(disp)) + return FALSE; + + dispstr = g_mime_content_disposition_get_disposition (disp); + + if (g_ascii_strcasecmp (dispstr, "attachment") == 0) + return TRUE; + + /* we also consider patches, images, audio, and non-pgp-signature + * application attachments to be attachments... */ + ctype = g_mime_object_get_content_type (part); + + if (g_mime_content_type_is_type (ctype, "*", "pgp-signature")) + return FALSE; /* don't consider as a signature */ + + if (g_mime_content_type_is_type (ctype, "text", "*")) { + if (g_mime_content_type_is_type (ctype, "*", "plain") || + g_mime_content_type_is_type (ctype, "*", "html")) + return FALSE; + else + return TRUE; + } + + for (u = 0; u != G_N_ELEMENTS(att_types); ++u) + if (g_mime_content_type_is_type ( + ctype, att_types[u].type, att_types[u].sub_type)) + return TRUE; + + return FALSE; +} + +static void +msg_cflags_cb (GMimeObject *parent, GMimeObject *part, MuFlags *flags) +{ + if (GMIME_IS_MULTIPART_SIGNED(part)) + *flags |= MU_FLAG_SIGNED; + + /* FIXME: An encrypted part might be signed at the same time. + * In that case the signed flag is lost. */ + if (GMIME_IS_MULTIPART_ENCRYPTED(part)) + *flags |= MU_FLAG_ENCRYPTED; + + if (*flags & MU_FLAG_HAS_ATTACH) + return; + + if (!GMIME_IS_PART(part)) + return; + + if (*flags & MU_FLAG_HAS_ATTACH) + return; + + if (looks_like_attachment (part)) + *flags |= MU_FLAG_HAS_ATTACH; +} + +static MuFlags +get_content_flags (MuMsgFile *self) +{ + MuFlags flags; + char *ml; + + flags = MU_FLAG_NONE; + + if (GMIME_IS_MESSAGE(self->_mime_msg)) + mu_mime_message_foreach (self->_mime_msg, + FALSE, /* never decrypt for this */ + (GMimeObjectForeachFunc)msg_cflags_cb, + &flags); + + ml = get_mailing_list (self); + if (ml) { + flags |= MU_FLAG_LIST; + g_free (ml); + } + + return flags; +} + +static MuFlags +get_flags (MuMsgFile *self) +{ + MuFlags flags; + + g_return_val_if_fail (self, MU_FLAG_INVALID); + + flags = mu_maildir_get_flags_from_path (self->_path); + flags |= get_content_flags (self); + + /* pseudo-flag --> unread means either NEW or NOT SEEN, just + * for searching convenience */ + if ((flags & MU_FLAG_NEW) || !(flags & MU_FLAG_SEEN)) + flags |= MU_FLAG_UNREAD; + + return flags; +} + +static size_t +get_size (MuMsgFile *self) +{ + g_return_val_if_fail (self, 0); + return self->_size; +} + +static MuMsgPrio +parse_prio_str (const char* priostr) +{ + int i; + struct { + const char* _str; + MuMsgPrio _prio; + } str_prio[] = { + { "high", MU_MSG_PRIO_HIGH }, + { "1", MU_MSG_PRIO_HIGH }, + { "2", MU_MSG_PRIO_HIGH }, + + { "normal", MU_MSG_PRIO_NORMAL }, + { "3", MU_MSG_PRIO_NORMAL }, + + { "low", MU_MSG_PRIO_LOW }, + { "list", MU_MSG_PRIO_LOW }, + { "bulk", MU_MSG_PRIO_LOW }, + { "4", MU_MSG_PRIO_LOW }, + { "5", MU_MSG_PRIO_LOW } + }; + + for (i = 0; i != G_N_ELEMENTS(str_prio); ++i) + if (g_ascii_strcasecmp (priostr, str_prio[i]._str) == 0) + return str_prio[i]._prio; + + /* e.g., last-fm uses 'fm-user'... as precedence */ + return MU_MSG_PRIO_NORMAL; +} + +static MuMsgPrio +get_prio (MuMsgFile *self) +{ + GMimeObject *obj; + const char* priostr; + + g_return_val_if_fail (self, MU_MSG_PRIO_NONE); + + obj = GMIME_OBJECT(self->_mime_msg); + + priostr = g_mime_object_get_header (obj, "Precedence"); + if (!priostr) + priostr = g_mime_object_get_header (obj, "X-Priority"); + if (!priostr) + priostr = g_mime_object_get_header (obj, "Importance"); + + return priostr ? parse_prio_str (priostr) : MU_MSG_PRIO_NORMAL; +} + +/* NOTE: buffer will be *freed* or returned unchanged */ +static char* +convert_to_utf8 (GMimePart *part, char *buffer) +{ + GMimeContentType *ctype; + const char* charset; + + ctype = g_mime_object_get_content_type (GMIME_OBJECT(part)); + g_return_val_if_fail (GMIME_IS_CONTENT_TYPE(ctype), NULL); + + /* of course, the charset specified may be incorrect... */ + charset = g_mime_content_type_get_parameter (ctype, "charset"); + if (charset) { + char *utf8; + if ((utf8 = mu_str_convert_to_utf8 + (buffer, g_mime_charset_iconv_name (charset)))) { + g_free (buffer); + buffer = utf8; + } + } else if (!g_utf8_validate (buffer, -1, NULL)) { + /* if it's already utf8, nothing to do otherwise: no + charset at all, or conversion failed; ugly * hack: + replace all non-ascii chars with '.' */ + mu_str_asciify_in_place (buffer); + } + + return buffer; +} + +static gchar* +stream_to_string (GMimeStream *stream, size_t buflen) +{ + char *buffer; + ssize_t bytes; + + buffer = g_new(char, buflen + 1); + g_mime_stream_reset (stream); + + /* we read everything in one go */ + bytes = g_mime_stream_read (stream, buffer, buflen); + if (bytes < 0) { + g_warning ("%s: failed to read from stream", __func__); + g_free (buffer); + return NULL; + } + + buffer[bytes]='\0'; + + return buffer; +} + +gchar* +mu_msg_mime_part_to_string (GMimePart *part, gboolean *err) +{ + GMimeDataWrapper *wrapper; + GMimeStream *stream; + ssize_t buflen; + char *buffer; + + buffer = NULL; + stream = NULL; + + g_return_val_if_fail (err, NULL); + + *err = TRUE; /* guilty until proven innocent */ + g_return_val_if_fail (GMIME_IS_PART(part), NULL); + + wrapper = g_mime_part_get_content (part); + if (!wrapper) { + /* this happens with invalid mails */ + g_debug ("failed to create data wrapper"); + goto cleanup; + } + + stream = g_mime_stream_mem_new (); + if (!stream) { + g_warning ("failed to create mem stream"); + goto cleanup; + } + + buflen = g_mime_data_wrapper_write_to_stream (wrapper, stream); + if (buflen <= 0) {/* empty buffer, not an error */ + *err = FALSE; + goto cleanup; + } + + buffer = stream_to_string (stream, (size_t)buflen); + + /* convert_to_utf8 will free the old 'buffer' if needed */ + buffer = convert_to_utf8 (part, buffer); + *err = FALSE; + +cleanup: + if (G_IS_OBJECT(stream)) + g_object_unref (stream); + + return buffer; +} + +static gboolean +contains (GSList *lst, const char *str) +{ + for (; lst; lst = g_slist_next(lst)) + if (g_strcmp0 ((char*)lst->data, str) == 0) + return TRUE; + return FALSE; +} + +/* + * NOTE: this will get the list of references with the oldest parent + * at the beginning */ +static GSList* +get_references (MuMsgFile *self) +{ + GSList *msgids; + unsigned u; + const char *headers[] = { "References", "In-reply-to", NULL }; + + for (msgids = NULL, u = 0; headers[u]; ++u) { + + char *str; + GMimeReferences *mime_refs; + int i, refs_len; + + str = mu_msg_file_get_header (self, headers[u]); + if (!str) + continue; + + mime_refs = g_mime_references_parse (NULL, str); + g_free (str); + + refs_len = g_mime_references_length (mime_refs); + for (i = 0; i < refs_len; ++i) { + const char* msgid; + msgid = g_mime_references_get_message_id (mime_refs, i); + + /* don't include duplicates */ + if (msgid && !contains (msgids, msgid)) + /* explicitly ensure it's utf8-safe, + * as GMime does not ensure that */ + msgids = g_slist_prepend (msgids, + g_strdup((msgid))); + } + g_mime_references_free (mime_refs); + } + + /* reverse, because we used g_slist_prepend for performance + * reasons */ + return g_slist_reverse (msgids); +} + +/* see: http://does-not-exist.org/mail-archives/mutt-dev/msg08249.html */ +static GSList* +get_tags (MuMsgFile *self) +{ + GSList *lst; + unsigned u; + struct { + const char *header; + char sepa; + } tagfields[] = { + { "X-Label", ' ' }, + { "X-Keywords", ',' }, + { "Keywords", ' ' } + }; + + for (lst = NULL, u = 0; u != G_N_ELEMENTS(tagfields); ++u) { + gchar *hdr; + hdr = mu_msg_file_get_header (self, tagfields[u].header); + if (hdr) { + GSList *hlst; + hlst = mu_str_to_list (hdr, tagfields[u].sepa, TRUE); + + if (lst) + (g_slist_last (lst))->next = hlst; + else + lst = hlst; + + g_free (hdr); + } + } + + return lst; +} + +static char* +cleanup_maybe (const char *str, gboolean *do_free) +{ + char *s; + + if (!str) + return NULL; + + if (!g_utf8_validate(str, -1, NULL)) { + if (*do_free) + s = mu_str_asciify_in_place ((char*)str); + else { + *do_free = TRUE; + s = mu_str_asciify_in_place(g_strdup (str)); + } + } else + s = (char*)str; + + mu_str_remove_ctrl_in_place (s); + + return s; +} + +G_GNUC_CONST static GMimeAddressType +address_type (MuMsgFieldId mfid) +{ + switch (mfid) { + case MU_MSG_FIELD_ID_BCC : return GMIME_ADDRESS_TYPE_BCC; + case MU_MSG_FIELD_ID_CC : return GMIME_ADDRESS_TYPE_CC; + case MU_MSG_FIELD_ID_TO : return GMIME_ADDRESS_TYPE_TO; + case MU_MSG_FIELD_ID_FROM: return GMIME_ADDRESS_TYPE_FROM; + default: g_return_val_if_reached (-1); + } +} + +static gchar* +get_msgid (MuMsgFile *self, gboolean *do_free) +{ + const char *msgid; + + msgid = g_mime_message_get_message_id (self->_mime_msg); + if (msgid && strlen(msgid) < MU_STORE_MAX_TERM_LENGTH) { + return (char*)msgid; + } else { /* if there is none, fake it */ + *do_free = TRUE; + return g_strdup_printf ("%016" PRIx64 "@fake-msgid", + mu_util_get_hash (self->_path)); + } +} + +char* +mu_msg_file_get_str_field (MuMsgFile *self, MuMsgFieldId mfid, + gboolean *do_free) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (mu_msg_field_is_string(mfid), NULL); + + *do_free = FALSE; /* default */ + + switch (mfid) { + + case MU_MSG_FIELD_ID_BCC: + case MU_MSG_FIELD_ID_CC: + case MU_MSG_FIELD_ID_FROM: + case MU_MSG_FIELD_ID_TO: + *do_free = TRUE; + return get_recipient (self, address_type(mfid)); + + case MU_MSG_FIELD_ID_PATH: return self->_path; + + case MU_MSG_FIELD_ID_MAILING_LIST: + *do_free = TRUE; + return (char*)get_mailing_list (self); + + case MU_MSG_FIELD_ID_SUBJECT: + return (char*)cleanup_maybe + (g_mime_message_get_subject (self->_mime_msg), do_free); + + case MU_MSG_FIELD_ID_MSGID: + return get_msgid (self, do_free); + + case MU_MSG_FIELD_ID_MAILDIR: return self->_maildir; + + case MU_MSG_FIELD_ID_BODY_TEXT: /* use mu_msg_get_body_text */ + case MU_MSG_FIELD_ID_BODY_HTML: /* use mu_msg_get_body_html */ + case MU_MSG_FIELD_ID_EMBEDDED_TEXT: + g_warning ("%s is not retrievable through: %s", + mu_msg_field_name (mfid), __func__); + return NULL; + + default: g_return_val_if_reached (NULL); + } +} + +GSList* +mu_msg_file_get_str_list_field (MuMsgFile *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (mu_msg_field_is_string_list(mfid), NULL); + + switch (mfid) { + case MU_MSG_FIELD_ID_REFS: return get_references (self); + case MU_MSG_FIELD_ID_TAGS: return get_tags (self); + default: g_return_val_if_reached (NULL); + } +} + +gint64 +mu_msg_file_get_num_field (MuMsgFile *self, const MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, -1); + g_return_val_if_fail (mu_msg_field_is_numeric(mfid), -1); + + switch (mfid) { + + case MU_MSG_FIELD_ID_DATE: { + GDateTime *dt; + dt = g_mime_message_get_date (self->_mime_msg); + return dt ? g_date_time_to_unix (dt) : 0; + } + + case MU_MSG_FIELD_ID_FLAGS: + return (gint64)get_flags(self); + + case MU_MSG_FIELD_ID_PRIO: + return (gint64)get_prio(self); + + case MU_MSG_FIELD_ID_SIZE: + return (gint64)get_size(self); + + default: g_return_val_if_reached (-1); + } +} + +char* +mu_msg_file_get_header (MuMsgFile *self, const char *header) +{ + const gchar *hdr; + + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (header, NULL); + + /* sadly, g_mime_object_get_header may return non-ascii; + * so, we need to ensure that + */ + hdr = g_mime_object_get_header (GMIME_OBJECT(self->_mime_msg), + header); + + return hdr ? mu_str_utf8ify(hdr) : NULL; +} + +struct _ForeachData { + GMimeObjectForeachFunc user_func; + gpointer user_data; + gboolean decrypt; +}; +typedef struct _ForeachData ForeachData; + +static void +foreach_cb (GMimeObject *parent, GMimeObject *part, ForeachData *fdata) +{ + /* invoke the callback function */ + fdata->user_func (parent, part, fdata->user_data); + + /* maybe iterate over decrypted parts */ + if (fdata->decrypt && + GMIME_IS_MULTIPART_ENCRYPTED (part)) { + GMimeObject *dec; + dec = mu_msg_crypto_decrypt_part + (GMIME_MULTIPART_ENCRYPTED(part), + MU_MSG_OPTION_NONE, NULL, NULL, NULL); + if (!dec) + return; + + if (GMIME_IS_MULTIPART (dec)) + g_mime_multipart_foreach ( + (GMIME_MULTIPART(dec)), + (GMimeObjectForeachFunc)foreach_cb, + fdata); + else + foreach_cb (parent, dec, fdata); + + g_object_unref (dec); + } +} + +void +mu_mime_message_foreach (GMimeMessage *msg, gboolean decrypt, + GMimeObjectForeachFunc func, gpointer user_data) +{ + ForeachData fdata; + + g_return_if_fail (GMIME_IS_MESSAGE (msg)); + g_return_if_fail (func); + + fdata.user_func = func; + fdata.user_data = user_data; + fdata.decrypt = decrypt; + + g_mime_message_foreach + (msg, + (GMimeObjectForeachFunc)foreach_cb, + &fdata); +} diff --git a/lib/mu-msg-file.h b/lib/mu-msg-file.h new file mode 100644 index 0000000..e26c4a7 --- /dev/null +++ b/lib/mu-msg-file.h @@ -0,0 +1,105 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_FILE_H__ +#define __MU_MSG_FILE_H__ + +struct _MuMsgFile; +typedef struct _MuMsgFile MuMsgFile; + +/** + * create a new message from a file + * + * @param path full path to the message + * @param mdir + * @param err error to receive (when function returns NULL), or NULL + * + * @return a new MuMsg, or NULL in case of error + */ +MuMsgFile *mu_msg_file_new (const char *path, + const char* mdir, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * destroy a MuMsgFile object + * + * @param self object to destroy, or NULL + */ +void mu_msg_file_destroy (MuMsgFile *self); + + + +/** + * get a specific header + * + * @param self a MuMsgFile instance + * @param header a header (e.g. 'X-Mailer' or 'List-Id') + * + * @return the value of the header or NULL if not found; free with g_free + */ +char* mu_msg_file_get_header (MuMsgFile *self, const char *header); + + +/** + * get a string value for this message + * + * @param self a valid MuMsgFile + * @param msfid the message field id to get (must be of type string) + * @param do_free receives TRUE or FALSE, conveying if this string + * should be owned & freed (TRUE) or not by caller. In case 'FALSE', + * this function should be treated as if it were returning a const + * char*, and note that in that case the string is only valid as long + * as the MuMsgFile is alive, ie. before mu_msg_file_destroy + * + * @return a string, or NULL + */ +char* mu_msg_file_get_str_field (MuMsgFile *self, + MuMsgFieldId msfid, + gboolean *do_free) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get a string-list value for this message + * + * @param self a valid MuMsgFile + * @param msfid the message field id to get (must be of type string-list) + * + * @return a GSList*, or NULL; free with mu_str_free_list + */ +GSList* mu_msg_file_get_str_list_field (MuMsgFile *self, MuMsgFieldId msfid) + G_GNUC_WARN_UNUSED_RESULT; + + + +/** + * get a numeric value for this message -- the return value should be + * cast into the actual type, e.g., time_t, MuMsgPrio etc. + * + * @param self a valid MuMsgFile + * @param msfid the message field id to get (must be string-based one) + * + * @return the numeric value, or -1 in case of error + */ +gint64 mu_msg_file_get_num_field (MuMsgFile *self, MuMsgFieldId mfid); + + +#endif /*__MU_MSG_FILE_H__*/ diff --git a/lib/mu-msg-iter.cc b/lib/mu-msg-iter.cc new file mode 100644 index 0000000..a401976 --- /dev/null +++ b/lib/mu-msg-iter.cc @@ -0,0 +1,437 @@ +/* -*- mode: c++; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <stdlib.h> +#include <unistd.h> + +#include <iostream> + +#include <string.h> +#include <errno.h> +#include <algorithm> +#include <xapian.h> + +#include <string> +#include <set> +#include <map> + +#include "utils/mu-util.h" +#include "utils/mu-utils.hh" + +#include "mu-msg.h" +#include "mu-msg-iter.h" +#include "mu-threader.h" + +struct ltstr { + bool operator () (const std::string &s1, + const std::string &s2) const { + return g_strcmp0 (s1.c_str(), s2.c_str()) < 0; + } +}; +typedef std::map <std::string, unsigned, ltstr> msgid_docid_map; + +class ThreadKeyMaker: public Xapian::KeyMaker { +public: + ThreadKeyMaker (GHashTable *threadinfo): _threadinfo(threadinfo) {} + virtual std::string operator()(const Xapian::Document &doc) const { + MuMsgIterThreadInfo *ti; + ti = (MuMsgIterThreadInfo*)g_hash_table_lookup + (_threadinfo, + GUINT_TO_POINTER(doc.get_docid())); + return std::string (ti && ti->threadpath ? ti->threadpath : ""); + } +private: + GHashTable *_threadinfo; +}; + +struct _MuMsgIter { +public: + _MuMsgIter (Xapian::Enquire &enq, size_t maxnum, + MuMsgFieldId sortfield, MuMsgIterFlags flags): + _enq(enq), _thread_hash (0), _msg(0), _flags(flags), + _skip_unreadable(flags & MU_MSG_ITER_FLAG_SKIP_UNREADABLE), + _skip_dups (flags & MU_MSG_ITER_FLAG_SKIP_DUPS) { + + bool descending = (flags & MU_MSG_ITER_FLAG_DESCENDING); + bool threads = (flags & MU_MSG_ITER_FLAG_THREADS); + + // first, we get _all_ matches (G_MAXINT), based the threads + // on that, then return <maxint> of those + _matches = _enq.get_mset (0, G_MAXINT); + + if (_matches.empty()) + return; + + if (threads) { + _matches.fetch(); + _cursor = _matches.begin(); + // NOTE: temporarily turn-off skipping duplicates, since we + // need threadinfo for *all* + _skip_dups = false; + _thread_hash = mu_threader_calculate + (this, _matches.size(), sortfield, descending); + _skip_dups = (flags & MU_MSG_ITER_FLAG_SKIP_DUPS); + ThreadKeyMaker keymaker(_thread_hash); + enq.set_sort_by_key (&keymaker, false); + _matches = _enq.get_mset (0, maxnum); + + } else if (sortfield != MU_MSG_FIELD_ID_NONE) { + enq.set_sort_by_value ((Xapian::valueno)sortfield, + descending); + _matches = _enq.get_mset (0, maxnum); + _cursor = _matches.begin(); + } + _cursor = _matches.begin(); + } + + ~_MuMsgIter () { + if (_thread_hash) + g_hash_table_destroy (_thread_hash); + + set_msg (NULL); + } + + const Xapian::Enquire& enquire() const { return _enq; } + Xapian::MSet& matches() { return _matches; } + + Xapian::MSet::const_iterator cursor () const { return _cursor; } + void set_cursor (Xapian::MSetIterator cur) { _cursor = cur; } + void cursor_next () { ++_cursor; } + + GHashTable *thread_hash () { return _thread_hash; } + + MuMsg *msg() const { return _msg; } + MuMsg *set_msg (MuMsg *msg) { + if (_msg) + mu_msg_unref (_msg); + return _msg = msg; + } + + MuMsgIterFlags flags() const { return _flags; } + + const std::string msgid () const { + const Xapian::Document doc (cursor().get_document()); + return doc.get_value(MU_MSG_FIELD_ID_MSGID); + } + + unsigned docid () const { + const Xapian::Document doc (cursor().get_document()); + return doc.get_docid(); + } + + bool looks_like_dup () const { + try { + const Xapian::Document doc (cursor().get_document()); + // is this message in the preferred map? if + // so, it's not a duplicate, otherwise, it + // isn't + msgid_docid_map::const_iterator pref_iter (_preferred_map.find (msgid())); + if (pref_iter != _preferred_map.end()) { + //std::cerr << "in the set!" << std::endl; + if ((*pref_iter).second == docid()) + return false; // in the set: not a dup! + else + return true; + } + + // otherwise, simply check if we've already seen this message-id, + // and, if so, it's considered a dup + if (_msg_uid_set.find (msgid()) != _msg_uid_set.end()) { + return true; + } else { + _msg_uid_set.insert (msgid()); + return false; + } + } catch (...) { + return true; + } + } + + static void each_preferred (const char *msgid, gpointer docidp, + msgid_docid_map *preferred_map) { + (*preferred_map)[msgid] = GPOINTER_TO_SIZE(docidp); + } + + void set_preferred_map (GHashTable *preferred_hash) { + if (!preferred_hash) + _preferred_map.clear(); + else + g_hash_table_foreach (preferred_hash, + (GHFunc)each_preferred, &_preferred_map); + } + + bool skip_dups () const { return _skip_dups; } + bool skip_unreadable () const { return _skip_unreadable; } + +private: + const Xapian::Enquire _enq; + Xapian::MSet _matches; + Xapian::MSet::const_iterator _cursor; + + GHashTable *_thread_hash; + MuMsg *_msg; + + MuMsgIterFlags _flags; + + mutable std::set <std::string, ltstr> _msg_uid_set; + bool _skip_unreadable; + + // the 'preferred map' (msgid->docid) is used when checking + // for duplicates; if a message is in the preferred map, it + // will not be excluded (but other messages with the same + // msgid will) + msgid_docid_map _preferred_map; + bool _skip_dups; +}; + +static gboolean +is_msg_file_readable (MuMsgIter *iter) +{ + gboolean readable; + std::string path + (iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_PATH)); + + if (path.empty()) + return FALSE; + + readable = (access (path.c_str(), R_OK) == 0) ? TRUE : FALSE; + return readable; +} + + +MuMsgIter* +mu_msg_iter_new (XapianEnquire *enq, size_t maxnum, + MuMsgFieldId sortfield, MuMsgIterFlags flags, + GError **err) +{ + g_return_val_if_fail (enq, NULL); + /* sortfield should be set to .._NONE when we're not threading */ + g_return_val_if_fail (mu_msg_field_id_is_valid (sortfield) || + sortfield == MU_MSG_FIELD_ID_NONE, + FALSE); + try { + MuMsgIter *iter (new MuMsgIter ((Xapian::Enquire&)*enq, + maxnum, + sortfield, + flags)); + // note: we check if it's a dup even for the first message, + // since we need its uid in the set for checking later messages + if ((iter->skip_unreadable() && !is_msg_file_readable (iter)) || + (iter->skip_dups() && iter->looks_like_dup ())) + mu_msg_iter_next (iter); /* skip! */ + + return iter; + + } catch (const Xapian::DatabaseModifiedError &dbmex) { + mu_util_g_set_error (err, MU_ERROR_XAPIAN_MODIFIED, + "database was modified; please reopen"); + return 0; + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0); +} + +void +mu_msg_iter_destroy (MuMsgIter *iter) +{ + try { delete iter; } MU_XAPIAN_CATCH_BLOCK; +} + +void +mu_msg_iter_set_preferred (MuMsgIter *iter, GHashTable *preferred_hash) +{ + g_return_if_fail (iter); + iter->set_preferred_map (preferred_hash); +} + +MuMsg* +mu_msg_iter_get_msg_floating (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, NULL); + g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL); + + try { + MuMsg *msg; + GError *err; + Xapian::Document *docp; + + docp = new Xapian::Document(iter->cursor().get_document()); + + err = NULL; + msg = iter->set_msg (mu_msg_new_from_doc((XapianDocument*)docp, + &err)); + if (!msg) + MU_HANDLE_G_ERROR(err); + + return msg; + + } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); +} + +gboolean +mu_msg_iter_reset (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, FALSE); + + iter->set_msg (NULL); + + try { + iter->set_cursor(iter->matches().begin()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN (FALSE); + + return TRUE; +} + +gboolean +mu_msg_iter_next (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, FALSE); + + iter->set_msg (NULL); + + if (mu_msg_iter_is_done(iter)) + return FALSE; + + try { + iter->cursor_next(); + + if (iter->cursor() == iter->matches().end()) + return FALSE; + + if ((iter->skip_unreadable() && !is_msg_file_readable (iter)) || + (iter->skip_dups() && iter->looks_like_dup ())) + return mu_msg_iter_next (iter); /* skip! */ + + return TRUE; + + } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); +} + + +gboolean +mu_msg_iter_is_done (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, TRUE); + + try { + return iter->cursor() == iter->matches().end() ? TRUE : FALSE; + + } MU_XAPIAN_CATCH_BLOCK_RETURN (TRUE); +} + +gboolean +mu_msg_iter_is_first (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, FALSE); + + return iter->cursor() == iter->matches().begin(); +} + +gboolean +mu_msg_iter_is_last (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, FALSE); + + if (mu_msg_iter_is_done (iter)) + return FALSE; + + return iter->cursor() + 1 == iter->matches().end(); +} + +/* hmmm.... is it impossible to get a 0 docid, or just very improbable? */ +unsigned +mu_msg_iter_get_docid (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, (unsigned int)-1); + g_return_val_if_fail (!mu_msg_iter_is_done(iter), + (unsigned int)-1); + try { + return iter->docid(); + + } MU_XAPIAN_CATCH_BLOCK_RETURN ((unsigned int)-1); +} + + +char* +mu_msg_iter_get_msgid (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, NULL); + g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL); + + try { + return g_strdup (iter->msgid().c_str()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); +} + +char** +mu_msg_iter_get_refs (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, NULL); + g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL); + + try { + std::string refs ( + iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_REFS)); + if (refs.empty()) + return NULL; + return g_strsplit (refs.c_str(),",", -1); + + } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); +} + +char* +mu_msg_iter_get_thread_id (MuMsgIter *iter) +{ + g_return_val_if_fail (iter, NULL); + g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL); + + try { + const std::string thread_id ( + iter->cursor().get_document().get_value(MU_MSG_FIELD_ID_THREAD_ID).c_str()); + return thread_id.empty() ? NULL : g_strdup (thread_id.c_str()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); +} + +const MuMsgIterThreadInfo* +mu_msg_iter_get_thread_info (MuMsgIter *iter) +{ + g_return_val_if_fail (!mu_msg_iter_is_done(iter), NULL); + + /* maybe we don't have thread info */ + if (!iter->thread_hash()) + return NULL; + + try { + const MuMsgIterThreadInfo *ti; + unsigned int docid; + + docid = mu_msg_iter_get_docid (iter); + ti = (const MuMsgIterThreadInfo*)g_hash_table_lookup + (iter->thread_hash(), GUINT_TO_POINTER(docid)); + + if (!ti) + g_warning ("no ti for %u\n", docid); + + return ti; + + } MU_XAPIAN_CATCH_BLOCK_RETURN (NULL); +} diff --git a/lib/mu-msg-iter.h b/lib/mu-msg-iter.h new file mode 100644 index 0000000..bce6a50 --- /dev/null +++ b/lib/mu-msg-iter.h @@ -0,0 +1,246 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_ITER_H__ +#define __MU_MSG_ITER_H__ + +#include <glib.h> +#include <mu-msg.h> + +G_BEGIN_DECLS + + +/** + * MuMsgIter is a structure to iterate over the results of a + * query. You can iterate only in one-direction, and you can do it + * only once. + * + */ + +struct _MuMsgIter; +typedef struct _MuMsgIter MuMsgIter; + + +enum _MuMsgIterFlags { + MU_MSG_ITER_FLAG_NONE = 0, + /* sort Z->A (only for threads) */ + MU_MSG_ITER_FLAG_DESCENDING = 1 << 0, + /* ignore results for which there is no existing + * readable message-file? */ + MU_MSG_ITER_FLAG_SKIP_UNREADABLE = 1 << 1, + /* ignore duplicate messages? */ + MU_MSG_ITER_FLAG_SKIP_DUPS = 1 << 2, + /* calculate threads? */ + MU_MSG_ITER_FLAG_THREADS = 1 << 3 +}; +typedef unsigned MuMsgIterFlags; + +/** + * create a new MuMsgIter -- basically, an iterator over the search + * results + * + * @param enq a Xapian::Enquire* cast to XapianEnquire* (because this + * is C, not C++),providing access to search results + * @param maxnum the maximum number of results + * @param sortfield field to sort by + * @param flags flags for this iterator (see MsgIterFlags) + + * @param err receives error information. if the error is + * MU_ERROR_XAPIAN_MODIFIED, the database should be reloaded. + * + * @return a new MuMsgIter, or NULL in case of error + */ +MuMsgIter *mu_msg_iter_new (XapianEnquire *enq, + size_t maxnum, + MuMsgFieldId sortfield, + MuMsgIterFlags flags, + GError **err) G_GNUC_WARN_UNUSED_RESULT; + +/** + * get the next message (which you got from + * e.g. mu_query_run) + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE if it succeeded, FALSE otherwise (e.g., because there + * are no more messages in the query result) + */ +gboolean mu_msg_iter_next (MuMsgIter *iter); + +/** + * Does this iterator point to the first item? + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE or FALSE + */ +gboolean mu_msg_iter_is_first (MuMsgIter *iter); + +/** + * Does this iterator point to the last item? + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE or FALSE + */ +gboolean mu_msg_iter_is_last (MuMsgIter *iter); + + +/** + * reset the iterator to the beginning + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE if it succeeded, FALSE otherwise + */ +gboolean mu_msg_iter_reset (MuMsgIter *iter); + +/** + * does this iterator point past the end of the list? + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE if the iter points past end of the list, FALSE + * otherwise + */ +gboolean mu_msg_iter_is_done (MuMsgIter *iter); + + +/** + * destroy the sequence of messages; ie. /all/ of them + * + * @param msg a valid MuMsgIter message or NULL + */ +void mu_msg_iter_destroy (MuMsgIter *iter); + + +/** + * get the corresponding MuMsg for this iter; this instance is owned + * by MuMsgIter, and becomes invalid after either mu_msg_iter_destroy + * or mu_msg_iter_next. _do not_ unref it; it's a floating reference. + * + * @param iter a valid MuMsgIter instance* + * + * @return a MuMsg instance, or NULL in case of error + */ +MuMsg* mu_msg_iter_get_msg_floating (MuMsgIter *iter) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * Provide a preferred_hash, which is a hashtable msgid->docid to + * indicate the messages which should /not/ be seen as duplicates. + * + * @param iter a valid MuMsgIter iterator + * @param preferred_hash a hashtable msgid->docid of message /not/ to + * mark as duplicates, or NULL + */ +void mu_msg_iter_set_preferred (MuMsgIter *iter, GHashTable *preferred_hash); + +/** + * get the document id for the current message + * + * @param iter a valid MuMsgIter iterator + * + * @return the docid or (unsigned int)-1 in case of error + */ +guint mu_msg_iter_get_docid (MuMsgIter *iter); + + +/** + * calculate the message threads + * + * @param iter a valid MuMsgIter iterator + * + * @return TRUE if it worked, FALSE otherwise. + */ +gboolean mu_msg_iter_calculate_threads (MuMsgIter *iter); + + +enum _MuMsgIterThreadProp { + MU_MSG_ITER_THREAD_PROP_NONE = 0 << 0, + + MU_MSG_ITER_THREAD_PROP_ROOT = 1 << 0, + MU_MSG_ITER_THREAD_PROP_FIRST_CHILD = 1 << 1, + MU_MSG_ITER_THREAD_PROP_LAST_CHILD = 1 << 2, + MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT = 1 << 3, + MU_MSG_ITER_THREAD_PROP_DUP = 1 << 4, + MU_MSG_ITER_THREAD_PROP_HAS_CHILD = 1 << 5 +}; +typedef guint8 MuMsgIterThreadProp; + +struct _MuMsgIterThreadInfo { + gchar *threadpath; /* a string describing the thread-path in + * such a way that we can sort by this + * string to get the right order. */ + guint level; /* thread-depth -- [0...] */ + MuMsgIterThreadProp prop; +}; +typedef struct _MuMsgIterThreadInfo MuMsgIterThreadInfo; + +/** + * get a the MuMsgThreaderInfo struct for this message; this only + * works when you created the mu-msg-iter with threading enabled + * (otherwise, return NULL) + * + * @param iter a valid MuMsgIter iterator + * + * @return an info struct + */ +const MuMsgIterThreadInfo* mu_msg_iter_get_thread_info (MuMsgIter *iter); + +/** + * get the message-id for this message + * + * @param iter a valid MuMsgIter iterator + * + * @return the message-id; free with g_free(). + */ +char* mu_msg_iter_get_msgid (MuMsgIter *iter) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * get the list of references for this messages as a NULL-terminated + * string array + * + * @param iter a valid MuMsgIter iterator + * + * @return a NULL-terminated string array. free with g_strfreev when + * it's no longer needed. + */ +char** mu_msg_iter_get_refs (MuMsgIter *iter) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get the thread-id for this message + * + * @param iter a valid MuMsgIter iterator + * + * @return the thread-id; free with g_free(). + */ +char* mu_msg_iter_get_thread_id (MuMsgIter *iter) + G_GNUC_WARN_UNUSED_RESULT; + + +/* FIXME */ +const char* mu_msg_iter_get_path (MuMsgIter *iter); + +G_END_DECLS + +#endif /*__MU_MSG_ITER_H__*/ diff --git a/lib/mu-msg-json.c b/lib/mu-msg-json.c new file mode 100644 index 0000000..52b20f9 --- /dev/null +++ b/lib/mu-msg-json.c @@ -0,0 +1,524 @@ +/* +** Copyright (C) 2018 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> +#include <ctype.h> + +#include <json-glib/json-glib.h> + +#include "mu-msg.h" +#include "mu-msg-iter.h" +#include "mu-msg-part.h" +#include "mu-maildir.h" + +static void +add_list_member (JsonBuilder *bob, const char* elm, const GSList *lst) +{ + const GSList *cur; + + if (!lst) + return; /* empty list, don't include */ + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_begin_array (bob); + + for (cur = lst; cur; cur = g_slist_next(cur)) + bob = json_builder_add_string_value (bob, (const char*)cur->data); + + bob = json_builder_end_array (bob); +} + +static void +add_string_member (JsonBuilder *bob, const char* elm, const char *str) +{ + if (!str) + return; /* don't include */ + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_string_value (bob, str); +} + +static void +add_int_member (JsonBuilder *bob, const char* elm, gint64 num) +{ + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_int_value (bob, num); +} + +static void +add_bool_member (JsonBuilder *bob, const char* elm, gboolean b) +{ + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_boolean_value (bob, b); +} + + +static void +consume_array_member (JsonBuilder *bob, const char* elm, JsonArray *arr) +{ + JsonNode *node; + + if (!arr) + return; /* nothing to do */ + + node = json_node_new (JSON_NODE_ARRAY); + json_node_init_array (node, arr); + json_array_unref (arr); + + bob = json_builder_set_member_name (bob, elm); + bob = json_builder_add_value (bob, node); /* consumes */ +} + + + +typedef struct { + JsonArray *from, *to, *cc, *bcc, *reply_to; +} ContactData; + +static void +add_contact (JsonArray **arr, MuMsgContact *c) +{ + JsonObject *cell; + + if (!*arr) + *arr = json_array_new (); + + cell = json_object_new (); + if (c->name) + json_object_set_string_member (cell, "name", c->name); + if (c->email) + json_object_set_string_member (cell, "email", c->email); + + json_array_add_object_element (*arr, cell); /* consumes */ +} + +static gboolean +each_contact (MuMsgContact *c, ContactData *cdata) +{ + switch (mu_msg_contact_type (c)) { + + case MU_MSG_CONTACT_TYPE_FROM: + add_contact(&cdata->from, c); + break; + case MU_MSG_CONTACT_TYPE_TO: + add_contact(&cdata->to ,c); + break; + case MU_MSG_CONTACT_TYPE_CC: + add_contact(&cdata->cc, c); + break; + case MU_MSG_CONTACT_TYPE_BCC: + add_contact(&cdata->bcc, c); + break; + case MU_MSG_CONTACT_TYPE_REPLY_TO: + add_contact(&cdata->reply_to, c); + break; + default: g_return_val_if_reached (FALSE); + } + + return TRUE; +} + + +static void +maybe_append_list_post_as_reply_to (JsonBuilder *bob, MuMsg *msg) +{ + /* some mailing lists do not set the reply-to; see pull #1278. So for + * those cases, check the List-Post address and use that instead */ + + GMatchInfo *minfo; + GRegex *rx; + const char* list_post; + + list_post = mu_msg_get_header (msg, "List-Post"); + if (!list_post) + return; + + rx = g_regex_new ("^(<?mailto:)?([a-z0-9%+@.-]+)>?", G_REGEX_CASELESS, 0, NULL); + g_return_if_fail(rx); + + if (g_regex_match (rx, list_post, 0, &minfo)) { + char *addr; + addr = g_match_info_fetch (minfo, 2); + + bob = json_builder_set_member_name (bob, "reply-to"); + bob = json_builder_begin_array(bob); + bob = json_builder_begin_object(bob); + add_string_member(bob, "email", addr); + g_free (addr); + + bob = json_builder_end_object(bob); + bob = json_builder_end_array(bob); + } + + g_match_info_free (minfo); + g_regex_unref (rx); +} + + +static void +add_contacts (JsonBuilder *bob, MuMsg *msg) +{ + ContactData cdata; + memset (&cdata, 0, sizeof(cdata)); + + mu_msg_contact_foreach (msg, + (MuMsgContactForeachFunc)each_contact, + &cdata); + + consume_array_member (bob, "to" , cdata.to); + consume_array_member (bob, "from" , cdata.from); + consume_array_member (bob, "cc" , cdata.cc); + consume_array_member (bob, "bcc" , cdata.bcc); + consume_array_member (bob, "reply-to", cdata.reply_to); + + if (!cdata.reply_to) + maybe_append_list_post_as_reply_to (bob, msg); +} + +struct _FlagData { + JsonBuilder *bob; + MuFlags msgflags; +}; +typedef struct _FlagData FlagData; + +static void +each_flag (MuFlags flag, FlagData *fdata) +{ + if (!(flag & fdata->msgflags)) + return; + + json_builder_add_string_value (fdata->bob, + mu_flag_name(flag)); +} + +static void +add_flags (JsonBuilder *bob, MuMsg *msg) +{ + FlagData fdata; + + fdata.msgflags = mu_msg_get_flags (msg); + fdata.bob = bob; + + bob = json_builder_set_member_name (bob, "flags"); + + bob = json_builder_begin_array (bob); + mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata); + bob = json_builder_end_array (bob); + +} + +static char* +get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index) +{ + char *path; + GError *err; + + err = NULL; + path = mu_msg_part_get_cache_path (msg, opts, index, &err); + if (!path) + goto errexit; + + if (!mu_msg_part_save (msg, opts, path, index, &err)) + goto errexit; + + return path; + +errexit: + g_warning ("failed to save mime part: %s", + err->message ? err->message : "something went wrong"); + g_clear_error (&err); + g_free (path); + return NULL; +} + + +static gchar* +get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) +{ + opts |= MU_MSG_OPTION_USE_EXISTING; + + if (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) || + g_ascii_strcasecmp (part->type, "image") != 0) + return NULL; + + return get_temp_file (msg, opts, part->index); +} + + +struct _PartInfo { + JsonBuilder *bob; + MuMsgOptions opts; +}; +typedef struct _PartInfo PartInfo; + + +static void +add_part_crypto (JsonBuilder *bob, MuMsgPart *mpart, PartInfo *pinfo) +{ + const char *verdict; + MuMsgPartSigStatusReport *report; + + + add_string_member (bob, "decryption", + pinfo->opts & MU_MSG_PART_TYPE_DECRYPTED ? "ok" : + pinfo->opts & MU_MSG_PART_TYPE_ENCRYPTED ? "failed" : + NULL); + + report = mpart->sig_status_report; + if (!report) + return; + + switch (report->verdict) { + case MU_MSG_PART_SIG_STATUS_GOOD: verdict = "verified"; break; + case MU_MSG_PART_SIG_STATUS_BAD: verdict = "bad"; break; + case MU_MSG_PART_SIG_STATUS_ERROR: verdict = "unverified"; break; + default: verdict = NULL; + } + + add_string_member (bob, "signature", verdict); + add_string_member (bob, "signers", report->signers); +} + +static void +add_part_type (JsonBuilder *bob, MuMsgPartType ptype) +{ + unsigned u; + struct PartTypes { + MuMsgPartType ptype; + const char* name; + } ptypes[] = { + { MU_MSG_PART_TYPE_LEAF, "leaf" }, + { MU_MSG_PART_TYPE_MESSAGE, "message" }, + { MU_MSG_PART_TYPE_INLINE, "inline" }, + { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" }, + { MU_MSG_PART_TYPE_SIGNED, "signed" }, + { MU_MSG_PART_TYPE_ENCRYPTED, "encrypted" } + }; + + bob = json_builder_set_member_name (bob, "type"); + bob = json_builder_begin_array(bob); + + for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u) + if (ptype & ptypes[u].ptype) + json_builder_add_string_value (bob, ptypes[u].name); + + bob = json_builder_end_array(bob); +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + char *name, *tmpfile; + + pinfo->bob = json_builder_begin_object(pinfo->bob); + + name = mu_msg_part_get_filename (part, TRUE); + tmpfile = get_temp_file_maybe (msg, part, pinfo->opts); + + add_int_member (pinfo->bob, "index", part->index); + add_string_member (pinfo->bob, "name", name); + + if (part->type && part->subtype) { + char *mime_type = + g_strdup_printf ("%s/%s", part->type, part->subtype); + add_string_member (pinfo->bob, "mime-type", mime_type); + g_free(mime_type); + } + + add_string_member (pinfo->bob, "temp", tmpfile); + add_part_type (pinfo->bob, part->part_type); + + if (mu_msg_part_maybe_attachment (part)) + add_bool_member (pinfo->bob, "attachment", TRUE); + + add_string_member (pinfo->bob, "cid", mu_msg_part_get_content_id(part)); + add_int_member (pinfo->bob, "size", part->size); + + add_part_crypto (pinfo->bob, part, pinfo); + + g_free (name); + g_free (tmpfile); + + pinfo->bob = json_builder_end_object(pinfo->bob); +} + + +static void +add_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + PartInfo pinfo; + + pinfo.opts = opts; + bob = json_builder_set_member_name (bob, "parts"); + bob = json_builder_begin_array (bob); + + mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, &pinfo); + + bob = json_builder_end_array (bob); +} + +static void +add_thread_info (JsonBuilder *bob, const MuMsgIterThreadInfo *ti) +{ + bob = json_builder_set_member_name (bob, "thread"); + bob = json_builder_begin_object(bob); + + add_string_member (bob, "path", ti->threadpath); + add_int_member (bob, "level", ti->level); + + bob = json_builder_set_member_name (bob, "flags"); + bob = json_builder_begin_array (bob); + + if (ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD) + bob = json_builder_add_string_value (bob, "first-child"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD) + bob = json_builder_add_string_value (bob, "last-child"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT) + bob = json_builder_add_string_value (bob, "empty-parent"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_DUP) + bob = json_builder_add_string_value (bob, "duplicate"); + if (ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD) + bob = json_builder_add_string_value (bob, "has-child"); + + bob = json_builder_end_array (bob); + bob = json_builder_end_object(bob); +} + +static void +add_body_txt_params (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + const GSList *params; + + params = mu_msg_get_body_text_content_type_parameters (msg, opts); + if (!params) + return; + + bob = json_builder_set_member_name (bob, "body-txt-params"); + bob = json_builder_begin_array (bob); + + while (params) { + const char *key, *val; + + key = (const char *)params->data; + params = g_slist_next(params); + if (!params) + break; + val = (const char *)params->data; + + if (key && val) { + bob = json_builder_begin_object(bob); + add_string_member(bob, key, val); + bob = json_builder_end_object(bob); + } + + params = g_slist_next(params); + } + + bob = json_builder_end_array(bob); +} + +static void /* ie., parts that require opening the message file */ +add_file_parts (JsonBuilder *bob, MuMsg *msg, MuMsgOptions opts) +{ + const char *str; + GError *err; + + err = NULL; + + if (!mu_msg_load_msg_file (msg, &err)) { + g_warning ("failed to load message file: %s", + err ? err->message : "some error occurred"); + g_clear_error (&err); + return; + } + + add_parts (bob, msg, opts); + add_contacts (bob, msg); + + /* add the user-agent / x-mailer */ + str = mu_msg_get_header (msg, "User-Agent"); + if (!str) + str = mu_msg_get_header (msg, "X-Mailer"); + add_string_member (bob, "user-agent", str); + add_body_txt_params (bob, msg, opts); + add_string_member (bob, "body-txt", mu_msg_get_body_text(msg, opts)); + add_string_member (bob, "body-html", mu_msg_get_body_html(msg, opts)); +} + +struct _JsonNode* +mu_msg_to_json (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti, + MuMsgOptions opts) +{ + JsonNode *node; + JsonBuilder *bob; + + time_t t; + size_t s; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) && + (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL); + bob = json_builder_new (); + bob = json_builder_begin_object (bob); + + if (ti) + add_thread_info (bob, ti); + + add_string_member (bob, "subject", mu_msg_get_subject (msg)); + + /* in the no-headers-only case (see below) we get a more complete list + * of contacts, so no need to get them here if that's the case */ + if (opts & MU_MSG_OPTION_HEADERS_ONLY) + add_contacts (bob, msg); + + t = mu_msg_get_date (msg); + if (t != (time_t)-1) + add_int_member (bob, "date", t); + + s = mu_msg_get_size (msg); + if (s != (size_t)-1) + add_int_member (bob, "size", s); + + add_string_member (bob, "message-id", mu_msg_get_msgid (msg)); + add_string_member (bob, "mailing-list", mu_msg_get_mailing_list (msg)); + add_string_member (bob, "path", mu_msg_get_path (msg)); + add_string_member (bob, "maildir", mu_msg_get_maildir (msg)); + add_string_member (bob, "priority", mu_msg_prio_name(mu_msg_get_prio(msg))); + + add_flags (bob, msg); + + add_list_member (bob, "tags", mu_msg_get_tags(msg)); + add_list_member (bob, "references", mu_msg_get_references (msg)); + add_string_member (bob, "in-reply-to", + mu_msg_get_header (msg, "In-Reply-To")); + + /* headers are retrieved from the database, views from the + * message file file attr things can only be gotten from the + * file (ie., mu view), not from the database (mu find). */ + if (!(opts & MU_MSG_OPTION_HEADERS_ONLY)) + add_file_parts (bob, msg, opts); + + bob = json_builder_end_object (bob); + node = json_builder_get_root (bob); + + g_clear_object (&bob); + + return node; +} diff --git a/lib/mu-msg-part.c b/lib/mu-msg-part.c new file mode 100644 index 0000000..099274d --- /dev/null +++ b/lib/mu-msg-part.c @@ -0,0 +1,1009 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2008-2014 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <string.h> +#include <unistd.h> + +#include "utils/mu-util.h" +#include "utils/mu-str.h" +#include "mu-msg-priv.h" +#include "mu-msg-part.h" + +struct _DoData { + GMimeObject *mime_obj; + unsigned index; +}; +typedef struct _DoData DoData; + +static void +do_it_with_index (MuMsg *msg, MuMsgPart *part, DoData *ddata) +{ + if (ddata->mime_obj) + return; + + if (part->index == ddata->index) { + /* Add a reference to this object, this way if it is + * encrypted it will not be garbage collected before + * we are done with it. */ + g_object_ref (part->data); + ddata->mime_obj = (GMimeObject*)part->data; + } +} + +static GMimeObject* +get_mime_object_at_index (MuMsg *msg, MuMsgOptions opts, unsigned index) +{ + DoData ddata; + + ddata.mime_obj = NULL; + ddata.index = index; + + /* wipe out some irrelevant options */ + opts &= ~MU_MSG_OPTION_VERIFY; + opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES; + + mu_msg_part_foreach (msg, opts, + (MuMsgPartForeachFunc)do_it_with_index, + &ddata); + + return ddata.mime_obj; +} + + +typedef gboolean (*MuMsgPartMatchFunc) (MuMsgPart *, gpointer); +struct _MatchData { + MuMsgPartMatchFunc match_func; + gpointer user_data; + int index; +}; +typedef struct _MatchData MatchData; + +static void +check_match (MuMsg *msg, MuMsgPart *part, MatchData *mdata) +{ + if (mdata->index != -1) + return; + + if (mdata->match_func (part, mdata->user_data)) + mdata->index = part->index; +} + +static int +get_matching_part_index (MuMsg *msg, MuMsgOptions opts, + MuMsgPartMatchFunc func, gpointer user_data) +{ + MatchData mdata; + + mdata.match_func = func; + mdata.user_data = user_data; + mdata.index = -1; + + mu_msg_part_foreach (msg, opts, + (MuMsgPartForeachFunc)check_match, + &mdata); + return mdata.index; +} + + +static void +accumulate_text_message (MuMsg *msg, MuMsgPart *part, GString **gstrp) +{ + const gchar *str; + char *adrs; + GMimeMessage *mimemsg; + InternetAddressList *addresses; + + /* put sender, recipients and subject in the string, so they + * can be indexed as well */ + mimemsg = GMIME_MESSAGE (part->data); + addresses = g_mime_message_get_addresses (mimemsg, GMIME_ADDRESS_TYPE_FROM); + adrs = internet_address_list_to_string (addresses, NULL, FALSE); + + g_string_append_printf + (*gstrp, "%s%s", adrs ? adrs : "", adrs ? "\n" : ""); + g_free (adrs); + + str = g_mime_message_get_subject (mimemsg); + g_string_append_printf + (*gstrp, "%s%s", str ? str : "", str ? "\n" : ""); + + addresses = g_mime_message_get_all_recipients (mimemsg); + adrs = internet_address_list_to_string (addresses, NULL, FALSE); + g_object_unref (addresses); + + g_string_append_printf + (*gstrp, "%s%s", adrs ? adrs : "", adrs ? "\n" : ""); + g_free (adrs); +} + +static void +accumulate_text_part (MuMsg *msg, MuMsgPart *part, GString **gstrp) +{ + GMimeContentType *ctype; + gboolean err; + char *txt; + + ctype = g_mime_object_get_content_type ((GMimeObject*)part->data); + if (!g_mime_content_type_is_type (ctype, "text", "plain")) + return; /* not plain text */ + + txt = mu_msg_mime_part_to_string((GMimePart*)part->data, &err); + if (txt) + g_string_append (*gstrp, txt); + + g_free (txt); +} + +static void +accumulate_text (MuMsg *msg, MuMsgPart *part, GString **gstrp) +{ + if (GMIME_IS_MESSAGE(part->data)) + accumulate_text_message (msg, part, gstrp); + else if (GMIME_IS_PART (part->data)) + accumulate_text_part (msg, part, gstrp); +} + +/* declaration, so we can use it earlier */ +static gboolean +handle_mime_object (MuMsg *msg, GMimeObject *mobj, GMimeObject *parent, + MuMsgOptions opts, unsigned *index, gboolean decrypted, + MuMsgPartForeachFunc func, gpointer user_data); + +static char* +get_text_from_mime_msg (MuMsg *msg, GMimeMessage *mmsg, MuMsgOptions opts) +{ + GString *gstr; + unsigned index; + + index = 1; + gstr = g_string_sized_new (4096); + handle_mime_object (msg, + mmsg->mime_part, + (GMimeObject *) mmsg, + opts, + &index, + FALSE, + (MuMsgPartForeachFunc)accumulate_text, + &gstr); + + return g_string_free (gstr, FALSE); +} + + +char* +mu_msg_part_get_text (MuMsg *msg, MuMsgPart *self, MuMsgOptions opts) +{ + GMimeObject *mobj; + GMimeMessage *mime_msg; + gboolean err; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (self && GMIME_IS_OBJECT(self->data), + NULL); + + mobj = (GMimeObject*)self->data; + + err = FALSE; + if (GMIME_IS_PART (mobj)) { + if (self->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN) + return mu_msg_mime_part_to_string ((GMimePart*)mobj, + &err); + else + return NULL; /* non-text MimePart */ + } + + mime_msg = NULL; + + if (GMIME_IS_MESSAGE_PART (mobj)) + mime_msg = g_mime_message_part_get_message + ((GMimeMessagePart*)mobj); + else if (GMIME_IS_MESSAGE (mobj)) + mime_msg = (GMimeMessage*)mobj; + + /* apparently, g_mime_message_part_get_message may still + * return NULL */ + if (mime_msg) + return get_text_from_mime_msg (msg, mime_msg, opts); + return NULL; +} + + +/* note: this will return -1 in case of error or if the size is + * unknown */ +static ssize_t +get_part_size (GMimePart *part) +{ + GMimeDataWrapper *wrapper; + GMimeStream *stream; + + wrapper = g_mime_part_get_content (part); + if (!GMIME_IS_DATA_WRAPPER(wrapper)) + return -1; + + stream = g_mime_data_wrapper_get_stream (wrapper); + if (!stream) + return -1; /* no stream -> size is 0 */ + else + return g_mime_stream_length (stream); + + /* NOTE: stream/wrapper are owned by gmime, no unreffing */ +} + + +static char* +cleanup_filename (char *fname) +{ + GString *gstr; + gchar *cur; + gunichar uc; + + gstr = g_string_sized_new (strlen (fname)); + + /* replace control characters, slashes, and colons by '-' */ + for (cur = fname; cur && *cur; cur = g_utf8_next_char (cur)) { + uc = g_utf8_get_char (cur); + if (g_unichar_iscntrl (uc) || uc == '/' || uc == ':') + g_string_append_unichar (gstr, '-'); + else + g_string_append_unichar (gstr, uc); + } + + g_free (fname); + return g_string_free (gstr, FALSE); +} + + +/* + * when a part doesn't have a filename, it can be useful to 'guess' one based on + * its mime-type, which allows other tools to handle them correctly, e.g. from + * mu4e. + * + * For now, we only handle calendar invitations in that way, but others may + * follow. + */ +static char* +guess_file_name (GMimeObject *mobj, unsigned index) +{ + GMimeContentType *ctype; + + ctype = g_mime_object_get_content_type (mobj); + + /* special case for calendars; map to '.vcs' */ + if (g_mime_content_type_is_type (ctype, "text", "calendar")) + return g_strdup_printf ("vcal-%u.vcs", index); + + /* fallback */ + return g_strdup_printf ("%u.msgpart", index); +} + + +static char* +mime_part_get_filename (GMimeObject *mobj, unsigned index, + gboolean construct_if_needed) +{ + gchar *fname; + + fname = NULL; + + if (GMIME_IS_PART (mobj)) { + /* the easy case: the part has a filename */ + fname = (gchar*)g_mime_part_get_filename (GMIME_PART(mobj)); + if (fname) /* don't include directory components */ + fname = g_path_get_basename (fname); + } + + if (!fname && !construct_if_needed) + return NULL; + + if (GMIME_IS_MESSAGE_PART(mobj)) { + GMimeMessage *msg; + const char *subj; + msg = g_mime_message_part_get_message + (GMIME_MESSAGE_PART(mobj)); + subj = g_mime_message_get_subject (msg); + fname = g_strdup_printf ("%s.eml", subj ? subj : "message"); + } + + if (!fname) + fname = guess_file_name (mobj, index); + + /* replace control characters, slashes, and colons */ + fname = cleanup_filename (fname); + + return fname; +} + + +char* +mu_msg_part_get_filename (MuMsgPart *mpart, gboolean construct_if_needed) +{ + g_return_val_if_fail (mpart, NULL); + g_return_val_if_fail (GMIME_IS_OBJECT(mpart->data), NULL); + + return mime_part_get_filename ((GMimeObject*)mpart->data, + mpart->index, construct_if_needed); +} + +const gchar* +mu_msg_part_get_content_id (MuMsgPart *mpart) +{ + g_return_val_if_fail (mpart, NULL); + g_return_val_if_fail (GMIME_IS_OBJECT(mpart->data), NULL); + return g_mime_object_get_content_id((GMimeObject*)mpart->data); +} + + + +static MuMsgPartType +get_disposition (GMimeObject *mobj) +{ + const char *disp; + + disp = g_mime_object_get_disposition (mobj); + if (!disp) + return MU_MSG_PART_TYPE_NONE; + + if (strcasecmp (disp, GMIME_DISPOSITION_ATTACHMENT) == 0) + return MU_MSG_PART_TYPE_ATTACHMENT; + + if (strcasecmp (disp, GMIME_DISPOSITION_INLINE) == 0) + return MU_MSG_PART_TYPE_INLINE; + + return MU_MSG_PART_TYPE_NONE; +} + +/* call 'func' with information about this MIME-part */ +static inline void +check_signature (MuMsg *msg, GMimeMultipartSigned *part, MuMsgOptions opts) +{ + GError *err; + + err = NULL; + mu_msg_crypto_verify_part (part, opts, &err); + if (err) { + g_warning ("error verifying signature: %s", err->message); + g_clear_error (&err); + } +} + + +/* Note: this is function will be called by GMime when it needs a + * password. However, GMime <= 2.6.10 does not handle + * getting passwords correctly, so this might fail. see: + * password_requester in mu-msg-crypto.c */ +static gchar* +get_console_pw (const char* user_id, const char *prompt_ctx, + gboolean reprompt, gpointer user_data) +{ + char *prompt, *pass; + + if (!g_mime_check_version(2,6,11)) + g_printerr ( + "*** the gmime library you are using has version " + "%u.%u.%u (<= 2.6.10)\n" + "*** this version has a bug in its password " + "retrieval routine, and probably won't work.\n", + gmime_major_version, gmime_minor_version, + gmime_micro_version); + + if (reprompt) + g_print ("Authentication failed. Please try again\n"); + + prompt = g_strdup_printf ("Password for %s: ", user_id); + + pass = mu_util_read_password (prompt); + g_free (prompt); + + return pass; +} + + +static gboolean +handle_encrypted_part (MuMsg *msg, GMimeMultipartEncrypted *part, + MuMsgOptions opts, unsigned *index, + MuMsgPartForeachFunc func, gpointer user_data) +{ + GError *err; + gboolean rv; + GMimeObject *dec; + MuMsgPartPasswordFunc pw_func; + + if (opts & MU_MSG_OPTION_CONSOLE_PASSWORD) + pw_func = (MuMsgPartPasswordFunc)get_console_pw; + else + pw_func = NULL; + + + err = NULL; + dec = mu_msg_crypto_decrypt_part (part, opts, pw_func, NULL, &err); + if (err) { + g_warning ("error decrypting part: %s", err->message); + g_clear_error (&err); + } + + if (dec) { + rv = handle_mime_object (msg, dec, (GMimeObject *) part, + opts, index, TRUE, func, user_data); + g_object_unref (dec); + } else { + /* On failure to decrypt, list the encrypted part as + * an attachment + */ + GMimeObject *encrypted; + + encrypted = g_mime_multipart_get_part ( + GMIME_MULTIPART (part), 1); + + g_return_val_if_fail (GMIME_IS_PART(encrypted), FALSE); + + rv = handle_mime_object (msg, encrypted, (GMimeObject *) part, + opts, index, FALSE, func, user_data); + } + + return rv; +} + +static gboolean +looks_like_text_body_part (GMimeContentType *ctype) +{ + unsigned u; + static struct { + const char *type; + const char *subtype; + } types[] = { + { "text", "plain" }, + { "text", "x-markdown" }, + { "text", "x-diff" }, + { "text", "x-patch" }, + { "application", "x-patch"} + /* possible other types */ + }; + + for (u = 0; u != G_N_ELEMENTS(types); ++u) + if (g_mime_content_type_is_type ( + ctype, types[u].type, types[u].subtype)) + return TRUE; + + return FALSE; +} + + +static MuMsgPartSigStatusReport* +copy_status_report_maybe (GObject *obj) +{ + MuMsgPartSigStatusReport *report, *copy; + + report = g_object_get_data (obj, SIG_STATUS_REPORT); + if (!report) + return NULL; /* nothing to copy */ + + copy = g_slice_new0(MuMsgPartSigStatusReport); + copy->verdict = report->verdict; + + if (report->report) + copy->report = g_strdup (report->report); + if (report->signers) + copy->signers = g_strdup (report->signers); + + return copy; +} + + + +/* call 'func' with information about this MIME-part */ +static gboolean +handle_part (MuMsg *msg, GMimePart *part, GMimeObject *parent, + MuMsgOptions opts, unsigned *index, gboolean decrypted, + MuMsgPartForeachFunc func, gpointer user_data) +{ + GMimeContentType *ct; + MuMsgPart msgpart; + + memset (&msgpart, 0, sizeof(MuMsgPart)); + + msgpart.size = get_part_size (part); + msgpart.part_type = MU_MSG_PART_TYPE_LEAF; + msgpart.part_type |= get_disposition ((GMimeObject*)part); + if (decrypted) + msgpart.part_type |= MU_MSG_PART_TYPE_DECRYPTED; + else if ((opts & MU_MSG_OPTION_DECRYPT) && + GMIME_IS_MULTIPART_ENCRYPTED (parent)) + msgpart.part_type |= MU_MSG_PART_TYPE_ENCRYPTED; + + + ct = g_mime_object_get_content_type ((GMimeObject*)part); + if (GMIME_IS_CONTENT_TYPE(ct)) { + msgpart.type = g_mime_content_type_get_media_type (ct); + msgpart.subtype = g_mime_content_type_get_media_subtype (ct); + /* store in the part_type as well, for quick checking */ + if (looks_like_text_body_part (ct)) + msgpart.part_type |= MU_MSG_PART_TYPE_TEXT_PLAIN; + else if (g_mime_content_type_is_type (ct, "text", "html")) + msgpart.part_type |= MU_MSG_PART_TYPE_TEXT_HTML; + } + + /* put the verification info in the pgp-signature and every + * descendent of a pgp-encrypted part */ + msgpart.sig_status_report = NULL; + if (g_ascii_strcasecmp (msgpart.subtype, "pgp-signature") == 0 || + decrypted) { + msgpart.sig_status_report = + copy_status_report_maybe (G_OBJECT(parent)); + if (msgpart.sig_status_report) + msgpart.part_type |= MU_MSG_PART_TYPE_SIGNED; + } + + msgpart.data = (gpointer)part; + msgpart.index = (*index)++; + + func (msg, &msgpart, user_data); + + mu_msg_part_sig_status_report_destroy (msgpart.sig_status_report); + + return TRUE; +} + + +/* call 'func' with information about this MIME-part */ +static gboolean +handle_message_part (MuMsg *msg, GMimeMessagePart *mimemsgpart, + GMimeObject *parent, MuMsgOptions opts, unsigned *index, + gboolean decrypted, + MuMsgPartForeachFunc func, gpointer user_data) +{ + MuMsgPart msgpart; + + memset (&msgpart, 0, sizeof(MuMsgPart)); + + msgpart.type = "message"; + msgpart.subtype = "rfc822"; + msgpart.index = (*index)++; + + /* msgpart.size = 0; /\* maybe calculate this? *\/ */ + + msgpart.part_type = MU_MSG_PART_TYPE_MESSAGE; + msgpart.part_type |= get_disposition ((GMimeObject*)mimemsgpart); + + msgpart.data = (gpointer)mimemsgpart; + func (msg, &msgpart, user_data); + + if (opts & MU_MSG_OPTION_RECURSE_RFC822) { + GMimeMessage *mmsg; /* may return NULL for some + * messages */ + mmsg = g_mime_message_part_get_message (mimemsgpart); + if (mmsg) + return handle_mime_object (msg, + mmsg->mime_part, + parent, + opts, + index, + decrypted, + func, + user_data); + } + + return TRUE; +} + +static gboolean +handle_multipart (MuMsg *msg, GMimeMultipart *mpart, GMimeObject *parent, + MuMsgOptions opts, unsigned *index, gboolean decrypted, + MuMsgPartForeachFunc func, gpointer user_data) +{ + gboolean res; + GMimeObject *part; + guint i; + + res = TRUE; + for (i = 0; i < mpart->children->len; i++) { + part = (GMimeObject *) mpart->children->pdata[i]; + res &= handle_mime_object (msg, part, parent, + opts, index, decrypted, + func, user_data); + } + + return res; +} + + +static gboolean +handle_mime_object (MuMsg *msg, GMimeObject *mobj, GMimeObject *parent, + MuMsgOptions opts, unsigned *index, gboolean decrypted, + MuMsgPartForeachFunc func, gpointer user_data) +{ + if (GMIME_IS_PART (mobj)) + return handle_part + (msg, GMIME_PART(mobj), parent, + opts, index, decrypted, func, user_data); + else if (GMIME_IS_MESSAGE_PART (mobj)) + return handle_message_part + (msg, GMIME_MESSAGE_PART(mobj), + parent, opts, index, decrypted, func, user_data); + else if ((opts & MU_MSG_OPTION_VERIFY) && + GMIME_IS_MULTIPART_SIGNED (mobj)) { + check_signature + (msg, GMIME_MULTIPART_SIGNED (mobj), opts); + return handle_multipart + (msg, GMIME_MULTIPART (mobj), mobj, opts, + index, decrypted, func, user_data); + } else if ((opts & MU_MSG_OPTION_DECRYPT) && + GMIME_IS_MULTIPART_ENCRYPTED (mobj)) + return handle_encrypted_part + (msg, GMIME_MULTIPART_ENCRYPTED (mobj), + opts, index, func, user_data); + else if (GMIME_IS_MULTIPART (mobj)) + return handle_multipart + (msg, GMIME_MULTIPART (mobj), parent, opts, + index, decrypted, func, user_data); + return TRUE; +} + + +gboolean +mu_msg_part_foreach (MuMsg *msg, MuMsgOptions opts, + MuMsgPartForeachFunc func, gpointer user_data) +{ + unsigned index; + + index = 1; + g_return_val_if_fail (msg, FALSE); + + if (!mu_msg_load_msg_file (msg, NULL)) + return FALSE; + + return handle_mime_object (msg, + msg->_file->_mime_msg->mime_part, + (GMimeObject *) msg->_file->_mime_msg, + opts, + &index, + FALSE, + func, + user_data); +} + + +static gboolean +write_part_to_fd (GMimePart *part, int fd, GError **err) +{ + GMimeStream *stream; + GMimeDataWrapper *wrapper; + gboolean rv; + + stream = g_mime_stream_fs_new (fd); + if (!GMIME_IS_STREAM(stream)) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "failed to create stream"); + return FALSE; + } + g_mime_stream_fs_set_owner (GMIME_STREAM_FS(stream), FALSE); + + wrapper = g_mime_part_get_content (part); + if (!GMIME_IS_DATA_WRAPPER(wrapper)) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "failed to create wrapper"); + g_object_unref (stream); + return FALSE; + } + g_object_ref (part); /* FIXME: otherwise, the unrefs below + * give errors...*/ + + if (g_mime_data_wrapper_write_to_stream (wrapper, stream) == -1) { + rv = FALSE; + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "failed to write to stream"); + } else + rv = TRUE; + + /* g_object_unref (wrapper); we don't own it */ + g_object_unref (stream); + + return rv; +} + + + +static gboolean +write_object_to_fd (GMimeObject *obj, int fd, GError **err) +{ + gchar *str; + str = g_mime_object_to_string (obj, NULL); + + if (!str) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "could not get string from object"); + return FALSE; + } + + if (write (fd, str, strlen(str)) == -1) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "failed to write object: %s", + strerror(errno)); + return FALSE; + } + + return TRUE; +} + + +static gboolean +save_object (GMimeObject *obj, MuMsgOptions opts, const char *fullpath, + GError **err) +{ + int fd; + gboolean rv; + gboolean use_existing, overwrite; + + use_existing = opts & MU_MSG_OPTION_USE_EXISTING; + overwrite = opts & MU_MSG_OPTION_OVERWRITE; + + /* don't try to overwrite when we already have it; useful when + * you're sure it's not a different file with the same name */ + if (use_existing && access (fullpath, F_OK) == 0) + return TRUE; + + /* ok, try to create the file */ + fd = mu_util_create_writeable_fd (fullpath, 0600, overwrite); + if (fd == -1) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, + "could not open '%s' for writing: %s", + fullpath, errno ? strerror(errno) : "error"); + return FALSE; + } + + if (GMIME_IS_PART (obj)) + rv = write_part_to_fd ((GMimePart*)obj, fd, err); + else + rv = write_object_to_fd (obj, fd, err); + + if (close (fd) != 0 && !err) { /* don't write on top of old err */ + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, + "could not close '%s': %s", + fullpath, errno ? strerror(errno) : "error"); + return FALSE; + } + + return rv; +} + + +gchar* +mu_msg_part_get_path (MuMsg *msg, MuMsgOptions opts, + const char* targetdir, unsigned index, GError **err) +{ + char *fname, *filepath; + GMimeObject* mobj; + + g_return_val_if_fail (msg, NULL); + + if (!mu_msg_load_msg_file (msg, NULL)) + return NULL; + + mobj = get_mime_object_at_index (msg, opts, index); + if (!mobj){ + mu_util_g_set_error (err, MU_ERROR_GMIME, + "cannot find part %u", index); + return NULL; + } + + fname = mime_part_get_filename (mobj, index, TRUE); + filepath = g_build_path (G_DIR_SEPARATOR_S, targetdir ? targetdir : "", + fname, NULL); + + /* Unref it since it was referenced earlier by + * get_mime_object_at_index */ + g_object_unref (mobj); + g_free (fname); + + return filepath; +} + + + +gchar* +mu_msg_part_get_cache_path (MuMsg *msg, MuMsgOptions opts, guint partid, + GError **err) +{ + char *dirname, *filepath; + const char* path; + + g_return_val_if_fail (msg, NULL); + + if (!mu_msg_load_msg_file (msg, NULL)) + return NULL; + + path = mu_msg_get_path (msg); + + /* g_compute_checksum_for_string may be better, but requires + * rel. new glib (2.16) */ + dirname = g_strdup_printf ("%s%c%x%c%u", + mu_util_cache_dir(), G_DIR_SEPARATOR, + g_str_hash (path), G_DIR_SEPARATOR, + partid); + + if (!mu_util_create_dir_maybe (dirname, 0700, FALSE)) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "failed to create dir %s", dirname); + g_free (dirname); + return NULL; + } + + filepath = mu_msg_part_get_path (msg, opts, dirname, partid, err); + g_free (dirname); + + return filepath; +} + + +gboolean +mu_msg_part_save (MuMsg *msg, MuMsgOptions opts, + const char *fullpath, guint partidx, GError **err) +{ + gboolean rv; + GMimeObject *part; + + g_return_val_if_fail (msg, FALSE); + g_return_val_if_fail (fullpath, FALSE); + g_return_val_if_fail (!((opts & MU_MSG_OPTION_OVERWRITE) && + (opts & MU_MSG_OPTION_USE_EXISTING)), FALSE); + + rv = FALSE; + + if (!mu_msg_load_msg_file (msg, err)) + return rv; + + part = get_mime_object_at_index (msg, opts, partidx); + + /* special case: convert a message-part into a message */ + if (GMIME_IS_MESSAGE_PART (part)) + part = (GMimeObject*)g_mime_message_part_get_message + (GMIME_MESSAGE_PART (part)); + + if (!part) + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "part %u does not exist", partidx); + else if (!GMIME_IS_PART(part) && !GMIME_IS_MESSAGE(part)) + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_GMIME, + "unexpected type %s for part %u", + G_OBJECT_TYPE_NAME((GObject*)part), + partidx); + else + rv = save_object (part, opts, fullpath, err); + + g_clear_object(&part); + + return rv; +} + + +gchar* +mu_msg_part_save_temp (MuMsg *msg, MuMsgOptions opts, guint partidx, + GError **err) +{ + gchar *filepath; + + filepath = mu_msg_part_get_cache_path (msg, opts, partidx, err); + if (!filepath) + return NULL; + + if (!mu_msg_part_save (msg, opts, filepath, partidx, err)) { + g_free (filepath); + return NULL; + } + + return filepath; +} + +static gboolean +match_cid (MuMsgPart *mpart, const char *cid) +{ + const char *this_cid; + + this_cid = g_mime_object_get_content_id ((GMimeObject*)mpart->data); + + return g_strcmp0 (this_cid, cid) ? TRUE : FALSE; +} + +int +mu_msg_find_index_for_cid (MuMsg *msg, MuMsgOptions opts, + const char *sought_cid) +{ + const char* cid; + + g_return_val_if_fail (msg, -1); + g_return_val_if_fail (sought_cid, -1); + + if (!mu_msg_load_msg_file (msg, NULL)) + return -1; + + cid = g_str_has_prefix (sought_cid, "cid:") ? + sought_cid + 4 : sought_cid; + + return get_matching_part_index (msg, opts, + (MuMsgPartMatchFunc)match_cid, + (gpointer)cid); +} + +struct _RxMatchData { + GSList *_lst; + const GRegex *_rx; + guint _idx; +}; +typedef struct _RxMatchData RxMatchData; + + +static void +match_filename_rx (MuMsg *msg, MuMsgPart *mpart, RxMatchData *mdata) +{ + char *fname; + + fname = mu_msg_part_get_filename (mpart, FALSE); + if (!fname) + return; + + if (g_regex_match (mdata->_rx, fname, 0, NULL)) + mdata->_lst = g_slist_prepend (mdata->_lst, + GUINT_TO_POINTER(mpart->index)); + g_free (fname); +} + + +GSList* +mu_msg_find_files (MuMsg *msg, MuMsgOptions opts, const GRegex *pattern) +{ + RxMatchData mdata; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (pattern, NULL); + + if (!mu_msg_load_msg_file (msg, NULL)) + return NULL; + + mdata._lst = NULL; + mdata._rx = pattern; + mdata._idx = 0; + + mu_msg_part_foreach (msg, opts, + (MuMsgPartForeachFunc)match_filename_rx, + &mdata); + return mdata._lst; +} + + +gboolean +mu_msg_part_maybe_attachment (MuMsgPart *part) +{ + g_return_val_if_fail (part, FALSE); + + /* attachments must be leaf parts */ + if (!(part->part_type & MU_MSG_PART_TYPE_LEAF)) + return FALSE; + + /* parts other than text/plain, text/html are considered + * attachments as well */ + if (!(part->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN) && + !(part->part_type & MU_MSG_PART_TYPE_TEXT_HTML)) + return TRUE; + + return part->part_type & MU_MSG_PART_TYPE_ATTACHMENT ? TRUE : FALSE; +} diff --git a/lib/mu-msg-part.h b/lib/mu-msg-part.h new file mode 100644 index 0000000..4f26ddf --- /dev/null +++ b/lib/mu-msg-part.h @@ -0,0 +1,268 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_PART_H__ +#define __MU_MSG_PART_H__ + +#include <glib.h> +#include <unistd.h> /* for ssize_t */ + +#define SIG_STATUS_REPORT "sig-status-report" + +G_BEGIN_DECLS + +enum _MuMsgPartType { + MU_MSG_PART_TYPE_NONE = 0, + + /* MIME part without children */ + MU_MSG_PART_TYPE_LEAF = 1 << 1, + /* an RFC822 message part? */ + MU_MSG_PART_TYPE_MESSAGE = 1 << 2, + /* disposition inline? */ + MU_MSG_PART_TYPE_INLINE = 1 << 3, + /* disposition attachment? */ + MU_MSG_PART_TYPE_ATTACHMENT = 1 << 4, + /* a signed part? */ + MU_MSG_PART_TYPE_SIGNED = 1 << 5, + /* an encrypted part? */ + MU_MSG_PART_TYPE_ENCRYPTED = 1 << 6, + /* a decrypted part? */ + MU_MSG_PART_TYPE_DECRYPTED = 1 << 7, + /* a text/plain part? */ + MU_MSG_PART_TYPE_TEXT_PLAIN = 1 << 8, + /* a text/html part? */ + MU_MSG_PART_TYPE_TEXT_HTML = 1 << 9 +}; +typedef enum _MuMsgPartType MuMsgPartType; + + +/* the signature status */ +enum _MuMsgPartSigStatus { + MU_MSG_PART_SIG_STATUS_UNSIGNED = 0, + + MU_MSG_PART_SIG_STATUS_GOOD, + MU_MSG_PART_SIG_STATUS_BAD, + MU_MSG_PART_SIG_STATUS_ERROR, + MU_MSG_PART_SIG_STATUS_FAIL +}; +typedef enum _MuMsgPartSigStatus MuMsgPartSigStatus; + +typedef struct { + MuMsgPartSigStatus verdict; + const char *report; + const char *signers; +} MuMsgPartSigStatusReport; + +/** + * destroy a MuMsgPartSignatureStatusReport object + * + * @param report a MuMsgPartSignatureStatusReport object + */ +void mu_msg_part_sig_status_report_destroy (MuMsgPartSigStatusReport *report); + + +struct _MuMsgPart { + + /* index of this message part */ + unsigned index; + + /* cid */ + /* const char *content_id; */ + + /* content-type: type/subtype, ie. text/plain */ + const char *type; + const char *subtype; + + /* size of the part; or < 0 if unknown */ + ssize_t size; + + gpointer data; /* opaque data */ + + MuMsgPartType part_type; + MuMsgPartSigStatusReport *sig_status_report; + }; +typedef struct _MuMsgPart MuMsgPart; + +/** + * get some appropriate file name for the mime-part + * + * @param mpart a MuMsgPart + * @param construct_if_needed if there is no + * real filename, construct one. + * + * @return the file name (free with g_free) + */ +char *mu_msg_part_get_filename (MuMsgPart *mpart, gboolean construct_if_needed) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get appropriate content id for the mime-part + * + * @param mpart a MuMsgPart + * + * @return const content id + */ +const gchar* +mu_msg_part_get_content_id (MuMsgPart *mpart) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * get the text in the MuMsgPart (ie. in its GMimePart) + * + * @param msg a MuMsg + * @param part a MuMsgPart + * @param opts MuMsgOptions + * + * @return utf8 string for this MIME part, to be freed by caller + */ +char* mu_msg_part_get_text (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * does this msg part look like an attachment? + * + * @param part a message part + * + * @return TRUE if it looks like an attachment, FALSE otherwise + */ +gboolean mu_msg_part_maybe_attachment (MuMsgPart *part); + + +/** + * save a specific attachment to some targetdir + * + * @param msg a valid MuMsg instance + * @param opts mu-message options (OVERWRITE/USE_EXISTING) + * @gchar filepath the filepath to save + * @param partidx index of the attachment you want to save + * @param err receives error information (when function returns NULL) + * + * @return full path to the message part saved or NULL in case or + * error; free with g_free + */ +gboolean mu_msg_part_save (MuMsg *msg, MuMsgOptions opts, + const char *filepath, guint partidx, + GError **err); + + +/** + * save a message part to a temporary file and return the full path to + * this file + * + * @param msg a MuMsg message + * @param opts mu-message options (OVERWRITE/USE_EXISTING) + * @param partidx index of the part to save + * @param err receives error information if any + * + * @return the full path to the temp file, or NULL in case of error + */ +gchar* mu_msg_part_save_temp (MuMsg *msg, MuMsgOptions opts, + guint partidx, GError **err) + G_GNUC_WARN_UNUSED_RESULT; + + + +/** + * get a filename for the saving the message part; try the filename + * specified for the message part if any, otherwise determine a unique + * name based on the partidx and the message path + * + * @param msg a msg + * @param opts mu-message options + * @param targetdir where to store the part + * @param partidx the part for which to determine a filename + * @param err receives error information (when function returns NULL) + * + * @return a filepath (g_free when done with it) or NULL in case of error + */ +gchar* mu_msg_part_get_path (MuMsg *msg, MuMsgOptions opts, + const char* targetdir, + guint partidx, GError **err) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get a full path name for a file for saving the message part INDEX; + * this path is unique (1:1) for this particular message and part for + * this user. Thus, it can be used as a cache. + * + * Will create the directory if needed. + * + * @param msg a msg + * @param opts mu-message options + * @param partidx the part for which to determine a filename + * @param err receives error information (when function returns NULL) + * + * @return a filepath (g_free when done with it) or NULL in case of error + */ +gchar* mu_msg_part_get_cache_path (MuMsg *msg, MuMsgOptions opts, + guint partidx, GError **err) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get the part index for the message part with a certain content-id + * + * @param msg a message + * @param content_id a content-id to search + * + * @return the part index number of the found part, or -1 if it was not found + */ +int mu_msg_find_index_for_cid (MuMsg *msg, MuMsgOptions opts, const char* content_id); + + + +/** + * retrieve a list of indices for mime-parts with filenames matching a regex + * + * @param msg a message + * @param opts + * @param a regular expression to match the filename with + * + * @return a list with indices for the files matching the pattern; the + * indices are the GPOINTER_TO_UINT(lst->data) of the list. They must + * be freed with g_slist_free + */ +GSList* mu_msg_find_files (MuMsg *msg, MuMsgOptions opts, const GRegex *pattern); + + +typedef void (*MuMsgPartForeachFunc) (MuMsg *msg, MuMsgPart*, gpointer); + + +/** + * call a function for each of the mime part in a message + * + * @param msg a valid MuMsg* instance + * @param func a callback function to call for each contact; when + * the callback does not return TRUE, it won't be called again + * @param user_data a user-provide pointer that will be passed to the callback + * @param options, bit-wise OR'ed + * + * @return FALSE in case of error, TRUE otherwise + */ +gboolean mu_msg_part_foreach (MuMsg *msg, MuMsgOptions opts, + MuMsgPartForeachFunc func, gpointer user_data); + +G_END_DECLS + +#endif /*__MU_MSG_PART_H__*/ diff --git a/lib/mu-msg-prio.c b/lib/mu-msg-prio.c new file mode 100644 index 0000000..96a959a --- /dev/null +++ b/lib/mu-msg-prio.c @@ -0,0 +1,65 @@ +/* +** Copyright (C) 2012-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-msg-prio.h" + + +const char* +mu_msg_prio_name (MuMsgPrio prio) +{ + switch (prio) { + case MU_MSG_PRIO_LOW : return "low"; + case MU_MSG_PRIO_NORMAL : return "normal"; + case MU_MSG_PRIO_HIGH : return "high"; + default : g_return_val_if_reached (NULL); + } +} + +MuMsgPrio +mu_msg_prio_from_char (char k) +{ + g_return_val_if_fail (k == 'l' || k == 'n' || k == 'h', + MU_MSG_PRIO_NONE); + return (MuMsgPrio)k; +} + +char +mu_msg_prio_char (MuMsgPrio prio) +{ + if (!(prio == 'l' || prio == 'n' || prio == 'h')) { + g_warning ("prio: %c", (char)prio); + } + + + g_return_val_if_fail (prio == 'l' || prio == 'n' || prio == 'h', + 0); + + return (char)prio; +} + + +void +mu_msg_prio_foreach (MuMsgPrioForeachFunc func, gpointer user_data) +{ + g_return_if_fail (func); + + func (MU_MSG_PRIO_LOW, user_data); + func (MU_MSG_PRIO_NORMAL, user_data); + func (MU_MSG_PRIO_HIGH, user_data); +} diff --git a/lib/mu-msg-prio.h b/lib/mu-msg-prio.h new file mode 100644 index 0000000..df3e8c3 --- /dev/null +++ b/lib/mu-msg-prio.h @@ -0,0 +1,84 @@ +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_PRIO_H__ +#define __MU_MSG_PRIO_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +enum _MuMsgPrio { + MU_MSG_PRIO_LOW = 'l', + MU_MSG_PRIO_NORMAL = 'n', + MU_MSG_PRIO_HIGH = 'h' +}; +typedef enum _MuMsgPrio MuMsgPrio; + +static const MuMsgPrio MU_MSG_PRIO_NONE = (MuMsgPrio)0; + + +/** + * get a printable name for the message priority + * (ie., MU_MSG_PRIO_LOW=>"low" etc.) + * + * @param prio a message priority + * + * @return a printable name for this priority + */ +const char* mu_msg_prio_name (MuMsgPrio prio) G_GNUC_CONST; + + +/** + * get the MuMsgPriority corresponding to a one-character shortcut + * ('l'=>MU_MSG_PRIO_, 'n'=>MU_MSG_PRIO_NORMAL or + * 'h'=>MU_MSG_PRIO_HIGH) + * + * @param k a character + * + * @return a message priority + */ +MuMsgPrio mu_msg_prio_from_char (char k) G_GNUC_CONST; + + +/** + * get the one-character shortcut corresponding to a message priority + * ('l'=>MU_MSG_PRIO_, 'n'=>MU_MSG_PRIO_NORMAL or + * 'h'=>MU_MSG_PRIO_HIGH) + * + * @param prio a message priority + * + * @return a shortcut character or 0 in case of error + */ +char mu_msg_prio_char (MuMsgPrio prio) G_GNUC_CONST; + +typedef void (*MuMsgPrioForeachFunc) (MuMsgPrio prio, gpointer user_data); +/** + * call a function for each message priority + * + * @param func a callback function + * @param user_data a user pointer to pass to the callback + */ +void mu_msg_prio_foreach (MuMsgPrioForeachFunc func, gpointer user_data); + + + +G_END_DECLS + +#endif /*__MU_MSG_PRIO_H__*/ diff --git a/lib/mu-msg-priv.h b/lib/mu-msg-priv.h new file mode 100644 index 0000000..8f61719 --- /dev/null +++ b/lib/mu-msg-priv.h @@ -0,0 +1,140 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_PRIV_H__ +#define __MU_MSG_PRIV_H__ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <gmime/gmime.h> +#include <stdlib.h> + +#include <mu-msg.h> +#include <mu-msg-file.h> +#include <mu-msg-doc.h> +#include "mu-msg-part.h" + +G_BEGIN_DECLS + +struct _MuMsgFile { + GMimeMessage *_mime_msg; + time_t _timestamp; + size_t _size; + char _path [PATH_MAX + 1]; + char _maildir [PATH_MAX + 1]; +}; + + +/* we put the the MuMsg definition in this separate -priv file, so we + * can split the mu_msg implementations over separate files */ +struct _MuMsg { + + guint _refcount; + + /* our two backend */ + MuMsgFile *_file; /* based on GMime, ie. a file on disc */ + MuMsgDoc *_doc; /* based on Xapian::Document */ + + /* lists where we push allocated strings / GSLists of string + * so we can free them when the struct gets destroyed (and we + * can return them as 'const to callers) + */ + GSList *_free_later_str; + GSList *_free_later_lst; +}; + + +/** + * convert a GMimePart to a string + * + * @param part a GMimePart + * @param err will receive TRUE if there was an error, FALSE + * otherwise. Must NOT be NULL. + * + * @return utf8 string for this MIME part, to be freed by caller + */ +gchar* mu_msg_mime_part_to_string (GMimePart *part, gboolean *err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * Like g_mime_message_foreach, but will recurse into encrypted parts + * if @param decrypt is TRUE and mu was built with crypto support + * + * @param msg a GMimeMessage + * @param decrypt whether to try to automatically decrypt + * @param func user callback function for each part + * @param user_data user point passed to callback function + * @param err receives error information + * + */ +void mu_mime_message_foreach (GMimeMessage *msg, gboolean decrypt, + GMimeObjectForeachFunc func, + gpointer user_data); + +/** + * callback function to retrieve a password from the user + * + * @param user_id the user name / id to get the password for + * @param prompt_ctx a string containing some helpful context for the prompt + * @param reprompt whether this is a reprompt after an earlier, incorrect password + * @param user_data the user_data pointer passed to mu_msg_part_decrypt_foreach + * + * @return a newly allocated (g_free'able) string + */ +typedef char* (*MuMsgPartPasswordFunc) (const char *user_id, const char *prompt_ctx, + gboolean reprompt, gpointer user_data); + + +/** + * verify the signature of a signed message part + * + * @param sig a signed message part + * @param opts message options + * @param err receive error information + * + * @return a status report object, free with mu_msg_part_sig_status_report_destroy + */ +void mu_msg_crypto_verify_part (GMimeMultipartSigned *sig, + MuMsgOptions opts, + GError **err); + +/** + * decrypt the given encrypted mime multipart + * + * @param enc encrypted part + * @param opts options + * @param password_func callback function to retrieve as password (or NULL) + * @param user_data pointer passed to the password func + * @param err receives error data + * + * @return the decrypted part, or NULL in case of error + */ +GMimeObject* mu_msg_crypto_decrypt_part (GMimeMultipartEncrypted *enc, MuMsgOptions opts, + MuMsgPartPasswordFunc func, gpointer user_data, + GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +G_END_DECLS + +#endif /*__MU_MSG_PRIV_H__*/ diff --git a/lib/mu-msg-sexp.c b/lib/mu-msg-sexp.c new file mode 100644 index 0000000..359ae22 --- /dev/null +++ b/lib/mu-msg-sexp.c @@ -0,0 +1,635 @@ +/* +** Copyright (C) 2011-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include <string.h> +#include <ctype.h> + +#include "utils/mu-str.h" +#include "mu-msg.h" +#include "mu-msg-iter.h" +#include "mu-msg-part.h" +#include "mu-maildir.h" + +static void +append_sexp_attr_list (GString *gstr, const char* elm, const GSList *lst) +{ + const GSList *cur; + + if (!lst) + return; /* empty list, don't include */ + + g_string_append_printf (gstr, "\t:%s ( ", elm); + + for (cur = lst; cur; cur = g_slist_next(cur)) { + char *str; + str = mu_str_escape_c_literal + ((const gchar*)cur->data, TRUE); + g_string_append_printf (gstr, "%s ", str); + g_free (str); + } + + g_string_append (gstr, ")\n"); +} + +static void +append_sexp_attr (GString *gstr, const char* elm, const char *str) +{ + gchar *esc, *utf8, *cur; + + if (!str || strlen(str) == 0) + return; /* empty: don't include */ + + + utf8 = mu_str_utf8ify (str); + + for (cur = utf8; *cur; ++cur) + if (iscntrl(*cur)) + *cur = ' '; + + esc = mu_str_escape_c_literal (utf8, TRUE); + g_free (utf8); + + g_string_append_printf (gstr, "\t:%s %s\n", elm, esc); + g_free (esc); +} + +static void +append_sexp_body_attr (GString *gstr, const char* elm, const char *str) +{ + gchar *esc; + + if (!str || strlen(str) == 0) + return; /* empty: don't include */ + + esc = mu_str_escape_c_literal (str, TRUE); + + g_string_append_printf (gstr, "\t:%s %s\n", elm, esc); + g_free (esc); +} + +struct _ContactData { + gboolean from, to, cc, bcc, reply_to; + GString *gstr; + MuMsgContactType prev_ctype; +}; +typedef struct _ContactData ContactData; + +static gchar* +get_name_email_pair (MuMsgContact *c) +{ + gchar *name, *email, *pair; + + name = (char*)mu_msg_contact_name(c); + email = (char*)mu_msg_contact_email(c); + + name = name ? mu_str_escape_c_literal (name, TRUE) : NULL; + email = email ? mu_str_escape_c_literal (email, TRUE) : NULL; + + pair = g_strdup_printf ("(%s . %s)", + name ? name : "nil", + email ? email : "nil"); + g_free (name); + g_free (email); + + return pair; +} + + +static void +add_prefix_maybe (GString *gstr, gboolean *field, const char *prefix) +{ + /* if there's nothing in the field yet, add the prefix */ + if (!*field) + g_string_append (gstr, prefix); + + *field = TRUE; +} + +static gboolean +each_contact (MuMsgContact *c, ContactData *cdata) +{ + char *pair; + MuMsgContactType ctype; + + ctype = mu_msg_contact_type (c); + + /* if the current type is not the previous type, close the + * previous first */ + if (cdata->prev_ctype != ctype && cdata->prev_ctype != (unsigned)-1) + g_string_append (cdata->gstr, ")\n"); + + switch (ctype) { + + case MU_MSG_CONTACT_TYPE_FROM: + add_prefix_maybe (cdata->gstr, &cdata->from, "\t:from ("); + break; + case MU_MSG_CONTACT_TYPE_TO: + add_prefix_maybe (cdata->gstr, &cdata->to, "\t:to ("); + break; + case MU_MSG_CONTACT_TYPE_CC: + add_prefix_maybe (cdata->gstr, &cdata->cc, "\t:cc ("); + break; + case MU_MSG_CONTACT_TYPE_BCC: + add_prefix_maybe (cdata->gstr, &cdata->bcc, "\t:bcc ("); + break; + case MU_MSG_CONTACT_TYPE_REPLY_TO: + add_prefix_maybe (cdata->gstr, &cdata->reply_to, + "\t:reply-to ("); + break; + default: g_return_val_if_reached (FALSE); + } + + cdata->prev_ctype = ctype; + + pair = get_name_email_pair (c); + g_string_append (cdata->gstr, pair); + g_free (pair); + + return TRUE; +} + +static void +maybe_append_list_post (GString *gstr, MuMsg *msg) +{ + /* some mailing lists do not set the reply-to; see pull #1278. So for + * those cases, check the List-Post address and use that instead */ + + GMatchInfo *minfo; + GRegex *rx; + const char* list_post; + + list_post = mu_msg_get_header (msg, "List-Post"); + if (!list_post) + return; + + rx = g_regex_new ("<?mailto:([a-z0-9!@#$%&'*+-/=?^_`{|}~]+)>?", + G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL); + g_return_if_fail(rx); + + if (g_regex_match (rx, list_post, 0, &minfo)) { + char *addr; + addr = g_match_info_fetch (minfo, 1); + g_string_append_printf (gstr,"\t:list-post ((nil . \"%s\"))\n", addr); + g_free(addr); + } + + g_match_info_free (minfo); + g_regex_unref (rx); +} + + + +static void +append_sexp_contacts (GString *gstr, MuMsg *msg) +{ + ContactData cdata; + + cdata.from = cdata.to = cdata.cc = cdata.bcc + = cdata.reply_to = FALSE; + cdata.gstr = gstr; + cdata.prev_ctype = (unsigned)-1; + + mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact, + &cdata); + if (cdata.from || cdata.to || cdata.cc || cdata.bcc || cdata.reply_to) + gstr = g_string_append (gstr, ")\n"); + + maybe_append_list_post (gstr, msg); +} + +struct _FlagData { + char *flagstr; + MuFlags msgflags; +}; +typedef struct _FlagData FlagData; + +static void +each_flag (MuFlags flag, FlagData *fdata) +{ + if (!(flag & fdata->msgflags)) + return; + + if (!fdata->flagstr) + fdata->flagstr = g_strdup (mu_flag_name(flag)); + else { + gchar *tmp; + tmp = g_strconcat (fdata->flagstr, " ", + mu_flag_name(flag), NULL); + g_free (fdata->flagstr); + fdata->flagstr = tmp; + } +} + +static void +append_sexp_flags (GString *gstr, MuMsg *msg) +{ + FlagData fdata; + + fdata.msgflags = mu_msg_get_flags (msg); + fdata.flagstr = NULL; + + mu_flags_foreach ((MuFlagsForeachFunc)each_flag, &fdata); + if (fdata.flagstr) + g_string_append_printf (gstr, "\t:flags (%s)\n", + fdata.flagstr); + g_free (fdata.flagstr); +} + +static char* +get_temp_file (MuMsg *msg, MuMsgOptions opts, unsigned index) +{ + char *path; + GError *err; + + err = NULL; + path = mu_msg_part_get_cache_path (msg, opts, index, &err); + if (!path) + goto errexit; + + if (!mu_msg_part_save (msg, opts, path, index, &err)) + goto errexit; + + return path; + +errexit: + g_warning ("failed to save mime part: %s", + err->message ? err->message : "something went wrong"); + g_clear_error (&err); + g_free (path); + return NULL; +} + + +static gchar* +get_temp_file_maybe (MuMsg *msg, MuMsgPart *part, MuMsgOptions opts) +{ + char *tmp, *tmpfile; + + opts |= MU_MSG_OPTION_USE_EXISTING; + + if (!(opts & MU_MSG_OPTION_EXTRACT_IMAGES) || + g_ascii_strcasecmp (part->type, "image") != 0) + return NULL; + + tmp = get_temp_file (msg, opts, part->index); + if (!tmp) + return NULL; + + tmpfile = mu_str_escape_c_literal (tmp, TRUE); + g_free (tmp); + return tmpfile; +} + + +struct _PartInfo { + char *parts; + MuMsgOptions opts; +}; +typedef struct _PartInfo PartInfo; + +static char* +sig_verdict (MuMsgPart *mpart) +{ + char *signers, *s; + const char *verdict; + MuMsgPartSigStatusReport *report; + + report = mpart->sig_status_report; + if (!report) + return g_strdup (""); + + switch (report->verdict) { + case MU_MSG_PART_SIG_STATUS_GOOD: + verdict = ":signature verified"; + break; + case MU_MSG_PART_SIG_STATUS_BAD: + verdict = ":signature bad"; + break; + case MU_MSG_PART_SIG_STATUS_ERROR: + verdict = ":signature unverified"; + break; + default: + verdict = ""; + break; + } + + if (!report->signers) + return g_strdup (verdict); + + signers = mu_str_escape_c_literal (report->signers, TRUE); + s = g_strdup_printf ("%s :signers %s", verdict, signers); + g_free (signers); + + return s; +} + +static const char* +dec_verdict (MuMsgPart *mpart) +{ + MuMsgPartType ptype; + + ptype = mpart->part_type; + + if (ptype & MU_MSG_PART_TYPE_DECRYPTED) + return ":decryption succeeded"; + else if (ptype & MU_MSG_PART_TYPE_ENCRYPTED) + return ":decryption failed"; + else + return ""; +} + + +static gchar * +get_part_type_string (MuMsgPartType ptype) +{ + GString *gstr; + unsigned u; + struct PartTypes { + MuMsgPartType ptype; + const char* name; + } ptypes[] = { + { MU_MSG_PART_TYPE_LEAF, "leaf" }, + { MU_MSG_PART_TYPE_MESSAGE, "message" }, + { MU_MSG_PART_TYPE_INLINE, "inline" }, + { MU_MSG_PART_TYPE_ATTACHMENT, "attachment" }, + { MU_MSG_PART_TYPE_SIGNED, "signed" }, + { MU_MSG_PART_TYPE_ENCRYPTED, "encrypted" } + }; + + gstr = g_string_sized_new (100); /* more than enough */ + gstr = g_string_append_c (gstr, '('); + + for (u = 0; u!= G_N_ELEMENTS(ptypes); ++u) { + if (ptype & ptypes[u].ptype) { + if (gstr->len > 1) + gstr = g_string_append_c (gstr, ' '); + gstr = g_string_append (gstr, ptypes[u].name); + } + } + + gstr = g_string_append_c (gstr, ')'); + + return g_string_free (gstr, FALSE); +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + char *name, *encname, *tmp, *parttype; + char *tmpfile, *cidesc, *verdict; + const char *cid; + + name = mu_msg_part_get_filename (part, TRUE); + encname = name ? + mu_str_escape_c_literal(name, TRUE) : + g_strdup("\"noname\""); + g_free (name); + + tmpfile = get_temp_file_maybe (msg, part, pinfo->opts); + parttype = get_part_type_string (part->part_type); + verdict = sig_verdict (part); + + cid = mu_msg_part_get_content_id(part); + cidesc = cid ? mu_str_escape_c_literal(cid, TRUE) : NULL; + + tmp = g_strdup_printf + ("%s(:index %d :name %s :mime-type \"%s/%s\"%s%s " + ":type %s " + ":attachment %s %s%s :size %i %s %s)", + pinfo->parts ? pinfo->parts: "", + part->index, + encname, + part->type ? part->type : "application", + part->subtype ? part->subtype : "octet-stream", + tmpfile ? " :temp" : "", tmpfile ? tmpfile : "", + parttype, + mu_msg_part_maybe_attachment (part) ? "t" : "nil", + cidesc ? " :cid" : "", cidesc ? cidesc : "", + (int)part->size, + verdict, + dec_verdict (part)); + + g_free (encname); + g_free (tmpfile); + g_free (parttype); + g_free (verdict); + g_free (cidesc); + + g_free (pinfo->parts); + pinfo->parts = tmp; +} + + +static void +append_sexp_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts) +{ + PartInfo pinfo; + + pinfo.parts = NULL; + pinfo.opts = opts; + + if (!mu_msg_part_foreach (msg, opts, (MuMsgPartForeachFunc)each_part, + &pinfo)) { + /* do nothing */ + } else if (pinfo.parts) { + g_string_append_printf (gstr, "\t:parts (%s)\n", pinfo.parts); + g_free (pinfo.parts); + } +} + +static void +append_sexp_thread_info (GString *gstr, const MuMsgIterThreadInfo *ti) +{ + g_string_append_printf + (gstr, "\t:thread (:path \"%s\" :level %u%s%s%s%s%s)\n", + ti->threadpath, + ti->level, + ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD ? + " :first-child t" : "", + ti->prop & MU_MSG_ITER_THREAD_PROP_LAST_CHILD ? + " :last-child t" : "", + ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT ? + " :empty-parent t" : "", + ti->prop & MU_MSG_ITER_THREAD_PROP_DUP ? + " :duplicate t" : "", + ti->prop & MU_MSG_ITER_THREAD_PROP_HAS_CHILD ? + " :has-child t" : ""); +} + +static void +append_sexp_param (GString *gstr, const GSList *param) +{ + for (;param; param = g_slist_next (param)) { + const char *str; + char *key, *value; + + str = param->data; + key = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup (""); + + param = g_slist_next (param); + str = param->data; + value = str ? mu_str_escape_c_literal (str, FALSE) : g_strdup (""); + + g_string_append_printf (gstr, "(\"%s\" . \"%s\")", key, value); + g_free (key); + g_free (value); + + if (param->next) + g_string_append_c (gstr, ' '); + } +} + +static void +append_message_file_parts (GString *gstr, MuMsg *msg, MuMsgOptions opts) +{ + const char *str; + GError *err; + const GSList *params; + + err = NULL; + + if (!mu_msg_load_msg_file (msg, &err)) { + g_warning ("failed to load message file: %s", + err ? err->message : "some error occurred"); + g_clear_error (&err); + return; + } + + append_sexp_parts (gstr, msg, opts); + append_sexp_contacts (gstr, msg); + + /* add the user-agent / x-mailer */ + str = mu_msg_get_header (msg, "User-Agent"); + if (str || (str = mu_msg_get_header (msg, "X-Mailer"))) + append_sexp_attr (gstr, "user-agent", str); + + params = mu_msg_get_body_text_content_type_parameters (msg, opts); + if (params) { + g_string_append_printf (gstr, "\t:body-txt-params ("); + append_sexp_param (gstr, params); + g_string_append_printf (gstr, ")\n"); + } + + append_sexp_body_attr (gstr, "body-txt", + mu_msg_get_body_text(msg, opts)); + append_sexp_body_attr (gstr, "body-html", + mu_msg_get_body_html(msg, opts)); +} + +static void +append_sexp_date_and_size (GString *gstr, MuMsg *msg) +{ + time_t t; + size_t s; + + t = mu_msg_get_date (msg); + if (t == (time_t)-1) /* invalid date? */ + t = 0; + + s = mu_msg_get_size (msg); + if (s == (size_t)-1) /* invalid size? */ + s = 0; + + g_string_append_printf + (gstr, + "\t:date (%d %u 0)\n\t:size %u\n", + (unsigned)(t >> 16), + (unsigned)(t & 0xffff), + (unsigned)s); +} + + +static void +append_sexp_tags (GString *gstr, MuMsg *msg) +{ + const GSList *tags, *t; + gchar *tagesc; + GString *tagstr = g_string_new(""); + + tags = mu_msg_get_tags (msg); + + for(t = tags; t; t = t->next) { + if (t != tags) + g_string_append(tagstr, " "); + + tagesc = mu_str_escape_c_literal((const gchar *)t->data, TRUE); + g_string_append(tagstr, tagesc); + + g_free(tagesc); + } + + if (tagstr->len > 0) + g_string_append_printf (gstr, "\t:tags (%s)\n", + tagstr->str); + g_string_free (tagstr, TRUE); +} + +char* +mu_msg_to_sexp (MuMsg *msg, unsigned docid, const MuMsgIterThreadInfo *ti, + MuMsgOptions opts) +{ + GString *gstr; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (!((opts & MU_MSG_OPTION_HEADERS_ONLY) && + (opts & MU_MSG_OPTION_EXTRACT_IMAGES)),NULL); + gstr = g_string_sized_new + ((opts & MU_MSG_OPTION_HEADERS_ONLY) ? 1024 : 8192); + + if (docid == 0) + g_string_append (gstr, "(\n"); + else + g_string_append_printf (gstr, "(\n\t:docid %u\n", docid); + + if (ti) + append_sexp_thread_info (gstr, ti); + + append_sexp_attr (gstr, "subject", mu_msg_get_subject (msg)); + + /* in the no-headers-only case (see below) we get a more complete list + * of contacts, so no need to get them here if that's the case */ + if (opts & MU_MSG_OPTION_HEADERS_ONLY) + append_sexp_contacts (gstr, msg); + + append_sexp_date_and_size (gstr, msg); + + append_sexp_attr (gstr, "message-id", mu_msg_get_msgid (msg)); + append_sexp_attr (gstr, "mailing-list", + mu_msg_get_mailing_list (msg)); + append_sexp_attr (gstr, "path", mu_msg_get_path (msg)); + append_sexp_attr (gstr, "maildir", mu_msg_get_maildir (msg)); + g_string_append_printf (gstr, "\t:priority %s\n", + mu_msg_prio_name(mu_msg_get_prio(msg))); + append_sexp_flags (gstr, msg); + append_sexp_tags (gstr, msg); + + append_sexp_attr_list (gstr, "references", + mu_msg_get_references (msg)); + append_sexp_attr (gstr, "in-reply-to", + mu_msg_get_header (msg, "In-Reply-To")); + + /* headers are retrieved from the database, views from the + * message file file attr things can only be gotten from the + * file (ie., mu view), not from the database (mu find). */ + if (!(opts & MU_MSG_OPTION_HEADERS_ONLY)) + append_message_file_parts (gstr, msg, opts); + + g_string_append (gstr, ")\n"); + return g_string_free (gstr, FALSE); +} diff --git a/lib/mu-msg.c b/lib/mu-msg.c new file mode 100644 index 0000000..976f3c9 --- /dev/null +++ b/lib/mu-msg.c @@ -0,0 +1,1013 @@ +/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <stdlib.h> +#include <ctype.h> + +#include <gmime/gmime.h> + +#include "mu-msg-priv.h" /* include before mu-msg.h */ +#include "mu-msg.h" +#include "utils/mu-str.h" + +#include "mu-maildir.h" + +/* note, we do the gmime initialization here rather than in + * mu-runtime, because this way we don't need mu-runtime for simple + * cases -- such as our unit tests. Also note that we need gmime init + * even for the doc backend, as we use the address parsing functions + * also there. */ +static gboolean _gmime_initialized = FALSE; + +static void +gmime_init (void) +{ + g_return_if_fail (!_gmime_initialized); + + g_mime_init(); + _gmime_initialized = TRUE; +} + +static void +gmime_uninit (void) +{ + g_return_if_fail (_gmime_initialized); + + g_mime_shutdown(); + _gmime_initialized = FALSE; +} + +static MuMsg* +msg_new (void) +{ + MuMsg *self; + + self = g_slice_new0 (MuMsg); + self->_refcount = 1; + + return self; +} + +MuMsg* +mu_msg_new_from_file (const char *path, const char *mdir, + GError **err) +{ + MuMsg *self; + MuMsgFile *msgfile; + + g_return_val_if_fail (path, NULL); + + if (G_UNLIKELY(!_gmime_initialized)) { + gmime_init (); + atexit (gmime_uninit); + } + + msgfile = mu_msg_file_new (path, mdir, err); + if (!msgfile) + return NULL; + + self = msg_new (); + self->_file = msgfile; + + return self; +} + +MuMsg* +mu_msg_new_from_doc (XapianDocument *doc, GError **err) +{ + MuMsg *self; + MuMsgDoc *msgdoc; + + g_return_val_if_fail (doc, NULL); + + if (G_UNLIKELY(!_gmime_initialized)) { + gmime_init (); + atexit (gmime_uninit); + } + + msgdoc = mu_msg_doc_new (doc, err); + if (!msgdoc) + return NULL; + + self = msg_new (); + self->_doc = msgdoc; + + return self; +} + +static void +mu_msg_destroy (MuMsg *self) +{ + if (!self) + return; + + mu_msg_file_destroy (self->_file); + mu_msg_doc_destroy (self->_doc); + + { /* cleanup the strings / lists we stored */ + mu_str_free_list (self->_free_later_str); + g_slist_foreach (self->_free_later_lst, + (GFunc)mu_str_free_list, NULL); + g_slist_free (self->_free_later_lst); + } + + g_slice_free (MuMsg, self); +} + +MuMsg* +mu_msg_ref (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + + ++self->_refcount; + + return self; +} + +void +mu_msg_unref (MuMsg *self) +{ + g_return_if_fail (self); + g_return_if_fail (self->_refcount >= 1); + + if (--self->_refcount == 0) + mu_msg_destroy (self); +} + +static const gchar* +free_later_str (MuMsg *self, gchar *str) +{ + if (str) + self->_free_later_str = + g_slist_prepend (self->_free_later_str, str); + return str; +} + +static const GSList* +free_later_lst (MuMsg *self, GSList *lst) +{ + if (lst) + self->_free_later_lst = + g_slist_prepend (self->_free_later_lst, lst); + return lst; +} + +/* use this instead of mu_msg_get_path so we don't get into infinite + * regress...*/ +static const char* +get_path (MuMsg *self) +{ + char *val; + gboolean do_free; + + do_free = TRUE; + val = NULL; + + if (self->_doc) + val = mu_msg_doc_get_str_field + (self->_doc, MU_MSG_FIELD_ID_PATH); + + /* not in the cache yet? try to get it from the file backend, + * in case we are using that */ + if (!val && self->_file) + val = mu_msg_file_get_str_field + (self->_file, MU_MSG_FIELD_ID_PATH, &do_free); + + /* shouldn't happen */ + if (!val) + g_warning ("%s: cannot find path", __func__); + + return free_later_str (self, val); +} + +/* for some data, we need to read the message file from disk */ +gboolean +mu_msg_load_msg_file (MuMsg *self, GError **err) +{ + const char *path; + + g_return_val_if_fail (self, FALSE); + + if (self->_file) + return TRUE; /* nothing to do */ + + if (!(path = get_path (self))) { + mu_util_g_set_error (err, MU_ERROR_INTERNAL, + "cannot get path for message"); + return FALSE; + } + + self->_file = mu_msg_file_new (path, NULL, err); + + return (self->_file != NULL); +} + +void +mu_msg_unload_msg_file (MuMsg *msg) +{ + g_return_if_fail (msg); + + mu_msg_file_destroy (msg->_file); + msg->_file = NULL; +} + +static const GSList* +get_str_list_field (MuMsg *self, MuMsgFieldId mfid) +{ + GSList *val; + + val = NULL; + + if (self->_doc && mu_msg_field_xapian_value (mfid)) + val = mu_msg_doc_get_str_list_field (self->_doc, mfid); + else if (mu_msg_field_gmime (mfid)) { + /* if we don't have a file object yet, we need to + * create it from the file on disk */ + if (!mu_msg_load_msg_file (self, NULL)) + return NULL; + val = mu_msg_file_get_str_list_field (self->_file, mfid); + } + + return free_later_lst (self, val); +} + +static const char* +get_str_field (MuMsg *self, MuMsgFieldId mfid) +{ + char *val; + gboolean do_free; + + do_free = TRUE; + val = NULL; + + if (self->_doc && mu_msg_field_xapian_value (mfid)) + val = mu_msg_doc_get_str_field (self->_doc, mfid); + + else if (mu_msg_field_gmime (mfid)) { + /* if we don't have a file object yet, we need to + * create it from the file on disk */ + if (!mu_msg_load_msg_file (self, NULL)) + return NULL; + val = mu_msg_file_get_str_field (self->_file, mfid, &do_free); + } else + val = NULL; + + return do_free ? free_later_str (self, val) : val; +} + +static gint64 +get_num_field (MuMsg *self, MuMsgFieldId mfid) +{ + guint64 val; + + val = -1; + if (self->_doc && mu_msg_field_xapian_value (mfid)) + val = mu_msg_doc_get_num_field (self->_doc, mfid); + else { + /* if we don't have a file object yet, we need to + * create it from the file on disk */ + if (!mu_msg_load_msg_file (self, NULL)) + return -1; + val = mu_msg_file_get_num_field (self->_file, mfid); + } + + return val; +} + +const char* +mu_msg_get_header (MuMsg *self, const char *header) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (header, NULL); + + /* if we don't have a file object yet, we need to + * create it from the file on disk */ + if (!mu_msg_load_msg_file (self, NULL)) + return NULL; + + return free_later_str + (self, mu_msg_file_get_header (self->_file, header)); +} + +time_t +mu_msg_get_timestamp (MuMsg *self) +{ + const char *path; + struct stat statbuf; + + g_return_val_if_fail (self, 0); + + if (self->_file) + return self->_file->_timestamp; + + path = mu_msg_get_path (self); + if (!path || stat (path, &statbuf) < 0) + return 0; + + return statbuf.st_mtime; +} + +const char* +mu_msg_get_path (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_PATH); +} + +const char* +mu_msg_get_subject (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_SUBJECT); +} + +const char* +mu_msg_get_msgid (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_MSGID); +} + +const char* +mu_msg_get_mailing_list (MuMsg *self) +{ + const char *ml; + char *decml; + + g_return_val_if_fail (self, NULL); + + ml = get_str_field (self, MU_MSG_FIELD_ID_MAILING_LIST); + if (!ml) + return NULL; + + decml = g_mime_utils_header_decode_text (NULL, ml); + if (!decml) + return NULL; + + return free_later_str (self, decml); +} + +const char* +mu_msg_get_maildir (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_MAILDIR); +} + +const char* +mu_msg_get_from (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_FROM); +} + +const char* +mu_msg_get_to (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_TO); +} + +const char* +mu_msg_get_cc (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_CC); +} + +const char* +mu_msg_get_bcc (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, MU_MSG_FIELD_ID_BCC); +} + +time_t +mu_msg_get_date (MuMsg *self) +{ + g_return_val_if_fail (self, (time_t)-1); + return (time_t)get_num_field (self, MU_MSG_FIELD_ID_DATE); +} + +MuFlags +mu_msg_get_flags (MuMsg *self) +{ + g_return_val_if_fail (self, MU_FLAG_NONE); + return (MuFlags)get_num_field (self, MU_MSG_FIELD_ID_FLAGS); +} + +size_t +mu_msg_get_size (MuMsg *self) +{ + g_return_val_if_fail (self, (size_t)-1); + return (size_t)get_num_field (self, MU_MSG_FIELD_ID_SIZE); +} + +MuMsgPrio +mu_msg_get_prio (MuMsg *self) +{ + g_return_val_if_fail (self, MU_MSG_PRIO_NORMAL); + return (MuMsgPrio)get_num_field (self, MU_MSG_FIELD_ID_PRIO); +} + +struct _BodyData { + GString *gstr; + gboolean want_html; +}; +typedef struct _BodyData BodyData; + +static void +accumulate_body (MuMsg *msg, MuMsgPart *mpart, BodyData *bdata) +{ + char *txt; + GMimePart *mimepart; + gboolean has_err, is_plain, is_html; + + if (!GMIME_IS_PART(mpart->data)) + return; + if (mpart->part_type & MU_MSG_PART_TYPE_ATTACHMENT) + return; + + mimepart = (GMimePart*)mpart->data; + is_html = mpart->part_type & MU_MSG_PART_TYPE_TEXT_HTML; + is_plain = mpart->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN; + + txt = NULL; + has_err = TRUE; + if ((bdata->want_html && is_html) || (!bdata->want_html && is_plain)) + txt = mu_msg_mime_part_to_string (mimepart, &has_err); + + if (!has_err && txt) + bdata->gstr = g_string_append (bdata->gstr, txt); + + g_free (txt); +} + +static char* +get_body (MuMsg *self, MuMsgOptions opts, gboolean want_html) +{ + BodyData bdata; + + bdata.want_html = want_html; + bdata.gstr = g_string_sized_new (4096); + + /* wipe out some irrelevant options */ + opts &= ~MU_MSG_OPTION_VERIFY; + opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES; + + mu_msg_part_foreach (self, opts, + (MuMsgPartForeachFunc)accumulate_body, + &bdata); + + if (bdata.gstr->len == 0) { + g_string_free (bdata.gstr, TRUE); + return NULL; + } else + return g_string_free (bdata.gstr, FALSE); +} + +typedef struct { + GMimeContentType *ctype; + gboolean want_html; +} ContentTypeData; + +static void +find_content_type (MuMsg *msg, MuMsgPart *mpart, ContentTypeData *cdata) +{ + GMimePart *wanted; + + if (!GMIME_IS_PART(mpart->data)) + return; + + /* text-like attachments are included when in text-mode */ + + if (!cdata->want_html && + (mpart->part_type & MU_MSG_PART_TYPE_TEXT_PLAIN)) + wanted = mpart->data; + else if (!(mpart->part_type & MU_MSG_PART_TYPE_ATTACHMENT) && + cdata->want_html && + (mpart->part_type & MU_MSG_PART_TYPE_TEXT_HTML)) + wanted = mpart->data; + else + wanted = NULL; + + if (wanted) + cdata->ctype = g_mime_object_get_content_type ( + GMIME_OBJECT(wanted)); +} + +static const GSList* +get_content_type_parameters (MuMsg *self, MuMsgOptions opts, gboolean want_html) +{ + ContentTypeData cdata; + + cdata.want_html = want_html; + cdata.ctype = NULL; + + /* wipe out some irrelevant options */ + opts &= ~MU_MSG_OPTION_VERIFY; + opts &= ~MU_MSG_OPTION_EXTRACT_IMAGES; + + mu_msg_part_foreach (self, opts, + (MuMsgPartForeachFunc)find_content_type, + &cdata); + + if (cdata.ctype) { + + GSList *gslist; + GMimeParamList *paramlist; + const GMimeParam *param; + int i, len; + + gslist = NULL; + paramlist = g_mime_content_type_get_parameters (cdata.ctype); + len = g_mime_param_list_length (paramlist); + + for (i = 0; i < len; ++i) { + param = g_mime_param_list_get_parameter_at (paramlist, i); + gslist = g_slist_prepend (gslist, g_strdup (param->name)); + gslist = g_slist_prepend (gslist, g_strdup (param->value)); + } + + return free_later_lst (self, g_slist_reverse (gslist)); + } + return NULL; +} + +const GSList* +mu_msg_get_body_text_content_type_parameters (MuMsg *self, MuMsgOptions opts) +{ + g_return_val_if_fail (self, NULL); + return get_content_type_parameters(self, opts, FALSE); +} + +const char* +mu_msg_get_body_html (MuMsg *self, MuMsgOptions opts) +{ + g_return_val_if_fail (self, NULL); + return free_later_str (self, get_body (self, opts, TRUE)); +} + +const char* +mu_msg_get_body_text (MuMsg *self, MuMsgOptions opts) +{ + g_return_val_if_fail (self, NULL); + return free_later_str (self, get_body (self, opts, FALSE)); +} + +const GSList* +mu_msg_get_references (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_list_field (self, MU_MSG_FIELD_ID_REFS); +} + +const GSList* +mu_msg_get_tags (MuMsg *self) +{ + g_return_val_if_fail (self, NULL); + return get_str_list_field (self, MU_MSG_FIELD_ID_TAGS); +} + +const char* +mu_msg_get_field_string (MuMsg *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, NULL); + return get_str_field (self, mfid); +} + +const GSList* +mu_msg_get_field_string_list (MuMsg *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, NULL); + return get_str_list_field (self, mfid); +} + +gint64 +mu_msg_get_field_numeric (MuMsg *self, MuMsgFieldId mfid) +{ + g_return_val_if_fail (self, -1); + return get_num_field (self, mfid); +} + +static gboolean +fill_contact (MuMsgContact *self, InternetAddress *addr, + MuMsgContactType ctype) +{ + if (!addr) + return FALSE; + + self->full_address = internet_address_to_string ( + addr, NULL, FALSE); + + self->name = internet_address_get_name (addr); + if (mu_str_is_empty (self->name)) { + self->name = NULL; + } + + self->type = ctype; + + /* we only support internet mailbox addresses; if we don't + * check, g_mime hits an assert + */ + if (INTERNET_ADDRESS_IS_MAILBOX(addr)) + self->email= internet_address_mailbox_get_addr + (INTERNET_ADDRESS_MAILBOX(addr)); + else + self->email = NULL; + + /* if there's no address, just a name, it's probably a local + * address (without @) */ + if (self->name && !self->email) + self->email = self->name; + + /* note, the address could be NULL e.g. when the recipient is something + * like 'Undisclosed recipients' + */ + return self->email != NULL; +} + +static void +address_list_foreach (InternetAddressList *addrlist, MuMsgContactType ctype, + MuMsgContactForeachFunc func, gpointer user_data) +{ + int i, len; + + if (!addrlist) + return; + + len = internet_address_list_length(addrlist); + + for (i = 0; i != len; ++i) { + MuMsgContact contact; + gboolean keep_going; + + if (!fill_contact(&contact, + internet_address_list_get_address (addrlist, i), + ctype)) + continue; + + keep_going = func(&contact, user_data); + g_free ((char*)contact.full_address); + + if (!keep_going) + break; + } +} + +static void +addresses_foreach (const char* addrs, MuMsgContactType ctype, + MuMsgContactForeachFunc func, gpointer user_data) +{ + InternetAddressList *addrlist; + + if (!addrs) + return; + + addrlist = internet_address_list_parse (NULL, addrs); + if (addrlist) { + address_list_foreach (addrlist, ctype, func, user_data); + g_object_unref (addrlist); + } +} + +static void +msg_contact_foreach_file (MuMsg *msg, MuMsgContactForeachFunc func, + gpointer user_data) +{ + int i; + struct { + GMimeAddressType _gmime_type; + MuMsgContactType _type; + } ctypes[] = { + {GMIME_ADDRESS_TYPE_FROM, MU_MSG_CONTACT_TYPE_FROM}, + {GMIME_ADDRESS_TYPE_REPLY_TO, MU_MSG_CONTACT_TYPE_REPLY_TO}, + {GMIME_ADDRESS_TYPE_TO, MU_MSG_CONTACT_TYPE_TO}, + {GMIME_ADDRESS_TYPE_CC, MU_MSG_CONTACT_TYPE_CC}, + {GMIME_ADDRESS_TYPE_BCC, MU_MSG_CONTACT_TYPE_BCC}, + }; + + for (i = 0; i != G_N_ELEMENTS(ctypes); ++i) { + InternetAddressList *addrlist; + addrlist = g_mime_message_get_addresses (msg->_file->_mime_msg, + ctypes[i]._gmime_type); + address_list_foreach (addrlist, ctypes[i]._type, func, user_data); + } +} + +static void +msg_contact_foreach_doc (MuMsg *msg, MuMsgContactForeachFunc func, + gpointer user_data) +{ + addresses_foreach (mu_msg_get_from (msg), + MU_MSG_CONTACT_TYPE_FROM, func, user_data); + addresses_foreach (mu_msg_get_to (msg), + MU_MSG_CONTACT_TYPE_TO, func, user_data); + addresses_foreach (mu_msg_get_cc (msg), + MU_MSG_CONTACT_TYPE_CC, func, user_data); + addresses_foreach (mu_msg_get_bcc (msg), + MU_MSG_CONTACT_TYPE_BCC, func, user_data); +} + +void +mu_msg_contact_foreach (MuMsg *msg, MuMsgContactForeachFunc func, + gpointer user_data) +{ + g_return_if_fail (msg); + g_return_if_fail (func); + + if (msg->_file) + msg_contact_foreach_file (msg, func, user_data); + else if (msg->_doc) + msg_contact_foreach_doc (msg, func, user_data); + else + g_return_if_reached (); +} + +static int +cmp_str (const char *s1, const char *s2) +{ + if (s1 == s2) + return 0; + else if (!s1) + return -1; + else if (!s2) + return 1; + + /* optimization 1: ascii */ + if (isascii(s1[0]) && isascii(s2[0])) { + int diff; + diff = tolower(s1[0]) - tolower(s2[0]); + if (diff != 0) + return diff; + } + + /* utf 8 */ + { + char *u1, *u2; + int diff; + + u1 = g_utf8_strdown (s1, -1); + u2 = g_utf8_strdown (s2, -1); + + diff = g_utf8_collate (u1, u2); + + g_free (u1); + g_free (u2); + + return diff; + } +} + +static int +cmp_subject (const char* s1, const char *s2) +{ + if (s1 == s2) + return 0; + else if (!s1) + return -1; + else if (!s2) + return 1; + + return cmp_str ( + mu_str_subject_normalize (s1), + mu_str_subject_normalize (s2)); +} + +int +mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid) +{ + g_return_val_if_fail (m1, 0); + g_return_val_if_fail (m2, 0); + g_return_val_if_fail (mu_msg_field_id_is_valid(mfid), 0); + + /* even though date is a numeric field, we can sort it by its + * string repr. in the database, which is much faster */ + if (mfid == MU_MSG_FIELD_ID_DATE || + mu_msg_field_is_string (mfid)) + return cmp_str (get_str_field (m1, mfid), + get_str_field (m2, mfid)); + + if (mfid == MU_MSG_FIELD_ID_SUBJECT) + return cmp_subject (get_str_field (m1, mfid), + get_str_field (m2, mfid)); + + /* TODO: note, we cast (potentially > MAXINT to int) */ + if (mu_msg_field_is_numeric (mfid)) + return get_num_field(m1, mfid) - get_num_field(m2, mfid); + + return 0; /* TODO: handle lists */ +} + +gboolean +mu_msg_is_readable (MuMsg *self) +{ + g_return_val_if_fail (self, FALSE); + + return access (mu_msg_get_path (self), R_OK) == 0 ? TRUE : FALSE; +} + +/* we need do to determine the + * /home/foo/Maildir/bar + * from the /bar + * that we got + */ +static char* +get_target_mdir (MuMsg *msg, const char *target_maildir, GError **err) +{ + char *rootmaildir, *rv; + const char *maildir; + gboolean not_top_level; + + /* maildir is the maildir stored in the message, e.g. '/foo' */ + maildir = mu_msg_get_maildir(msg); + if (!maildir) { + mu_util_g_set_error (err, MU_ERROR_GMIME, + "message without maildir"); + return NULL; + } + + /* the 'rootmaildir' is the filesystem path from root to + * maildir, ie. /home/user/Maildir/foo */ + rootmaildir = mu_maildir_get_maildir_from_path (mu_msg_get_path(msg)); + if (!rootmaildir) { + mu_util_g_set_error (err, MU_ERROR_GMIME, + "cannot determine maildir"); + return NULL; + } + + /* we do a sanity check: verify that that maildir is a suffix of + * rootmaildir;*/ + not_top_level = TRUE; + if (!g_str_has_suffix (rootmaildir, maildir) && + /* special case for the top-level '/' maildir, and + * remember not_top_level */ + (not_top_level = (g_strcmp0 (maildir, "/") != 0))) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE, + "path is '%s', but maildir is '%s' ('%s')", + rootmaildir, mu_msg_get_maildir(msg), + mu_msg_get_path (msg)); + g_free (rootmaildir); + return NULL; + } + + /* if we're not at the top-level, remove the final '/' from + * the rootmaildir */ + if (not_top_level) + rootmaildir[strlen(rootmaildir) - + strlen (mu_msg_get_maildir(msg))] = '\0'; + + rv = g_strconcat (rootmaildir, target_maildir, NULL); + g_free (rootmaildir); + + return rv; +} + +/* + * move a msg to another maildir, trying to maintain 'integrity', + * ie. msg in 'new/' will go to new/, one in cur/ goes to cur/. be + * super-paranoid here... + */ +gboolean +mu_msg_move_to_maildir (MuMsg *self, const char *maildir, + MuFlags flags, gboolean ignore_dups, + gboolean new_name, GError **err) +{ + char *newfullpath; + char *targetmdir; + + g_return_val_if_fail (self, FALSE); + g_return_val_if_fail (maildir, FALSE); /* i.e. "/inbox" */ + + /* targetmdir is the full path to maildir, i.e., + * /home/foo/Maildir/inbox */ + targetmdir = get_target_mdir (self, maildir, err); + if (!targetmdir) + return FALSE; + + newfullpath = mu_maildir_move_message (mu_msg_get_path (self), + targetmdir, flags, + ignore_dups, new_name, + err); + if (!newfullpath) { + g_free (targetmdir); + return FALSE; + } + + /* clear the old backends */ + mu_msg_doc_destroy (self->_doc); + self->_doc = NULL; + + mu_msg_file_destroy (self->_file); + + /* and create a new one */ + self->_file = mu_msg_file_new (newfullpath, maildir, err); + g_free (targetmdir); + g_free (newfullpath); + + return self->_file ? TRUE : FALSE; +} + +/* + * Rename a message-file, keeping the same flags. This is useful for tricking + * some 3rd party progs such as mbsync + */ +gboolean +mu_msg_tickle (MuMsg *self, GError **err) +{ + g_return_val_if_fail (self, FALSE); + + return mu_msg_move_to_maildir (self, + mu_msg_get_maildir (self), + mu_msg_get_flags (self), + FALSE, TRUE, err); +} + +const char* +mu_str_flags_s (MuFlags flags) +{ + return mu_flags_to_str_s (flags, MU_FLAG_TYPE_ANY); +} + +char* +mu_str_flags (MuFlags flags) +{ + return g_strdup (mu_str_flags_s(flags)); +} + +static void +cleanup_contact (char *contact) +{ + char *c, *c2; + + /* replace "'<> with space */ + for (c2 = contact; *c2; ++c2) + if (*c2 == '"' || *c2 == '\'' || *c2 == '<' || *c2 == '>') + *c2 = ' '; + + /* remove everything between '()' if it's after the 5th pos; + * good to cleanup corporate contact address spam... */ + c = g_strstr_len (contact, -1, "("); + if (c && c - contact > 5) + *c = '\0'; + + g_strstrip (contact); +} + + +/* this is still somewhat simplistic... */ +const char* +mu_str_display_contact_s (const char *str) +{ + static gchar contact[255]; + gchar *c, *c2; + + str = str ? str : ""; + g_strlcpy (contact, str, sizeof(contact)); + + /* we check for '<', so we can strip out the address stuff in + * e.g. 'Hello World <hello@world.xx>, but only if there is + * something alphanumeric before the < + */ + c = g_strstr_len (contact, -1, "<"); + if (c != NULL) { + for (c2 = contact; c2 < c && !(isalnum(*c2)); ++c2); + if (c2 != c) /* apparently, there was something, + * so we can remove the <... part*/ + *c = '\0'; + } + + cleanup_contact (contact); + + return contact; +} + +char* +mu_str_display_contact (const char *str) +{ + g_return_val_if_fail (str, NULL); + + return g_strdup (mu_str_display_contact_s (str)); +} diff --git a/lib/mu-msg.h b/lib/mu-msg.h new file mode 100644 index 0000000..e4842aa --- /dev/null +++ b/lib/mu-msg.h @@ -0,0 +1,654 @@ +/* -*- mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2010-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_MSG_H__ +#define __MU_MSG_H__ + +#include <mu-flags.h> +#include <mu-msg-fields.h> +#include <mu-msg-prio.h> +#include <utils/mu-util.h> + +G_BEGIN_DECLS + +struct _MuMsg; +typedef struct _MuMsg MuMsg; + +/* options for various functions */ +enum _MuMsgOptions { + MU_MSG_OPTION_NONE = 0, +/* 1 << 0 is still free! */ + + /* for -> sexp conversion */ + MU_MSG_OPTION_HEADERS_ONLY = 1 << 1, + MU_MSG_OPTION_EXTRACT_IMAGES = 1 << 2, + + /* below options are for checking signatures; only effective + * if mu was built with crypto support */ + MU_MSG_OPTION_VERIFY = 1 << 4, + MU_MSG_OPTION_AUTO_RETRIEVE = 1 << 5, + MU_MSG_OPTION_USE_AGENT = 1 << 6, + /* MU_MSG_OPTION_USE_PKCS7 = 1 << 7, /\* gpg is the default *\/ */ + + /* get password from console if needed */ + MU_MSG_OPTION_CONSOLE_PASSWORD = 1 << 7, + + MU_MSG_OPTION_DECRYPT = 1 << 8, + + /* misc */ + MU_MSG_OPTION_OVERWRITE = 1 << 9, + MU_MSG_OPTION_USE_EXISTING = 1 << 10, + + /* recurse into submessages */ + MU_MSG_OPTION_RECURSE_RFC822 = 1 << 11 + +}; +typedef enum _MuMsgOptions MuMsgOptions; + + + +/** + * create a new MuMsg* instance which parses a message and provides + * read access to its properties; call mu_msg_unref when done with it. + * + * @param path full path to an email message file + * @param mdir the maildir for this message; ie, if the path is + * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar; you can + * pass NULL for this parameter, in which case some maildir-specific + * information is not available. + * @param err receive error information (MU_ERROR_FILE or + * MU_ERROR_GMIME), or NULL. There will only be err info if the + * function returns NULL + * + * @return a new MuMsg instance or NULL in case of error; call + * mu_msg_unref when done with this message + */ +MuMsg *mu_msg_new_from_file (const char* filepath, const char *maildir, + GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * create a new MuMsg* instance based on a Xapian::Document + * + * @param store a MuStore ptr + * @param doc a ptr to a Xapian::Document (but cast to XapianDocument, + * because this is C not C++). MuMsg takes _ownership_ of this pointer; + * don't touch it afterwards + * @param err receive error information, or NULL. There + * will only be err info if the function returns NULL + * + * @return a new MuMsg instance or NULL in case of error; call + * mu_msg_unref when done with this message + */ +MuMsg *mu_msg_new_from_doc (XapianDocument* doc, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * if we don't have a message file yet (because this message is + * database-backed), load it. + * + * @param msg a MuMsg + * @param err receives error information + * + * @return TRUE if this succeeded, FALSE in case of error + */ +gboolean mu_msg_load_msg_file (MuMsg *msg, GError **err); + + +/** + * close the file-backend, if any; this function is for the use case + * where you have a large amount of messages where you need some + * file-backed field (body or attachments). If you don't close the + * file-backend after retrieving the desired field, you'd quickly run + * out of file descriptors. If this message does not have a + * file-backend, do nothing. + * + * @param msg a message object + */ +void mu_msg_unload_msg_file (MuMsg *msg); + +/** + * increase the reference count for this message + * + * @param msg a message + * + * @return the message with its reference count increased, or NULL in + * case of error. + */ +MuMsg *mu_msg_ref (MuMsg *msg); + +/** + * decrease the reference count for this message. if the reference + * count reaches 0, the message will be destroyed. + * + * @param msg a message + */ +void mu_msg_unref (MuMsg *msg); + +/** + * cache the values from the backend (file or db), so we don't the + * backend anymore + * + * @param self a message + */ +void mu_msg_cache_values (MuMsg *self); + + +/** + * get the plain text body of this message + * + * @param msg a valid MuMsg* instance + * @param opts options for getting the body + * + * @return the plain text body or NULL in case of error or if there is no + * such body. the returned string should *not* be modified or freed. + * The returned data is in UTF8 or NULL. + */ +const char* mu_msg_get_body_text (MuMsg *msg, MuMsgOptions opts); + + +/** + * get the content type parameters for the text body part + * + * @param msg a valid MuMsg* instance + * @param opts options for getting the body + * + * @return the value of the requested body part content type parameter, or + * NULL in case of error or if there is no such body. the returned string + * should *not* be modified or freed. The returned data is in UTF8 or NULL. + */ +const GSList* mu_msg_get_body_text_content_type_parameters (MuMsg *self, MuMsgOptions opts); + + +/** + * get the html body of this message + * + * @param msg a valid MuMsg* instance + * @param opts options for getting the body + * + * @return the html body or NULL in case of error or if there is no + * such body. the returned string should *not* be modified or freed. + */ +const char* mu_msg_get_body_html (MuMsg *msgMu, MuMsgOptions opts); + +/** + * get the sender (From:) of this message + * + * @param msg a valid MuMsg* instance + * + * @return the sender of this Message or NULL in case of error or if there + * is no sender. the returned string should *not* be modified or freed. + */ +const char* mu_msg_get_from (MuMsg *msg); + + +/** + * get the recipients (To:) of this message + * + * @param msg a valid MuMsg* instance + * + * @return the sender of this Message or NULL in case of error or if there + * are no recipients. the returned string should *not* be modified or freed. + */ +const char* mu_msg_get_to (MuMsg *msg); + + +/** + * get the carbon-copy recipients (Cc:) of this message + * + * @param msg a valid MuMsg* instance + * + * @return the Cc: recipients of this Message or NULL in case of error or if + * there are no such recipients. the returned string should *not* be modified + * or freed. + */ +const char* mu_msg_get_cc (MuMsg *msg); + +/** + * get the blind carbon-copy recipients (Bcc:) of this message; this + * field usually only appears in outgoing messages + * + * @param msg a valid MuMsg* instance + * + * @return the Bcc: recipients of this Message or NULL in case of + * error or if there are no such recipients. the returned string + * should *not* be modified or freed. + */ +const char* mu_msg_get_bcc (MuMsg *msg); + +/** + * get the file system path of this message + * + * @param msg a valid MuMsg* instance + * + * @return the path of this Message or NULL in case of error. + * the returned string should *not* be modified or freed. + */ +const char* mu_msg_get_path (MuMsg *msg); + + +/** + * get the maildir this message lives in; ie, if the path is + * ~/Maildir/foo/bar/cur/msg, the maildir would be foo/bar + * + * @param msg a valid MuMsg* instance + * + * @return the maildir requested or NULL in case of error. The returned + * string should *not* be modified or freed. + */ +const char* mu_msg_get_maildir (MuMsg *msg); + + +/** + * get the subject of this message + * + * @param msg a valid MuMsg* instance + * + * @return the subject of this Message or NULL in case of error or if there + * is no subject. the returned string should *not* be modified or freed. + */ +const char* mu_msg_get_subject (MuMsg *msg); + +/** + * get the Message-Id of this message + * + * @param msg a valid MuMsg* instance + * + * @return the Message-Id of this message (without the enclosing <>), + * or a fake message-id for messages that don't have them, or NULL in + * case of error. + */ +const char* mu_msg_get_msgid (MuMsg *msg); + + +/** + * get the mailing list for a message, i.e. the mailing-list + * identifier in the List-Id header. + * + * @param msg a valid MuMsg* instance + * + * @return the mailing list id for this message (without the enclosing <>) + * or NULL in case of error or if there is none. the returned string + * should *not* be modified or freed. + */ +const char* mu_msg_get_mailing_list (MuMsg *msg); + + +/** + * get the message date/time (the Date: field) as time_t, using UTC + * + * @param msg a valid MuMsg* instance + * + * @return message date/time or 0 in case of error or if there + * is no such header. + */ +time_t mu_msg_get_date (MuMsg *msg); + +/** + * get the flags for this message + * + * @param msg valid MuMsg* instance + * + * @return the file/content flags as logically OR'd #MuMsgFlags or 0 + * if there are none. Non-standard flags are ignored. + */ +MuFlags mu_msg_get_flags (MuMsg *msg); + + +/** + * get the file size in bytes of this message + * + * @param msg a valid MuMsg* instance + * + * @return the filesize + */ +size_t mu_msg_get_size (MuMsg *msg); + + +/** + * get some field value as string + * + * @param msg a valid MuMsg instance + * @param field the field to retrieve; it must be a string-typed field + * + * @return a string that should not be freed + */ +const char* mu_msg_get_field_string (MuMsg *msg, MuMsgFieldId mfid); + + +/** + * get some field value as string-list + * + * @param msg a valid MuMsg instance + * @param field the field to retrieve; it must be a string-list-typed field + * + * @return a list that should not be freed + */ +const GSList* mu_msg_get_field_string_list (MuMsg *self, MuMsgFieldId mfid); + +/** + * get some field value as string + * + * @param msg a valid MuMsg instance + * @param field the field to retrieve; it must be a numeric field + * + * @return a string that should not be freed + */ +gint64 mu_msg_get_field_numeric (MuMsg *msg, MuMsgFieldId mfid); + +/** + * get the message priority for this message (MU_MSG_PRIO_LOW, + * MU_MSG_PRIO_NORMAL or MU_MSG_PRIO_HIGH) the X-Priority, + * X-MSMailPriority, Importance and Precedence header are checked, in + * that order. if no known or explicit priority is set, + * MU_MSG_PRIO_NORMAL is assumed + * + * @param msg a valid MuMsg* instance + * + * @return the message priority (!= 0) or 0 in case of error + */ +MuMsgPrio mu_msg_get_prio (MuMsg *msg); + +/** + * get the timestamp (mtime) for the file containing this message + * + * @param msg a valid MuMsg* instance + * + * @return the timestamp or 0 in case of error + */ +time_t mu_msg_get_timestamp (MuMsg *msg); + + +/** + * get a specific header from the message. This value will _not_ be + * cached + * + * @param self a MuMsg instance + * @param header a specific header (like 'X-Mailer' or 'Organization') + * + * @return a header string which is valid as long as this MuMsg is + */ +const char* mu_msg_get_header (MuMsg *self, const char *header); + + +/** + * get the list of references (consisting of both the References and + * In-Reply-To fields), with the oldest first and the direct parent as + * the last one. Note, any reference (message-id) will appear at most + * once, duplicates are filtered out. + * + * @param msg a valid MuMsg + * + * @return a list with the references for this msg. Don't modify/free + */ +const GSList* mu_msg_get_references (MuMsg *msg); + +/** + * get the list of tags (ie., X-Label) + * + * @param msg a valid MuMsg + * + * @return a list with the tags for this msg. Don't modify/free + */ +const GSList* mu_msg_get_tags (MuMsg *self); + + +/** + * compare two messages for sorting + * + * @param m1 a message + * @param m2 another message + * @param mfid the message to use for the comparison + * + * @return negative if m1 is smaller, positive if m1 is smaller, 0 if + * they are equal + */ +int mu_msg_cmp (MuMsg *m1, MuMsg *m2, MuMsgFieldId mfid); + + +/** + * check whether there there's a readable file behind this message + * + * @param self a MuMsg* + * + * @return TRUE if the message file is readable, FALSE otherwise + */ +gboolean mu_msg_is_readable (MuMsg *self); + + +struct _MuMsgIterThreadInfo; + + +/** + * convert the msg to a Lisp symbolic expression (for further processing in + * e.g. emacs) + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * and for message parts: + * MU_MSG_OPTION_CHECK_SIGNATURES: check signatures + * MU_MSG_OPTION_AUTO_RETRIEVE_KEY: attempt to retrieve keys online + * MU_MSG_OPTION_USE_AGENT: attempt to use GPG-agent + * MU_MSG_OPTION_USE_PKCS7: attempt to use PKCS (instead of gpg) + * + * @return a string with the sexp (free with g_free) or NULL in case of error + */ +char* mu_msg_to_sexp (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +#ifdef HAVE_JSON_GLIB + +struct _JsonNode; /* forward declaration */ + +/** + * convert the msg to json + * + * @param msg a valid message + * @param docid the docid for this message, or 0 + * @param ti thread info for the current message, or NULL + * @param opts, bitwise OR'ed; + * - MU_MSG_OPTION_HEADERS_ONLY: only include message fields which can be + * obtained from the database (this is much faster if the MuMsg is + * database-backed, so no file needs to be opened) + * - MU_MSG_OPTION_EXTRACT_IMAGES: extract image attachments as temporary + * files and include links to those in the sexp + * + * @return a string with the sexp (free with g_free) or NULL in case of error + */ +struct _JsonNode* mu_msg_to_json (MuMsg *msg, unsigned docid, + const struct _MuMsgIterThreadInfo *ti, + MuMsgOptions ops) G_GNUC_WARN_UNUSED_RESULT; +#endif /*HAVE_JSON_GLIB*/ + +/** + * move a message to another maildir; note that this does _not_ update + * the database + * + * @param msg a message with an existing file system path in an actual + * maildir + * @param maildir the subdir where the message should go, relative to + * rootmaildir. e.g. "/archive" + * @param flags to set for the target (influences the filename, path) + * @param silently ignore the src=target case (return TRUE) + * @param new_name whether to create a new unique name, or keep the + * old one + * @param err (may be NULL) may contain error information; note if the + * function return FALSE, err is not set for all error condition + * (ie. not for parameter error + * + * @return TRUE if it worked, FALSE otherwise + */ +gboolean mu_msg_move_to_maildir (MuMsg *msg, const char *maildir, + MuFlags flags, gboolean ignore_dups, + gboolean new_name, + GError **err); + +/** + * Tickle a message -- ie., rename a message to some new semi-random name,while + * maintaining the maildir and flags. This can be useful when dealing with + * third-party tools such as mbsync that depend on changed filenames. + * + * @param msg a message with an existing file system path in an actual + * maildir + * @param err (may be NULL) may contain error information; note if the + * function return FALSE, err is not set for all error condition + * (ie. not for parameter error + * + * @return TRUE if it worked, FALSE otherwise + */ +gboolean mu_msg_tickle (MuMsg *msg, GError **err); + + +enum _MuMsgContactType { /* Reply-To:? */ + MU_MSG_CONTACT_TYPE_TO = 0, + MU_MSG_CONTACT_TYPE_FROM, + MU_MSG_CONTACT_TYPE_CC, + MU_MSG_CONTACT_TYPE_BCC, + MU_MSG_CONTACT_TYPE_REPLY_TO, + + MU_MSG_CONTACT_TYPE_NUM +}; +typedef guint MuMsgContactType; + +/* not a 'real' contact type */ +#define MU_MSG_CONTACT_TYPE_ALL (MU_MSG_CONTACT_TYPE_NUM + 1) + +#define mu_msg_contact_type_is_valid(MCT)\ + ((MCT) < MU_MSG_CONTACT_TYPE_NUM) + +struct _MuMsgContact { + const char *name; /**< Foo Bar */ + const char *email; /**< foo@bar.cuux */ + const char *full_address; /**< Foo Bar <foo@bar.cuux> */ + MuMsgContactType type; /**< MU_MSG_CONTACT_TYPE_{ TO, + CC, BCC, FROM, REPLY_TO} */ +}; +typedef struct _MuMsgContact MuMsgContact; + + +/** + * macro to get the name of a contact + * + * @param ct a MuMsgContact + * + * @return the name + */ +#define mu_msg_contact_name(ct) ((ct)->name) + +/** + * macro to get the email address of a contact + * + * @param ct a MuMsgContact + * + * @return the address + */ +#define mu_msg_contact_email(ct) ((ct)->email) + +/** + * macro to get the contact type + * + * @param ct a MuMsgContact + * + * @return the contact type + */ +#define mu_msg_contact_type(ct) ((ct)->type) + + +/** + * callback function + * + * @param contact + * @param user_data a user provided data pointer + * + * @return TRUE if we should continue the foreach, FALSE otherwise + */ +typedef gboolean (*MuMsgContactForeachFunc) (MuMsgContact* contact, + gpointer user_data); + +/** + * call a function for each of the contacts in a message; the order is: + * from to cc bcc (of each there are zero or more) + * + * @param msg a valid MuMsgGMime* instance + * @param func a callback function to call for each contact; when + * the callback does not return TRUE, it won't be called again + * @param user_data a user-provide pointer that will be passed to the callback + * + */ +void mu_msg_contact_foreach (MuMsg *msg, MuMsgContactForeachFunc func, + gpointer user_data); + + + +/** + * create a 'display contact' from an email header To/Cc/Bcc/From-type address + * ie., turn + * "Foo Bar" <foo@bar.com> + * into + * Foo Bar + * Note that this is based on some simple heuristics. Max length is 255 bytes. + * + * mu_str_display_contact_s returns a statically allocated + * buffer (ie, non-reentrant), while mu_str_display_contact + * returns a newly allocated string that you must free with g_free + * when done with it. + * + * @param str a 'contact str' (ie., what is in the To/Cc/Bcc/From + * fields), or NULL + * + * @return a newly allocated string with a display contact + */ +const char* mu_str_display_contact_s (const char *str) G_GNUC_CONST; +char *mu_str_display_contact (const char *str) G_GNUC_WARN_UNUSED_RESULT; + +/** + * get a display string for a given set of flags, OR'ed in + * @param flags; one character per flag: + * D=draft,F=flagged,N=new,P=passed,R=replied,S=seen,T=trashed + * a=has-attachment,s=signed, x=encrypted + * + * mu_str_file_flags_s returns a ptr to a static buffer, + * while mu_str_file_flags returns dynamically allocated + * memory that must be freed after use. + * + * @param flags file flags + * + * @return a string representation of the flags; see above + * for what to do with it + */ +const char* mu_str_flags_s (MuFlags flags) G_GNUC_CONST; +char* mu_str_flags (MuFlags flags) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +G_END_DECLS + +#endif /*__MU_MSG_H__*/ diff --git a/lib/mu-query.cc b/lib/mu-query.cc new file mode 100644 index 0000000..cafb3eb --- /dev/null +++ b/lib/mu-query.cc @@ -0,0 +1,524 @@ +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include <stdexcept> +#include <string> +#include <cctype> +#include <cstring> +#include <sstream> + +#include <stdlib.h> +#include <xapian.h> +#include <glib/gstdio.h> + +#include "mu-query.h" +#include "mu-msg-fields.h" + +#include "mu-msg-iter.h" + +#include "utils/mu-str.h" +#include "utils/mu-date.h" +#include <utils/mu-utils.hh> + +#include <query/mu-proc-iface.hh> +#include <query/mu-xapian.hh> + +using namespace Mu; + +struct MuProc: public Mu::ProcIface { + + MuProc (const Xapian::Database& db): db_{db} {} + + static MuMsgFieldId field_id (const std::string& field) { + + if (field.empty()) + return MU_MSG_FIELD_ID_NONE; + + MuMsgFieldId id = mu_msg_field_id_from_name (field.c_str(), FALSE); + if (id != MU_MSG_FIELD_ID_NONE) + return id; + else if (field.length() == 1) + return mu_msg_field_id_from_shortcut (field[0], FALSE); + else + return MU_MSG_FIELD_ID_NONE; + } + + std::string + process_value (const std::string& field, + const std::string& value) const override { + const auto id = field_id (field); + if (id == MU_MSG_FIELD_ID_NONE) + return value; + switch(id) { + case MU_MSG_FIELD_ID_PRIO: { + if (!value.empty()) + return std::string(1, value[0]); + } break; + + case MU_MSG_FIELD_ID_FLAGS: { + const auto flag = mu_flag_char_from_name (value.c_str()); + if (flag) + return std::string(1, tolower(flag)); + } break; + + default: + break; + } + + return value; // XXX prio/flags, etc. alias + } + + void add_field (std::vector<FieldInfo>& fields, MuMsgFieldId id) const { + + const auto shortcut = mu_msg_field_shortcut(id); + if (!shortcut) + return; // can't be searched + + const auto name = mu_msg_field_name (id); + const auto pfx = mu_msg_field_xapian_prefix (id); + + if (!name || !pfx) + return; + + fields.push_back ({{name}, {pfx}, + (bool)mu_msg_field_xapian_index(id), + id}); + } + + std::vector<FieldInfo> + process_field (const std::string& field) const override { + + std::vector<FieldInfo> fields; + + if (field == "contact" || field == "recip") { // multi fields + add_field (fields, MU_MSG_FIELD_ID_TO); + add_field (fields, MU_MSG_FIELD_ID_CC); + add_field (fields, MU_MSG_FIELD_ID_BCC); + if (field == "contact") + add_field (fields, MU_MSG_FIELD_ID_FROM); + } else if (field == "") { + add_field (fields, MU_MSG_FIELD_ID_TO); + add_field (fields, MU_MSG_FIELD_ID_CC); + add_field (fields, MU_MSG_FIELD_ID_BCC); + add_field (fields, MU_MSG_FIELD_ID_FROM); + add_field (fields, MU_MSG_FIELD_ID_SUBJECT); + add_field (fields, MU_MSG_FIELD_ID_BODY_TEXT); + } else { + const auto id = field_id (field.c_str()); + if (id != MU_MSG_FIELD_ID_NONE) + add_field (fields, id); + } + + return fields; + } + + bool is_range_field (const std::string& field) const override { + const auto id = field_id (field.c_str()); + if (id == MU_MSG_FIELD_ID_NONE) + return false; + else + return mu_msg_field_is_range_field (id); + } + + Range process_range (const std::string& field, const std::string& lower, + const std::string& upper) const override { + + const auto id = field_id (field.c_str()); + if (id == MU_MSG_FIELD_ID_NONE) + return { lower, upper }; + + std::string l2 = lower; + std::string u2 = upper; + + if (id == MU_MSG_FIELD_ID_DATE) { + l2 = Mu::date_to_time_t_string (lower, true); + u2 = Mu::date_to_time_t_string (upper, false); + } else if (id == MU_MSG_FIELD_ID_SIZE) { + l2 = Mu::size_to_string (lower, true); + u2 = Mu::size_to_string (upper, false); + } + + return { l2, u2 }; + } + + std::vector<std::string> + process_regex (const std::string& field, const std::regex& rx) const override { + + const auto id = field_id (field.c_str()); + if (id == MU_MSG_FIELD_ID_NONE) + return {}; + + char pfx[] = { mu_msg_field_xapian_prefix(id), '\0' }; + + std::vector<std::string> terms; + for (auto it = db_.allterms_begin(pfx); it != db_.allterms_end(pfx); ++it) { + if (std::regex_search((*it).c_str() + 1, rx)) // avoid copy + terms.push_back(*it); + } + + return terms; + } + + const Xapian::Database& db_; +}; + +struct _MuQuery { +public: + _MuQuery (MuStore *store): _store(mu_store_ref(store)) {} + ~_MuQuery () { mu_store_unref (_store); } + + Xapian::Database& db() const { + const auto db = reinterpret_cast<Xapian::Database*> + (mu_store_get_read_only_database (_store)); + if (!db) + throw Mu::Error(Error::Code::NotFound, "no database"); + return *db; + } +private: + MuStore *_store; +}; + +static const Xapian::Query +get_query (MuQuery *mqx, const char* searchexpr, bool raw, GError **err) try { + + Mu::WarningVec warns; + const auto tree = Mu::parse (searchexpr, warns, + std::make_unique<MuProc>(mqx->db())); + for (const auto w: warns) + std::cerr << w << std::endl; + + return Mu::xapian_query (tree); + +} catch (...) { + mu_util_g_set_error (err,MU_ERROR_XAPIAN_QUERY, + "parse error in query"); + throw; +} + +MuQuery* +mu_query_new (MuStore *store, GError **err) +{ + g_return_val_if_fail (store, NULL); + + try { + return new MuQuery (store); + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0); + + return 0; +} + +void +mu_query_destroy (MuQuery *self) +{ + try { delete self; } MU_XAPIAN_CATCH_BLOCK; +} + + +/* this function is for handling the case where a DatabaseModified + * exception is raised. We try to reopen the database, and run the + * query again. */ +static MuMsgIter * +try_requery (MuQuery *self, const char* searchexpr, MuMsgFieldId sortfieldid, + int maxnum, MuQueryFlags flags, GError **err) +{ + try { + /* let's assume that infinite regression is + * impossible */ + self->db().reopen(); + MU_WRITE_LOG ("reopening db after modification"); + return mu_query_run (self, searchexpr, sortfieldid, + maxnum, flags, err); + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0); +} + + +static MuMsgIterFlags +msg_iter_flags (MuQueryFlags flags) +{ + MuMsgIterFlags iflags; + + iflags = MU_MSG_ITER_FLAG_NONE; + + if (flags & MU_QUERY_FLAG_DESCENDING) + iflags |= MU_MSG_ITER_FLAG_DESCENDING; + if (flags & MU_QUERY_FLAG_SKIP_UNREADABLE) + iflags |= MU_MSG_ITER_FLAG_SKIP_UNREADABLE; + if (flags & MU_QUERY_FLAG_SKIP_DUPS) + iflags |= MU_MSG_ITER_FLAG_SKIP_DUPS; + if (flags & MU_QUERY_FLAG_THREADS) + iflags |= MU_MSG_ITER_FLAG_THREADS; + + return iflags; +} + + + +static Xapian::Enquire +get_enquire (MuQuery *self, const char *searchexpr, MuMsgFieldId sortfieldid, + bool descending, bool raw, GError **err) +{ + Xapian::Enquire enq (self->db()); + + try { + if (raw) + enq.set_query(Xapian::Query(Xapian::Query(searchexpr))); + else if (!mu_str_is_empty(searchexpr) && + g_strcmp0 (searchexpr, "\"\"") != 0) /* NULL or "" or """" */ + enq.set_query(get_query (self, searchexpr, raw, err)); + else/* empty or "" means "matchall" */ + enq.set_query(Xapian::Query::MatchAll); + } catch (...) { + mu_util_g_set_error (err, MU_ERROR_XAPIAN_QUERY, + "parse error in query"); + throw; + } + + enq.set_cutoff(0,0); + return enq; +} + +/* + * record all threadids for the messages; also 'orig_set' receives all + * original matches (a map msgid-->docid), so we can make sure the + * originals are not seen as 'duplicates' later (when skipping + * duplicates). We want to favor the originals over the related + * messages, when skipping duplicates. + */ +static GHashTable* +get_thread_ids (MuMsgIter *iter, GHashTable **orig_set) +{ + GHashTable *ids; + + ids = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify)g_free, NULL); + *orig_set = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify)g_free, NULL); + + while (!mu_msg_iter_is_done (iter)) { + char *thread_id, *msgid; + unsigned docid; + /* record the thread id for the message */ + if ((thread_id = mu_msg_iter_get_thread_id (iter))) + g_hash_table_insert (ids, thread_id, + GSIZE_TO_POINTER(TRUE)); + /* record the original set */ + docid = mu_msg_iter_get_docid(iter); + if (docid != 0 && (msgid = mu_msg_iter_get_msgid (iter))) + g_hash_table_insert (*orig_set, msgid, + GSIZE_TO_POINTER(docid)); + + if (!mu_msg_iter_next (iter)) + break; + } + + return ids; +} + + +static Xapian::Query +get_related_query (MuMsgIter *iter, GHashTable **orig_set) +{ + GHashTable *hash; + GList *id_list, *cur; + std::vector<Xapian::Query> qvec; + static std::string pfx (1, mu_msg_field_xapian_prefix + (MU_MSG_FIELD_ID_THREAD_ID)); + + /* orig_set receives the hash msgid->docid of the set of + * original matches */ + hash = get_thread_ids (iter, orig_set); + /* id_list now gets a list of all thread-ids seen in the query + * results; either in the Message-Id field or in + * References. */ + id_list = g_hash_table_get_keys (hash); + + // now, we create a vector with queries for each of the + // thread-ids, which we combine below. This is /much/ faster + // than creating the query as 'query = Query (OR, query)'... + for (cur = id_list; cur; cur = g_list_next(cur)) + qvec.push_back (Xapian::Query((std::string + (pfx + (char*)cur->data)))); + + g_hash_table_destroy (hash); + g_list_free (id_list); + + return Xapian::Query (Xapian::Query::OP_OR, qvec.begin(), qvec.end()); +} + + +static void +get_related_messages (MuQuery *self, MuMsgIter **iter, int maxnum, + MuMsgFieldId sortfieldid, MuQueryFlags flags, + Xapian::Query orig_query) +{ + GHashTable *orig_set; + Xapian::Enquire enq (self->db()); + MuMsgIter *rel_iter; + const bool inc_related = flags & MU_QUERY_FLAG_INCLUDE_RELATED; + + orig_set = NULL; + Xapian::Query new_query = get_related_query (*iter, &orig_set); + /* If related message are not desired, filter out messages which would not + have matched the original query. + */ + if (!inc_related) + new_query = Xapian::Query (Xapian::Query::OP_AND, orig_query, new_query); + enq.set_query(new_query); + enq.set_cutoff(0,0); + + rel_iter= mu_msg_iter_new ( + reinterpret_cast<XapianEnquire*>(&enq), + maxnum, + sortfieldid, + msg_iter_flags (flags), + NULL); + + mu_msg_iter_destroy (*iter); + + // set the preferred set for the iterator (ie., the set of + // messages not considered to be duplicates) to be the + // original matches -- the matches without considering + // 'related' + mu_msg_iter_set_preferred (rel_iter, orig_set); + g_hash_table_destroy (orig_set); + + *iter = rel_iter; +} + + +MuMsgIter* +mu_query_run (MuQuery *self, const char *searchexpr, MuMsgFieldId sortfieldid, + int maxnum, MuQueryFlags flags, GError **err) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (searchexpr, NULL); + g_return_val_if_fail (mu_msg_field_id_is_valid (sortfieldid) || + sortfieldid == MU_MSG_FIELD_ID_NONE, + NULL); + try { + MuMsgIter *iter; + MuQueryFlags first_flags; + const bool threads = flags & MU_QUERY_FLAG_THREADS; + const bool inc_related = flags & MU_QUERY_FLAG_INCLUDE_RELATED; + const bool descending = flags & MU_QUERY_FLAG_DESCENDING; + const bool raw = flags & MU_QUERY_FLAG_RAW; + Xapian::Enquire enq (get_enquire(self, searchexpr, sortfieldid, + descending, raw, err)); + + /* when we're doing a 'include-related query', wea're actually + * doing /two/ queries; one to get the initial matches, and + * based on that one to get all messages in threads in those + * matches. + */ + + /* get the 'real' maxnum if it was specified as < 0 */ + maxnum = maxnum < 0 ? self->db().get_doccount() : maxnum; + /* Calculating threads involves two queries, so do the calculation only in + * the second query instead of in both. + */ + if (threads) + first_flags = (MuQueryFlags)(flags & ~MU_QUERY_FLAG_THREADS); + else + first_flags = flags; + /* Perform the initial query, returning up to max num results. + */ + iter = mu_msg_iter_new ( + reinterpret_cast<XapianEnquire*>(&enq), + maxnum, + sortfieldid, + msg_iter_flags (first_flags), + err); + /* If we want threads or related messages, find related messages using a + * second query based on the message ids / refs of the first query's result. + * Do this even if we don't want to include related messages in the final + * result so we can apply the threading algorithm to the related message set + * of a maxnum-sized result instead of the unbounded result of the first + * query. If threads are desired but related message are not, we will remove + * the undesired related messages later. + */ + if(threads||inc_related) + get_related_messages (self, &iter, maxnum, sortfieldid, flags, + enq.get_query()); + + if (err && *err && (*err)->code == MU_ERROR_XAPIAN_MODIFIED) { + g_clear_error (err); + return try_requery (self, searchexpr, sortfieldid, + maxnum, flags, err); + } else + return iter; + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN (err, MU_ERROR_XAPIAN, 0); +} + + +size_t +mu_query_count_run (MuQuery *self, const char *searchexpr) try +{ + g_return_val_if_fail (self, 0); + g_return_val_if_fail (searchexpr, 0); + + const auto enq{get_enquire(self, searchexpr,MU_MSG_FIELD_ID_NONE, false, false, NULL)}; + auto mset(enq.get_mset(0, self->db().get_doccount())); + mset.fetch(); + + return mset.size(); + +} MU_XAPIAN_CATCH_BLOCK_RETURN (0); + + + + +char* +mu_query_internal_xapian (MuQuery *self, const char *searchexpr, GError **err) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (searchexpr, NULL); + + try { + Xapian::Query query (get_query(self, searchexpr, false, err)); + return g_strdup(query.get_description().c_str()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL); +} + + +char* +mu_query_internal (MuQuery *self, const char *searchexpr, + gboolean warn, GError **err) +{ + g_return_val_if_fail (self, NULL); + g_return_val_if_fail (searchexpr, NULL); + + try { + Mu::WarningVec warns; + const auto tree = Mu::parse (searchexpr, warns, + std::make_unique<MuProc>(self->db())); + std::stringstream ss; + ss << tree; + + if (warn) { + for (const auto w: warns) + std::cerr << w << std::endl; + } + + return g_strdup(ss.str().c_str()); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(NULL); +} diff --git a/lib/mu-query.h b/lib/mu-query.h new file mode 100644 index 0000000..26cdb0f --- /dev/null +++ b/lib/mu-query.h @@ -0,0 +1,136 @@ +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_QUERY_H__ +#define __MU_QUERY_H__ + +#include <glib.h> +#include <mu-store.hh> +#include <mu-msg-iter.h> +#include <utils/mu-util.h> + +G_BEGIN_DECLS + +struct _MuQuery; +typedef struct _MuQuery MuQuery; + +/** + * create a new MuQuery instance. + * + * @param store a MuStore object + * @param err receives error information (if there is any); if + * function returns non-NULL, err will _not_be set. err can be NULL + * possible errors (err->code) are MU_ERROR_XAPIAN_DIR and + * MU_ERROR_XAPIAN_NOT_UPTODATE + * + * @return a new MuQuery instance, or NULL in case of error. + * when the instance is no longer needed, use mu_query_destroy + * to free it + */ +MuQuery* mu_query_new (MuStore *store, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * destroy the MuQuery instance + * + * @param self a MuQuery instance, or NULL + */ +void mu_query_destroy (MuQuery *self); + + +typedef enum { + MU_QUERY_FLAG_NONE = 0 << 0, /**< no flags */ + MU_QUERY_FLAG_DESCENDING = 1 << 0, /**< sort z->a */ + MU_QUERY_FLAG_SKIP_UNREADABLE = 1 << 1, /**< skip unreadable msgs */ + MU_QUERY_FLAG_SKIP_DUPS = 1 << 2, /**< skip duplicate msgs */ + MU_QUERY_FLAG_INCLUDE_RELATED = 1 << 3, /**< include related msgs */ + MU_QUERY_FLAG_THREADS = 1 << 4, /**< calculate threading info */ + MU_QUERY_FLAG_RAW = 1 << 5 /**< don't parse the query */ +} MuQueryFlags; + +/** + * run a Xapian query; for the syntax, please refer to the mu-query + * manpage + * + * @param self a valid MuQuery instance + * @param expr the search expression; use "" to match all messages + * @param sortfield the field id to sort by or MU_MSG_FIELD_ID_NONE if + * sorting is not desired + * @param maxnum maximum number of search results to return, or <= 0 for + * unlimited + * @param flags bitwise OR'd flags to influence the query (see MuQueryFlags) + * @param err receives error information (if there is any); if + * function returns non-NULL, err will _not_be set. err can be NULL + * possible error (err->code) is MU_ERROR_QUERY, + * + * @return a MuMsgIter instance you can iterate over, or NULL in + * case of error + */ +MuMsgIter* mu_query_run (MuQuery *self, const char* expr, + MuMsgFieldId sortfieldid, int maxnum, + MuQueryFlags flags, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +/** + * run a Xapian query to count the number of matches; for the syntax, please + * refer to the mu-query manpage + * + * @param self a valid MuQuery instance + * @param expr the search expression; use "" to match all messages + * + * @return the number of matches + */ +size_t mu_query_count_run (MuQuery *self, const char *searchexpr); + +/** + * get Xapian's internal string representation of the query + * + * @param self a MuQuery instance + * @param searchexpr a xapian search expression + * @param warn print warnings to stderr + * @param err receives error information (if there is any); if + * function returns non-NULL, err will _not_be set. err can be NULL + * + * @return the string representation of the xapian query, or NULL in case of + * error; free the returned value with g_free + */ +char* mu_query_internal (MuQuery *self, const char *searchexpr, + gboolean warn, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * get Xapian's internal string representation of the query + * + * @param self a MuQuery instance + * @param searchexpr a xapian search expression + * @param err receives error information (if there is any); if + * function returns non-NULL, err will _not_be set. err can be NULL + * + * @return the string representation of the xapian query, or NULL in case of + * error; free the returned value with g_free + */ +char* mu_query_internal_xapian (MuQuery *self, const char* searchexpr, + GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + + +G_END_DECLS + +#endif /*__MU_QUERY_H__*/ diff --git a/lib/mu-runtime.cc b/lib/mu-runtime.cc new file mode 100644 index 0000000..b8fb606 --- /dev/null +++ b/lib/mu-runtime.cc @@ -0,0 +1,118 @@ +/* +** Copyright (C) 2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-runtime.h" +#include "utils/mu-util.h" + +#include <locale.h> /* for setlocale() */ + +#include <string> +#include <unordered_map> +static std::unordered_map<MuRuntimePath, std::string> RuntimePaths; + +constexpr auto PartsDir = "parts"; +constexpr auto LogDir = "log"; +constexpr auto XapianDir = "xapian"; +constexpr auto Mu = "mu"; +constexpr auto Bookmarks = "bookmarks"; + +static const std::string Sepa{G_DIR_SEPARATOR_S}; + +static void +init_paths_xdg () +{ + RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, g_get_user_cache_dir() + + Sepa + Mu + Sepa + XapianDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, g_get_user_cache_dir() + + Sepa + Mu); + RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, g_get_user_cache_dir() + + Sepa + Mu + Sepa + PartsDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, g_get_user_cache_dir() + + Sepa + Mu); + RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, g_get_user_config_dir() + + Sepa + Mu); +} + +static void +init_paths_muhome (const char *muhome) +{ + RuntimePaths.emplace(MU_RUNTIME_PATH_XAPIANDB, muhome + Sepa + XapianDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_CACHE, muhome); + RuntimePaths.emplace(MU_RUNTIME_PATH_MIMECACHE, muhome + Sepa + PartsDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_LOGDIR, muhome + Sepa + LogDir); + RuntimePaths.emplace(MU_RUNTIME_PATH_BOOKMARKS, muhome + Sepa + Bookmarks); +} + +gboolean +mu_runtime_init (const char* muhome, const char *name) +{ + g_return_val_if_fail (RuntimePaths.empty(), FALSE); + g_return_val_if_fail (name, FALSE); + + setlocale (LC_ALL, ""); + + if (muhome) + init_paths_muhome (muhome); + else + init_paths_xdg(); + + for (const auto& d: RuntimePaths ) { + char* dir; + if (d.first == MU_RUNTIME_PATH_BOOKMARKS) // special case + dir = g_path_get_dirname (d.second.c_str()); + else + dir = g_strdup (d.second.c_str()); + + auto ok = mu_util_create_dir_maybe (dir, 0700, TRUE); + if (!ok) { + g_critical ("failed to create %s", dir); + g_free (dir); + mu_runtime_uninit(); + return FALSE; + } + g_free (dir); + } + + const auto log_path = RuntimePaths[MU_RUNTIME_PATH_LOGDIR] + + Sepa + name + ".log"; + + if (!mu_log_init (log_path.c_str(), MU_LOG_OPTIONS_BACKUP)) { + mu_runtime_uninit(); + return FALSE; + } + + return TRUE; +} + +void +mu_runtime_uninit (void) +{ + RuntimePaths.clear(); + mu_log_uninit(); +} + +const char* +mu_runtime_path (MuRuntimePath path) +{ + const auto it = RuntimePaths.find (path); + if (it == RuntimePaths.end()) + return NULL; + else + return it->second.c_str(); +} diff --git a/lib/mu-runtime.h b/lib/mu-runtime.h new file mode 100644 index 0000000..f300815 --- /dev/null +++ b/lib/mu-runtime.h @@ -0,0 +1,67 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- +** +** Copyright (C) 2012-2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#ifndef __MU_RUNTIME_H__ +#define __MU_RUNTIME_H__ + +#include <glib.h> +#include <utils/mu-log.h> + +G_BEGIN_DECLS + +/** + * initialize the mu runtime system; initializes logging and other + * systems. To uninitialize, use mu_runtime_uninit + * + * @param muhome path where to find the mu home directory (typically, ~/.cache/mu) + * @param name of the main program, ie. 'mu', 'mug' or + * 'procmule'. this influences the name of the e.g. the logfile + * + * @return TRUE if succeeded, FALSE in case of error + */ +gboolean mu_runtime_init (const char *muhome, const char *name); + +/** + * free all resources + * + */ +void mu_runtime_uninit (void); + + +typedef enum { + MU_RUNTIME_PATH_XAPIANDB, /* mu xapian db path */ + MU_RUNTIME_PATH_BOOKMARKS, /* mu bookmarks file path */ + MU_RUNTIME_PATH_CACHE, /* mu cache path for attachments etc. */ + MU_RUNTIME_PATH_MIMECACHE, /* mu cache path for attachments etc. */ + MU_RUNTIME_PATH_LOGDIR, /* mu path for log files */ + + MU_RUNTIME_PATH_NUM +} MuRuntimePath; + +/** + * get a file system path to some 'special' file or directory + * + * @return ma string which should be not be modified/freed, or NULL in + * case of error. + */ +const char* mu_runtime_path (MuRuntimePath path); + +G_END_DECLS + +#endif /*__MU_RUNTIME_H__*/ diff --git a/lib/mu-script.c b/lib/mu-script.c new file mode 100644 index 0000000..c5b167f --- /dev/null +++ b/lib/mu-script.c @@ -0,0 +1,360 @@ +/* +** Copyright (C) 2012-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#ifdef BUILD_GUILE + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wredundant-decls" +#include <libguile.h> +#pragma GCC diagnostic pop +#endif /*BUILD_GUILE*/ + +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <dirent.h> +#include <errno.h> +#include <unistd.h> + +#include "utils/mu-str.h" +#include "mu-script.h" +#include "utils/mu-util.h" + +/** + * Structure with information about a certain script. + * the values will be *freed* when MuScriptInfo is freed + */ +struct _MuScriptInfo { + char *_name; /* filename-sans-extension */ + char *_path; /* full path to script */ + char *_oneline; /* one-line description */ + char *_descr; /* longer description */ +}; + + +/* create a new MuScriptInfo* object*/ +static MuScriptInfo* +script_info_new (void) +{ + return g_slice_new0 (MuScriptInfo); +} + +/* destroy a MuScriptInfo* object */ +static void +script_info_destroy (MuScriptInfo *msi) +{ + if (!msi) + return; + + g_free (msi->_name); + g_free (msi->_path); + g_free (msi->_oneline); + g_free (msi->_descr); + + g_slice_free (MuScriptInfo, msi); +} + +/* compare two MuScripInfo* objects (for sorting) */ +static int +script_info_cmp (MuScriptInfo *msi1, MuScriptInfo *msi2) +{ + return strcmp (msi1->_name, msi2->_name); + +} + +const char* +mu_script_info_name (MuScriptInfo *msi) +{ + g_return_val_if_fail (msi, NULL); + return msi->_name; +} + +const char* +mu_script_info_path (MuScriptInfo *msi) +{ + g_return_val_if_fail (msi, NULL); + return msi->_path; +} + +const char* +mu_script_info_one_line (MuScriptInfo *msi) +{ + g_return_val_if_fail (msi, NULL); + return msi->_oneline; +} + +const char* +mu_script_info_description (MuScriptInfo *msi) +{ + g_return_val_if_fail (msi, NULL); + return msi->_descr; +} + + +gboolean +mu_script_info_matches_regex (MuScriptInfo *msi, const char *rxstr, + GError **err) +{ + GRegex *rx; + gboolean match; + + g_return_val_if_fail (msi, FALSE); + g_return_val_if_fail (rxstr, FALSE); + + rx = g_regex_new (rxstr, G_REGEX_CASELESS|G_REGEX_OPTIMIZE, 0, err); + if (!rx) + return FALSE; + + match = FALSE; + if (msi->_name) + match = g_regex_match (rx, msi->_name, 0, NULL); + if (!match && msi->_oneline) + match = g_regex_match (rx, msi->_oneline, 0, NULL); + + return match; +} + +void +mu_script_info_list_destroy (GSList *lst) +{ + g_slist_foreach (lst, (GFunc)script_info_destroy, NULL); + g_slist_free (lst); +} + + +static GIOChannel * +open_channel (const char *path) +{ + GError *err; + GIOChannel *io_chan; + + err = NULL; + + io_chan = g_io_channel_new_file (path, "r", &err); + if (!io_chan) { + g_warning ("failed to open '%s': %s", path, + err ? err->message : "something went wrong"); + g_clear_error (&err); + return NULL; + } + + return io_chan; +} + +static void +end_channel (GIOChannel *io_chan) +{ + GIOStatus status; + GError *err; + + err = NULL; + status = g_io_channel_shutdown (io_chan, FALSE, + &err); + if (status != G_IO_STATUS_NORMAL) { + g_warning ("failed to shutdown io-channel: %s", + err ? err->message : "something went wrong"); + g_clear_error (&err); + } + + g_io_channel_unref (io_chan); +} + + + +static gboolean +get_descriptions (MuScriptInfo *msi, const char *prefix) +{ + GIOStatus io_status; + GIOChannel *script_io; + GError *err; + + char *line, *descr, *oneline; + + if (!prefix) + return TRUE; /* not an error */ + + if (!(script_io = open_channel (msi->_path))) + return FALSE; + + err = NULL; + line = descr = oneline = NULL; + + do { + g_free (line); + io_status = g_io_channel_read_line (script_io, &line, + NULL, NULL, &err); + if (io_status != G_IO_STATUS_NORMAL) + break; + + if (!g_str_has_prefix (line, prefix)) + continue; + + if (!oneline) + oneline = g_strdup (line + strlen (prefix)); + else { + char *tmp; + tmp = descr; + descr = g_strdup_printf + ("%s%s", descr ? descr : "", + line + strlen(prefix)); + g_free (tmp); + } + + } while (TRUE); + + if (io_status != G_IO_STATUS_EOF) { + g_warning ("error reading %s: %s", msi->_path, + err ? err->message : "something went wrong"); + g_clear_error (&err); + } + + end_channel (script_io); + msi->_oneline = oneline; + msi->_descr = descr; + + return TRUE; +} + + + +GSList* +mu_script_get_script_info_list (const char *path, const char *ext, + const char *descprefix, GError **err) +{ + DIR *dir; + GSList *lst; + struct dirent *dentry; + + g_return_val_if_fail (path, NULL); + + dir = opendir (path); + if (!dir) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_OPEN, + "failed to open '%s': %s", + path, strerror(errno)); + return NULL; + } + + /* create a list of names, paths */ + lst = NULL; + while ((dentry = readdir (dir))) { + MuScriptInfo *msi; + /* only consider files with certain extensions, + * if ext != NULL */ + if (ext && !g_str_has_suffix (dentry->d_name, ext)) + continue; + msi = script_info_new (); + msi->_name = g_strdup (dentry->d_name); + if (ext) /* strip the extension */ + msi->_name[strlen(msi->_name) - strlen(ext)] = '\0'; + msi->_path = g_strdup_printf ("%s%c%s", path, G_DIR_SEPARATOR, + dentry->d_name); + /* set the one-line and long description */ + get_descriptions (msi, descprefix); + lst = g_slist_prepend (lst, msi); + } + + closedir (dir); /* ignore error checking... */ + + return g_slist_sort (lst, (GCompareFunc)script_info_cmp); +} + + +MuScriptInfo* +mu_script_find_script_with_name (GSList *lst, const char *name) +{ + GSList *cur; + + g_return_val_if_fail (name, NULL); + + for (cur = lst; cur; cur = g_slist_next (cur)) { + + MuScriptInfo *msi; + msi = (MuScriptInfo*)cur->data; + + if (g_strcmp0 (name, mu_script_info_name (msi)) == 0) + return msi; + } + + return NULL; +} + +#ifdef BUILD_GUILE +static void +guile_shell (void *closure, int argc, char **argv) +{ + scm_shell (argc, argv); +} + +gboolean +mu_script_guile_run (MuScriptInfo *msi, const char *muhome, + const char **args, GError **err) +{ + const char *s; + char *mainargs, *expr; + char **argv; + + g_return_val_if_fail (msi, FALSE); + g_return_val_if_fail (muhome, FALSE); + + argv = g_new0 (char*, 6); + argv[0] = g_strdup("guile2.2"); + argv[1] = g_strdup("-l"); + + if (access (mu_script_info_path (msi), R_OK) != 0) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_READ, + "failed to read script: %s", + strerror(errno)); + return FALSE; + } + + s = mu_script_info_path (msi); + argv[2] = g_strdup (s ? s : ""); + + mainargs = mu_str_quoted_from_strv (args); + expr = g_strdup_printf ( + "(main '(\"%s\" \"--muhome=%s\" %s))", + mu_script_info_name (msi), + muhome, + mainargs ? mainargs : ""); + + g_free (mainargs); + argv[3] = g_strdup("-c"); + argv[4] = expr; + + scm_boot_guile (5, argv, guile_shell, NULL); + + /* never reached but let's be correct(TM)*/ + g_strfreev (argv); + return TRUE; +} +#else /*!BUILD_GUILE*/ +gboolean +mu_script_guile_run (MuScriptInfo *msi, const char *muhome, + const char **args, GError **err) +{ + mu_util_g_set_error (err, MU_ERROR_INTERNAL, + "this mu does not have guile support"); + return FALSE; +} +#endif /*!BUILD_GUILE*/ diff --git a/lib/mu-script.h b/lib/mu-script.h new file mode 100644 index 0000000..586b6f0 --- /dev/null +++ b/lib/mu-script.h @@ -0,0 +1,132 @@ +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_SCRIPT_H__ +#define __MU_SCRIPT_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +/* Opaque structure with information about a script */ +struct _MuScriptInfo; +typedef struct _MuScriptInfo MuScriptInfo; + +/** + * get the name of the script (sans-extension, if some extension was + * provided to mu_script_get_scripts) + * + * @param msi a MuScriptInfo structure + * + * @return the name + */ +const char* mu_script_info_name (MuScriptInfo *msi); + + +/** + * get the full filesystem path of the script + * + * @param msi a MuScriptInfo structure + * + * @return the path + */ +const char* mu_script_info_path (MuScriptInfo *msi); + +/** + * get a one-line description for the script + * + * @param msi a MuScriptInfo structure + * + * @return the description, or NULL if there was none + */ +const char* mu_script_info_one_line (MuScriptInfo *msi); + +/** + * get a full description for the script + * + * @param msi a MuScriptInfo structure + * + * @return the description, or NULL if there was none + */ +const char* mu_script_info_description (MuScriptInfo *msi); + +/** + * check whether either the name or one-line description of a + * MuScriptInfo matches regular expression rxstr + * + * @param msi a MuScriptInfo + * @param rxstr a regular expression string + * @param err receives error information + * + * @return TRUE if it matches, FALSE if not or in case of error + */ +gboolean mu_script_info_matches_regex (MuScriptInfo *msi, const char *rxstr, + GError **err); + +/** + * Get the list of all scripts in path with extension ext + * + * @param path a file system path + * @param ext an extension (e.g., ".scm"), or NULL + * @param prefix for the one-line description + * (e.g., ";; DESCRIPTION: "), or NULL + * @param err receives error information, if any + * + * @return a list of Mu + */ +GSList *mu_script_get_script_info_list (const char *path, const char *ext, + const char *descprefix, GError **err); + +/** + * destroy a list of MuScriptInfo* objects + * + * @param scriptslst a list of MuScriptInfo* objects + */ +void mu_script_info_list_destroy (GSList *lst); + + +/** + * find the MuScriptInfo object for the first script with a certain + * name, or return NULL if not found. + * + * @param lst a list of MuScriptInfo* objects + * @param name the name to search for + * + * @return a MuScriptInfo* object, or NULL if not found. + */ +MuScriptInfo* mu_script_find_script_with_name (GSList *lst, const char *name); + + +/** + * run the guile script at path + * + * @param msi MuScriptInfo object for the script + * @param muhome path to the mu home dir + * @param args NULL-terminated array of strings (argv for the script) + * @param err receives error information + * + * @return FALSE in case of error -- otherwise, this function will + * _not return_ + */ +gboolean mu_script_guile_run (MuScriptInfo *msi, const char *muhome, + const char **args, GError **err); + +G_END_DECLS + +#endif /*__MU_SCRIPT_H__*/ diff --git a/lib/mu-store.cc b/lib/mu-store.cc new file mode 100644 index 0000000..d9141e9 --- /dev/null +++ b/lib/mu-store.cc @@ -0,0 +1,1433 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include <mutex> +#include <array> +#include <cstdlib> +#include <xapian.h> +#include <unordered_map> +#include <atomic> +#include <iostream> +#include <cstring> +#include "mu-store.hh" +#include "utils/mu-str.h" +#include "utils/mu-error.hh" + +#include "mu-msg-part.h" +#include "utils/mu-utils.hh" + +using namespace Mu; + +constexpr auto SchemaVersionKey = "schema-version"; +constexpr auto RootMaildirKey = "maildir"; // XXX: make this 'root-maildir' +constexpr auto ContactsKey = "contacts"; +constexpr auto PersonalAddressesKey = "personal-addresses"; +constexpr auto CreatedKey = "created"; +constexpr auto BatchSize = 150'000; + +constexpr auto ExpectedSchemaVersion = MU_STORE_SCHEMA_VERSION; + + + +extern "C" { +static unsigned add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err); +} + +/* we cache these prefix strings, so we don't have to allocate them all + * the time; this should save 10-20 string allocs per message */ +G_GNUC_CONST static const std::string& +prefix (MuMsgFieldId mfid) +{ + static std::string fields[MU_MSG_FIELD_ID_NUM]; + static bool initialized = false; + + if (G_UNLIKELY(!initialized)) { + for (int i = 0; i != MU_MSG_FIELD_ID_NUM; ++i) + fields[i] = std::string (1, mu_msg_field_xapian_prefix + ((MuMsgFieldId)i)); + initialized = true; + } + + return fields[mfid]; +} + +static void +add_synonym_for_flag (MuFlags flag, Xapian::WritableDatabase *db) +{ + static const std::string pfx(prefix(MU_MSG_FIELD_ID_FLAGS)); + + db->clear_synonyms (pfx + mu_flag_name (flag)); + db->add_synonym (pfx + mu_flag_name (flag), pfx + + (std::string(1, (char)(tolower(mu_flag_char(flag)))))); +} + + +static void +add_synonym_for_prio (MuMsgPrio prio, Xapian::WritableDatabase *db) +{ + static const std::string pfx (prefix(MU_MSG_FIELD_ID_PRIO)); + + std::string s1 (pfx + mu_msg_prio_name (prio)); + std::string s2 (pfx + (std::string(1, mu_msg_prio_char (prio)))); + + db->clear_synonyms (s1); + db->clear_synonyms (s2); + + db->add_synonym (s1, s2); +} + +struct Store::Private { + +#define LOCKED std::lock_guard<std::mutex> l(lock_); + + Private (const std::string& path, bool readonly): + db_path_{path}, + db_{readonly? + std::make_shared<Xapian::Database>(db_path_) : + std::make_shared<Xapian::WritableDatabase>(db_path_, Xapian::DB_OPEN)}, + root_maildir_{db()->get_metadata(RootMaildirKey)}, + created_{atoll(db()->get_metadata(CreatedKey).c_str())}, + schema_version_{db()->get_metadata(SchemaVersionKey)}, + personal_addresses_{Mu::split(db()->get_metadata(PersonalAddressesKey),",")}, + contacts_{db()->get_metadata(ContactsKey)} { + } + + Private (const std::string& path, const std::string& root_maildir, + const StringVec& personal_addresses): + db_path_{path}, + db_{std::make_shared<Xapian::WritableDatabase>( + db_path_, Xapian::DB_CREATE_OR_OVERWRITE)}, + root_maildir_{root_maildir}, + created_{time({})}, + schema_version_{MU_STORE_SCHEMA_VERSION}, + personal_addresses_{personal_addresses} { + + writable_db()->set_metadata(SchemaVersionKey, schema_version_); + writable_db()->set_metadata(RootMaildirKey, root_maildir_); + writable_db()->set_metadata(CreatedKey, Mu::format("%" PRId64, (int64_t)created_)); + + std::string addrs; + for (const auto& addr : personal_addresses_) { // _very_ minimal check. + if (addr.find(",") != std::string::npos) + throw Mu::Error(Error::Code::InvalidArgument, + "e-mail address '%s' contains comma", addr.c_str()); + addrs += (addrs.empty() ? "": ",") + addr; + } + writable_db()->set_metadata (PersonalAddressesKey, addrs); + + } + + ~Private() try { + LOCKED; + if (wdb()) { + wdb()->set_metadata (ContactsKey, contacts_.serialize()); + if (in_transaction_) // auto-commit. + wdb()->commit_transaction(); + } + } MU_XAPIAN_CATCH_BLOCK; + + std::shared_ptr<Xapian::Database> db() const { + if (!db_) + throw Mu::Error(Error::Code::NotFound, "no database found"); + return db_; + } + + std::shared_ptr<Xapian::WritableDatabase> wdb() const { + return std::dynamic_pointer_cast<Xapian::WritableDatabase>(db_); + } + + std::shared_ptr<Xapian::WritableDatabase> writable_db() const { + auto w_db{wdb()}; + if (!w_db) + throw Mu::Error(Error::Code::AccessDenied, "database is read-only"); + else + return w_db; + } + + void begin_transaction () try { + wdb()->begin_transaction(); + in_transaction_ = true; + dirtiness_ = 0; + } MU_XAPIAN_CATCH_BLOCK; + + void commit_transaction () try { + in_transaction_ = false; + dirtiness_ = 0; + wdb()->commit_transaction(); + } MU_XAPIAN_CATCH_BLOCK; + + void add_synonyms () { + mu_flags_foreach ((MuFlagsForeachFunc)add_synonym_for_flag, + writable_db().get()); + mu_msg_prio_foreach ((MuMsgPrioForeachFunc)add_synonym_for_prio, + writable_db().get()); + } + + time_t metadata_time_t (const std::string& key) const { + const auto ts = db()->get_metadata(key); + return (time_t)atoll(db()->get_metadata(key).c_str()); + } + + + + const std::string db_path_; + std::shared_ptr<Xapian::Database> db_; + const std::string root_maildir_; + const time_t created_{}; + const std::string schema_version_; + const StringVec personal_addresses_; + Contacts contacts_; + + std::atomic<bool> in_transaction_{}; + std::mutex lock_; + + size_t dirtiness_{}; + + mutable std::atomic<std::size_t> ref_count_{1}; +}; + + +static void +hash_str (char *buf, size_t buf_size, const char *data) +{ + g_snprintf(buf, buf_size, "016%" PRIx64, mu_util_get_hash(data)); +} + + +static std::string +get_uid_term (const char* path) +{ + char uid_term[1 + 16 + 1] = {'\0'}; + uid_term[0] = mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_UID); + hash_str(uid_term + 1, sizeof(uid_term)-1, path); + + return std::string{uid_term, sizeof(uid_term)}; +} + + +#undef LOCKED +#define LOCKED std::lock_guard<std::mutex> l(priv_->lock_); + +Store::Store (const std::string& path, bool readonly): + priv_{std::make_unique<Private>(path, readonly)} +{ + if (ExpectedSchemaVersion != schema_version()) + throw Mu::Error(Error::Code::SchemaMismatch, + "expected schema-version %s, but got %s", + ExpectedSchemaVersion, schema_version().c_str()); +} + +Store::Store (const std::string& path, const std::string& maildir, + const StringVec& personal_addresses): + priv_{std::make_unique<Private>(path, maildir, personal_addresses)} +{} + +Store::~Store() = default; + +bool +Store::read_only() const +{ + return !priv_->wdb(); +} + +const std::string& +Store::root_maildir () const +{ + return priv_->root_maildir_; +} + +const StringVec& +Store::personal_addresses(void) const +{ + return priv_->personal_addresses_; +} + +const std::string& +Store::database_path() const +{ + return priv_->db_path_; +} + +const Contacts& +Store::contacts() const +{ + LOCKED; + return priv_->contacts_; +} + +std::size_t +Store::size() const +{ + return priv_->db()->get_doccount(); +} + +bool +Store::empty() const +{ + return size() == 0; +} + + +const std::string& +Store::schema_version() const +{ + return priv_->schema_version_; +} + +time_t +Store::created() const +{ + return priv_->created_; +} + +static std::string +maildir_from_path (const std::string& root, const std::string& path) +{ + if (G_UNLIKELY(root.empty()) || root.length() >= path.length() || + path.find(root) != 0) + throw Mu::Error{Error::Code::InvalidArgument, + "root '%s' is not a proper suffix of path '%s'", + root.c_str(), path.c_str()}; + + auto mdir{path.substr(root.length())}; + auto slash{mdir.rfind('/')}; + + if (G_UNLIKELY(slash == std::string::npos) || slash < 4) + throw Mu::Error{Error::Code::InvalidArgument, + "invalid path: %s", path.c_str()}; + mdir.erase(slash); + auto subdir=mdir.data()+slash-4; + if (G_UNLIKELY(strncmp(subdir, "/cur", 4) != 0 && + strncmp(subdir, "/new", 4))) + throw Mu::Error{Error::Code::InvalidArgument, + "cannot find '/new' or '/cur' - invalid path: %s", path.c_str()}; + if (mdir.length() == 4) + return "/"; + + mdir.erase(mdir.length()-4); + return mdir; +} + + +unsigned +Store::add_message (const std::string& path) +{ + LOCKED; + + GError *gerr{}; + const auto maildir{maildir_from_path(root_maildir(), path)}; + auto msg{mu_msg_new_from_file (path.c_str(), maildir.c_str(), &gerr)}; + if (G_UNLIKELY(!msg)) + throw Error{Error::Code::Message, "failed to create message: %s", + gerr ? gerr->message : "something went wrong"}; + + auto store{reinterpret_cast<MuStore*>(this)}; // yuk. + const auto docid{add_or_update_msg (store, 0, msg, &gerr)}; + mu_msg_unref (msg); + if (G_UNLIKELY(docid == MU_STORE_INVALID_DOCID)) + throw Error{Error::Code::Message, "failed to store message: %s", + gerr ? gerr->message : "something went wrong"}; + + return docid; +} + +bool +Store::remove_message (const std::string& path) +{ + LOCKED; + + try { + const std::string term{(get_uid_term(path.c_str()))}; + auto wdb{priv()->wdb()}; + + wdb->delete_document (term); + + } MU_XAPIAN_CATCH_BLOCK_RETURN (false); + + return true; +} + + + +time_t +Store::dirstamp (const std::string& path) const +{ + LOCKED; + + const auto ts = priv_->db()->get_metadata(path); + if (ts.empty()) + return 0; + else + return (time_t)strtoll(ts.c_str(), NULL, 16); +} + +void +Store::set_dirstamp (const std::string& path, time_t tstamp) +{ + LOCKED; + + std::array<char, 2*sizeof(tstamp)+1> data{}; + const std::size_t len = g_snprintf (data.data(), data.size(), "%zx", tstamp); + + priv_->writable_db()->set_metadata(path, std::string{data.data(), len}); +} + + +MuMsg* +Store::find_message (unsigned docid) const +{ + LOCKED; + + try { + Xapian::Document *doc{new Xapian::Document{priv_->db()->get_document (docid)}}; + GError *gerr{}; + auto msg{mu_msg_new_from_doc (reinterpret_cast<XapianDocument*>(doc), &gerr)}; + if (!msg) { + g_warning ("could not create message: %s", gerr ? gerr->message : + "something went wrong"); + g_clear_error(&gerr); + } + + return msg; + + } MU_XAPIAN_CATCH_BLOCK_RETURN (nullptr); +} + + +bool +Store::contains_message (const std::string& path) const +{ + LOCKED; + + try { + const std::string term (get_uid_term(path.c_str())); + return priv_->db()->term_exists (term); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(false); +} + +void +Store::begin_transaction () try +{ + LOCKED; + priv_->begin_transaction(); + +} MU_XAPIAN_CATCH_BLOCK; + +void +Store::commit_transaction () try +{ + LOCKED; + priv_->commit_transaction(); + +} MU_XAPIAN_CATCH_BLOCK; + +bool +Store::in_transaction () const +{ + return priv_->in_transaction_; +} + + +//////////////////////////////////////////////////////////////////////////////// +// C compat +extern "C" { + + +struct MuStore_ { Mu::Store* self; }; + + +static const Mu::Store* +self (const MuStore *store) +{ + if (!store) { + g_error ("invalid store"); // terminates + return {}; + } + + return reinterpret_cast<const Mu::Store*>(store); +} + +static Mu::Store* +mutable_self (MuStore *store) +{ + if (!store) { + g_error ("invalid store"); // terminates + return {}; + } + + auto s = reinterpret_cast<Mu::Store*>(store); + if (s->read_only()) { + g_error ("store is read-only"); // terminates + return {}; + } + + return s; +} + + +MuStore* +mu_store_new_readable (const char* xpath, GError **err) +{ + g_return_val_if_fail (xpath, NULL); + + g_debug ("opening database at %s (read-only)", xpath); + + try { + return reinterpret_cast<MuStore*>(new Store (xpath)); + + } catch (const Mu::Error& me) { + g_warning ("failed to open database: %s", me.what()); + } catch (const Xapian::Error& dbe) { + g_warning ("failed to open database @ %s: %s", xpath, + dbe.get_error_string() ? dbe.get_error_string() : "something went wrong"); + } + + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_OPEN, + "failed to open database @ %s", xpath); + + return NULL; +} + +MuStore* +mu_store_new_writable (const char* xpath, GError **err) +{ + g_return_val_if_fail (xpath, NULL); + + g_debug ("opening database at %s (writable)", xpath); + + try { + return reinterpret_cast<MuStore*>(new Store (xpath, false/*!readonly*/)); + + } catch (const Mu::Error& me) { + if (me.code() == Mu::Error::Code::SchemaMismatch) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_SCHEMA_MISMATCH, + "%s", me.what()); + return NULL; + } + } catch (const Xapian::DatabaseLockError& dle) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, + "database @ %s is write-locked", xpath); + return NULL; + } catch (const Xapian::Error& dbe) { + g_warning ("failed to open database @ %s: %s", xpath, + dbe.get_error_string() ? dbe.get_error_string() : "something went wrong"); + } + + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_OPEN, + "failed to open database @ %s", xpath); + + return NULL; +} + + +MuStore* +mu_store_new_create (const char* xpath, const char *root_maildir, + const char **personal_addresses, GError **err) +{ + g_return_val_if_fail (xpath, NULL); + g_return_val_if_fail (root_maildir, NULL); + + g_debug ("create database at %s (root-maildir=%s)", xpath, root_maildir); + + try { + StringVec addrs; + for (auto i = 0; personal_addresses && personal_addresses[i]; ++i) + addrs.emplace_back(personal_addresses[i]); + + return reinterpret_cast<MuStore*>( + new Store (xpath, std::string{root_maildir}, addrs)); + + } catch (const Xapian::DatabaseLockError& dle) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, + "database @ %s is write-locked already", xpath); + } catch (...) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN, + "error opening database @ %s", xpath); + } + + return NULL; +} + + +MuStore* +mu_store_ref (MuStore* store) +{ + g_return_val_if_fail (store, NULL); + g_return_val_if_fail (self(store)->priv()->ref_count_ > 0, NULL); + + ++self(store)->priv()->ref_count_; + return store; +} + + +MuStore* +mu_store_unref (MuStore* store) +{ + g_return_val_if_fail (store, NULL); + g_return_val_if_fail (self(store)->priv()->ref_count_ > 0, NULL); + + auto me = reinterpret_cast<Mu::Store*>(store); + + if (--me->priv()->ref_count_ == 0) + delete me; + + return NULL; +} + +gboolean +mu_store_is_read_only (const MuStore *store) +{ + g_return_val_if_fail (store, FALSE); + + try { + return self(store)->read_only() ? TRUE : FALSE; + + } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); + +} + + +const MuContacts* +mu_store_contacts (MuStore *store) +{ + g_return_val_if_fail (store, FALSE); + + try { + return self(store)->contacts().mu_contacts(); + + } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); +} + +unsigned +mu_store_count (const MuStore *store, GError **err) +{ + g_return_val_if_fail (store, (unsigned)-1); + + try { + return self(store)->size(); + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, + (unsigned)-1); +} + +const char* +mu_store_schema_version (const MuStore *store) +{ + g_return_val_if_fail (store, NULL); + + return self(store)->schema_version().c_str(); +} + +XapianDatabase* +mu_store_get_read_only_database (MuStore *store) +{ + g_return_val_if_fail (store, NULL); + return (XapianWritableDatabase*)self(store)->priv()->db().get(); +} + + + + +gboolean +mu_store_contains_message (const MuStore *store, const char* path) +{ + g_return_val_if_fail (store, FALSE); + g_return_val_if_fail (path, FALSE); + + try { + return self(store)->contains_message(path) ? TRUE : FALSE; + + } MU_XAPIAN_CATCH_BLOCK_RETURN(FALSE); +} + +unsigned +mu_store_get_docid_for_path (const MuStore *store, const char* path, GError **err) +{ + g_return_val_if_fail (store, FALSE); + g_return_val_if_fail (path, FALSE); + + try { + const std::string term (get_uid_term(path)); + Xapian::Query query (term); + Xapian::Enquire enq (*self(store)->priv()->db().get()); + + enq.set_query (query); + + Xapian::MSet mset (enq.get_mset (0,1)); + if (mset.empty()) + throw Mu::Error(Error::Code::NotFound, + "message @ %s not found in store", path); + + return *mset.begin(); + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, + MU_STORE_INVALID_DOCID); +} + + +MuError +mu_store_foreach (MuStore *store, + MuStoreForeachFunc func, void *user_data, GError **err) +{ + g_return_val_if_fail (store, MU_ERROR); + g_return_val_if_fail (func, MU_ERROR); + + try { + Xapian::Enquire enq (*self(store)->priv()->db().get()); + + enq.set_query (Xapian::Query::MatchAll); + enq.set_cutoff (0,0); + + Xapian::MSet matches(enq.get_mset (0, self(store)->size())); + if (matches.empty()) + return MU_OK; /* database is empty */ + + for (Xapian::MSet::iterator iter = matches.begin(); + iter != matches.end(); ++iter) { + Xapian::Document doc (iter.get_document()); + const std::string path(doc.get_value(MU_MSG_FIELD_ID_PATH)); + MuError res = func (path.c_str(), user_data); + if (res != MU_OK) + return res; + } + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(err, MU_ERROR_XAPIAN, + MU_ERROR_XAPIAN); + + return MU_OK; +} + + +MuMsg* +mu_store_get_msg (const MuStore *store, unsigned docid, GError **err) +{ + g_return_val_if_fail (store, NULL); + g_return_val_if_fail (docid != 0, NULL); + + return self(store)->find_message(docid); +} + + +const char* +mu_store_database_path (const MuStore *store) +{ + g_return_val_if_fail (store, NULL); + + return self(store)->database_path().c_str(); +} + + +const char* +mu_store_root_maildir (const MuStore *store) +{ + g_return_val_if_fail (store, NULL); + + return self(store)->root_maildir().c_str(); +} + + +time_t +mu_store_created (const MuStore *store) +{ + g_return_val_if_fail (store, (time_t)0); + + return self(store)->created(); +} + +char** +mu_store_personal_addresses (const MuStore *store) +{ + g_return_val_if_fail (store, NULL); + + const auto size = self(store)->personal_addresses().size(); + auto addrs = g_new0 (char*, 1 + size); + for (size_t i = 0; i != size; ++i) + addrs[i] = g_strdup(self(store)->personal_addresses()[i].c_str()); + + return addrs; +} + +void +mu_store_flush (MuStore *store) try { + + g_return_if_fail (store); + + if (self(store)->priv()->in_transaction_) + mutable_self(store)->commit_transaction (); + + mutable_self(store)->priv()->wdb()->set_metadata( + ContactsKey, self(store)->priv()->contacts_.serialize()); + +} MU_XAPIAN_CATCH_BLOCK; + +static void +add_terms_values_date (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid) +{ + const auto dstr = Mu::date_to_time_t_string ( + (time_t)mu_msg_get_field_numeric (msg, mfid)); + + doc.add_value ((Xapian::valueno)mfid, dstr); +} + +static void +add_terms_values_size (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid) +{ + const auto szstr = + Mu::size_to_string (mu_msg_get_field_numeric (msg, mfid)); + doc.add_value ((Xapian::valueno)mfid, szstr); +} + +G_GNUC_CONST +static const std::string& +flag_val (char flagchar) +{ + static const std::string + pfx (prefix(MU_MSG_FIELD_ID_FLAGS)), + draftstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_DRAFT))), + flaggedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_FLAGGED))), + passedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_PASSED))), + repliedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_REPLIED))), + seenstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_SEEN))), + trashedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_TRASHED))), + newstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_NEW))), + signedstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_SIGNED))), + cryptstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_ENCRYPTED))), + attachstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_HAS_ATTACH))), + unreadstr (pfx + (char)tolower(mu_flag_char(MU_FLAG_UNREAD))), + liststr (pfx + (char)tolower(mu_flag_char(MU_FLAG_LIST))); + + switch (flagchar) { + + case 'D': return draftstr; + case 'F': return flaggedstr; + case 'P': return passedstr; + case 'R': return repliedstr; + case 'S': return seenstr; + case 'T': return trashedstr; + + case 'N': return newstr; + + case 'z': return signedstr; + case 'x': return cryptstr; + case 'a': return attachstr; + case 'l': return liststr; + + case 'u': return unreadstr; + + default: + g_return_val_if_reached (flaggedstr); + return flaggedstr; + } +} + +/* pre-calculate; optimization */ +G_GNUC_CONST static const std::string& +prio_val (MuMsgPrio prio) +{ + static const std::string pfx (prefix(MU_MSG_FIELD_ID_PRIO)); + + static const std::string + low (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_LOW))), + norm (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_NORMAL))), + high (pfx + std::string(1, mu_msg_prio_char(MU_MSG_PRIO_HIGH))); + + switch (prio) { + case MU_MSG_PRIO_LOW: return low; + case MU_MSG_PRIO_NORMAL: return norm; + case MU_MSG_PRIO_HIGH: return high; + default: + g_return_val_if_reached (norm); + return norm; + } +} + + +static void // add term, truncate if needed. +add_term (Xapian::Document& doc, const std::string& term) +{ + if (term.length() < MU_STORE_MAX_TERM_LENGTH) + doc.add_term(term); + else + doc.add_term(term.substr(0, MU_STORE_MAX_TERM_LENGTH)); +} + + + +static void +add_terms_values_number (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid) +{ + gint64 num = mu_msg_get_field_numeric (msg, mfid); + + const std::string numstr (Xapian::sortable_serialise((double)num)); + doc.add_value ((Xapian::valueno)mfid, numstr); + + if (mfid == MU_MSG_FIELD_ID_FLAGS) { + const char *cur = mu_flags_to_str_s + ((MuFlags)num,(MuFlagType)MU_FLAG_TYPE_ANY); + g_return_if_fail (cur); + while (*cur) { + add_term (doc, flag_val(*cur)); + ++cur; + } + + } else if (mfid == MU_MSG_FIELD_ID_PRIO) + add_term (doc, prio_val((MuMsgPrio)num)); +} + + +/* for string and string-list */ +static void +add_terms_values_str (Xapian::Document& doc, const char *val, MuMsgFieldId mfid) +{ + const auto flat = Mu::utf8_flatten (val); + + if (mu_msg_field_xapian_index (mfid)) { + Xapian::TermGenerator termgen; + termgen.set_document (doc); + termgen.index_text (flat, 1, prefix(mfid)); + } + + if (mu_msg_field_xapian_term(mfid)) + add_term(doc, prefix(mfid) + flat); + +} + +static void +add_terms_values_string (Xapian::Document& doc, MuMsg *msg, MuMsgFieldId mfid) +{ + const char *orig; + + if (!(orig = mu_msg_get_field_string (msg, mfid))) + return; /* nothing to do */ + + /* the value is what we display in search results; the + * unchanged original */ + if (mu_msg_field_xapian_value(mfid)) + doc.add_value ((Xapian::valueno)mfid, orig); + + add_terms_values_str (doc, orig, mfid); +} + +static void +add_terms_values_string_list (Xapian::Document& doc, MuMsg *msg, + MuMsgFieldId mfid) +{ + const GSList *lst; + + lst = mu_msg_get_field_string_list (msg, mfid); + if (!lst) + return; + + if (mu_msg_field_xapian_value (mfid)) { + gchar *str; + str = mu_str_from_list (lst, ','); + if (str) + doc.add_value ((Xapian::valueno)mfid, str); + g_free (str); + } + + if (mu_msg_field_xapian_term (mfid)) { + for (; lst; lst = g_slist_next ((GSList*)lst)) + add_terms_values_str (doc, (const gchar*)lst->data, + mfid); + } +} + + +struct PartData { + PartData (Xapian::Document& doc, MuMsgFieldId mfid): + _doc (doc), _mfid(mfid) {} + Xapian::Document _doc; + MuMsgFieldId _mfid; +}; + +/* index non-body text parts */ +static void +maybe_index_text_part (MuMsg *msg, MuMsgPart *part, PartData *pdata) +{ + char *txt; + Xapian::TermGenerator termgen; + + /* only deal with attachments/messages; inlines are indexed as + * body parts */ + if (!(part->part_type & MU_MSG_PART_TYPE_ATTACHMENT) && + !(part->part_type & MU_MSG_PART_TYPE_MESSAGE)) + return; + + txt = mu_msg_part_get_text (msg, part, MU_MSG_OPTION_NONE); + if (!txt) + return; + + termgen.set_document(pdata->_doc); + const auto str = Mu::utf8_flatten (txt); + g_free (txt); + + termgen.index_text (str, 1, prefix(MU_MSG_FIELD_ID_EMBEDDED_TEXT)); +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartData *pdata) +{ + char *fname; + static const std::string + file (prefix(MU_MSG_FIELD_ID_FILE)), + mime (prefix(MU_MSG_FIELD_ID_MIME)); + + /* save the mime type of any part */ + if (part->type) { + char ctype[MU_STORE_MAX_TERM_LENGTH + 1]; + g_snprintf(ctype, sizeof(ctype), "%s/%s", part->type, part->subtype); + add_term(pdata->_doc, mime + ctype); + } + + if ((fname = mu_msg_part_get_filename (part, FALSE))) { + const auto flat = Mu::utf8_flatten (fname); + g_free (fname); + add_term(pdata->_doc, file + flat); + } + + maybe_index_text_part (msg, part, pdata); +} + + +static void +add_terms_values_attach (Xapian::Document& doc, MuMsg *msg, + MuMsgFieldId mfid) +{ + PartData pdata (doc, mfid); + mu_msg_part_foreach (msg, MU_MSG_OPTION_RECURSE_RFC822, + (MuMsgPartForeachFunc)each_part, &pdata); +} + + +static void +add_terms_values_body (Xapian::Document& doc, MuMsg *msg, + MuMsgFieldId mfid) +{ + if (mu_msg_get_flags(msg) & MU_FLAG_ENCRYPTED) + return; /* ignore encrypted bodies */ + + auto str = mu_msg_get_body_text (msg, MU_MSG_OPTION_NONE); + if (!str) /* FIXME: html->txt fallback needed */ + str = mu_msg_get_body_html (msg, MU_MSG_OPTION_NONE); + if (!str) + return; /* no body... */ + + Xapian::TermGenerator termgen; + termgen.set_document(doc); + + const auto flat = Mu::utf8_flatten(str); + termgen.index_text (flat, 1, prefix(mfid)); +} + +struct MsgDoc { + Xapian::Document *_doc; + MuMsg *_msg; + Store *_store; + /* callback data, to determine whether this message is 'personal' */ + gboolean _personal; + const StringVec *_my_addresses; +}; + + +static void +add_terms_values_default (MuMsgFieldId mfid, MsgDoc *msgdoc) +{ + if (mu_msg_field_is_numeric (mfid)) + add_terms_values_number + (*msgdoc->_doc, msgdoc->_msg, mfid); + else if (mu_msg_field_is_string (mfid)) + add_terms_values_string + (*msgdoc->_doc, msgdoc->_msg, mfid); + else if (mu_msg_field_is_string_list(mfid)) + add_terms_values_string_list + (*msgdoc->_doc, msgdoc->_msg, mfid); + else + g_return_if_reached (); +} + +static void +add_terms_values (MuMsgFieldId mfid, MsgDoc* msgdoc) +{ + /* note: contact-stuff (To/Cc/From) will handled in + * each_contact_info, not here */ + if (!mu_msg_field_xapian_index(mfid) && + !mu_msg_field_xapian_term(mfid) && + !mu_msg_field_xapian_value(mfid)) + return; + + switch (mfid) { + case MU_MSG_FIELD_ID_DATE: + add_terms_values_date (*msgdoc->_doc, msgdoc->_msg, mfid); + break; + case MU_MSG_FIELD_ID_SIZE: + add_terms_values_size (*msgdoc->_doc, msgdoc->_msg, mfid); + break; + case MU_MSG_FIELD_ID_BODY_TEXT: + add_terms_values_body (*msgdoc->_doc, msgdoc->_msg, mfid); + break; + /* note: add_terms_values_attach handles _FILE, _MIME and + * _ATTACH_TEXT msgfields */ + case MU_MSG_FIELD_ID_FILE: + add_terms_values_attach (*msgdoc->_doc, msgdoc->_msg, mfid); + break; + case MU_MSG_FIELD_ID_MIME: + case MU_MSG_FIELD_ID_EMBEDDED_TEXT: + break; + case MU_MSG_FIELD_ID_THREAD_ID: + case MU_MSG_FIELD_ID_UID: + break; /* already taken care of elsewhere */ + default: + return add_terms_values_default (mfid, msgdoc); + } +} + + +static const std::string& +xapian_pfx (MuMsgContact *contact) +{ + static const std::string empty; + + /* use ptr to string to prevent copy... */ + switch (contact->type) { + case MU_MSG_CONTACT_TYPE_TO: + return prefix(MU_MSG_FIELD_ID_TO); + case MU_MSG_CONTACT_TYPE_FROM: + return prefix(MU_MSG_FIELD_ID_FROM); + case MU_MSG_CONTACT_TYPE_CC: + return prefix(MU_MSG_FIELD_ID_CC); + case MU_MSG_CONTACT_TYPE_BCC: + return prefix(MU_MSG_FIELD_ID_BCC); + default: + g_warning ("unsupported contact type %u", + (unsigned)contact->type); + return empty; + } +} + + +static void +add_address_subfields (Xapian::Document& doc, const char *addr, + const std::string& pfx) +{ + const char *at, *domain_part; + char *name_part; + + /* add "foo" and "bar.com" as terms as well for + * "foo@bar.com" */ + if (G_UNLIKELY(!(at = (g_strstr_len (addr, -1, "@"))))) + return; + + name_part = g_strndup(addr, at - addr); // foo + domain_part = at + 1; + + add_term(doc, pfx + name_part); + add_term(doc, pfx + domain_part); + + g_free (name_part); +} + +static gboolean +each_contact_info (MuMsgContact *contact, MsgDoc *msgdoc) +{ + /* for now, don't store reply-to addresses */ + if (mu_msg_contact_type (contact) == MU_MSG_CONTACT_TYPE_REPLY_TO) + return TRUE; + + const std::string pfx (xapian_pfx(contact)); + if (pfx.empty()) + return TRUE; /* unsupported contact type */ + + if (!mu_str_is_empty(contact->name)) { + Xapian::TermGenerator termgen; + termgen.set_document (*msgdoc->_doc); + const auto flat = Mu::utf8_flatten(contact->name); + termgen.index_text (flat, 1, pfx); + } + + if (!mu_str_is_empty(contact->email)) { + const auto flat = Mu::utf8_flatten(contact->email); + add_term(*msgdoc->_doc, pfx + flat); + add_address_subfields (*msgdoc->_doc, contact->email, pfx); + /* store it also in our contacts cache */ + auto& contacts = msgdoc->_store->priv()->contacts_; + contacts.add(Mu::ContactInfo(contact->full_address, + contact->email, + contact->name ? contact->name : "", + msgdoc->_personal, + mu_msg_get_date(msgdoc->_msg))); + } + + return TRUE; +} + + +static gboolean +each_contact_check_if_personal (MuMsgContact *contact, MsgDoc *msgdoc) +{ + if (msgdoc->_personal || !contact->email) + return TRUE; + + for (const auto& cur : *msgdoc->_my_addresses) { + if (g_ascii_strcasecmp + (contact->email, + (const char*)cur.c_str()) == 0) { + msgdoc->_personal = TRUE; + break; + } + } + + return TRUE; +} + +static Xapian::Document +new_doc_from_message (MuStore *store, MuMsg *msg) +{ + Xapian::Document doc; + MsgDoc docinfo = {&doc, msg, mutable_self(store), 0, NULL}; + + mu_msg_field_foreach ((MuMsgFieldForeachFunc)add_terms_values, &docinfo); + + /* determine whether this is 'personal' email, ie. one of my + * e-mail addresses is explicitly mentioned -- it's not a + * mailing list message. Callback will update docinfo->_personal */ + const auto& personal_addresses = self(store)->personal_addresses(); + if (personal_addresses.size()) { + docinfo._my_addresses = &personal_addresses; + mu_msg_contact_foreach + (msg, + (MuMsgContactForeachFunc)each_contact_check_if_personal, + &docinfo); + } + + /* also store the contact-info as separate terms, and add it + * to the cache */ + mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)each_contact_info, + &docinfo); + + // g_printerr ("\n--%s\n--\n", doc.serialise().c_str()); + + return doc; +} + +static void +update_threading_info (Xapian::WritableDatabase* db, + MuMsg *msg, Xapian::Document& doc) +{ + const GSList *refs; + + // refs contains a list of parent messages, with the oldest + // one first until the last one, which is the direct parent of + // the current message. of course, it may be empty. + // + // NOTE: there may be cases where the list is truncated; we happily + // ignore that case. + refs = mu_msg_get_references (msg); + + char thread_id[16+1]; + hash_str(thread_id, sizeof(thread_id), + refs ? (const char*)refs->data : mu_msg_get_msgid (msg)); + + add_term (doc, prefix(MU_MSG_FIELD_ID_THREAD_ID) + thread_id); + doc.add_value((Xapian::valueno)MU_MSG_FIELD_ID_THREAD_ID, thread_id); +} + + +static unsigned +add_or_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) +{ + g_return_val_if_fail (store, MU_STORE_INVALID_DOCID); + g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID); + + try { + Xapian::docid id; + Xapian::Document doc (new_doc_from_message(store, msg)); + const std::string term (get_uid_term (mu_msg_get_path(msg))); + + auto self = mutable_self(store); + auto wdb = self->priv()->wdb(); + + if (!self->in_transaction()) + self->priv()->begin_transaction(); + + add_term (doc, term); + + // update the threading info if this message has a message id + if (mu_msg_get_msgid (msg)) + update_threading_info (wdb.get(), msg, doc); + + if (docid == 0) + id = wdb->replace_document (term, doc); + else { + wdb->replace_document (docid, doc); + id = docid; + } + + if (++self->priv()->dirtiness_ >= BatchSize) + self->priv()->commit_transaction(); + + return id; + + } MU_XAPIAN_CATCH_BLOCK_G_ERROR (err, MU_ERROR_XAPIAN_STORE_FAILED); + + return MU_STORE_INVALID_DOCID; +} + +unsigned +mu_store_add_msg (MuStore *store, MuMsg *msg, GError **err) +{ + g_return_val_if_fail (store, MU_STORE_INVALID_DOCID); + g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID); + + return add_or_update_msg (store, 0, msg, err); +} + +unsigned +mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg, GError **err) +{ + g_return_val_if_fail (store, MU_STORE_INVALID_DOCID); + g_return_val_if_fail (msg, MU_STORE_INVALID_DOCID); + g_return_val_if_fail (docid != 0, MU_STORE_INVALID_DOCID); + + return add_or_update_msg (store, docid, msg, err); +} + +unsigned +mu_store_add_path (MuStore *store, const char *path, GError **err) try { + + MuMsg *msg; + unsigned docid; + + g_return_val_if_fail (store, FALSE); + g_return_val_if_fail (path, FALSE); + + const auto maildir{maildir_from_path(self(store)->root_maildir(), path)}; + msg = mu_msg_new_from_file (path, maildir.c_str(), err); + if (!msg) + return MU_STORE_INVALID_DOCID; + + docid = add_or_update_msg (store, 0, msg, err); + mu_msg_unref (msg); + + return docid; + +} catch (const Mu::Error& me) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN, + "%s", me.what()); + return MU_STORE_INVALID_DOCID; +} catch (...) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_INTERNAL, + "caught exception"); + return MU_STORE_INVALID_DOCID; +} + +XapianWritableDatabase* +mu_store_get_writable_database (MuStore *store) +{ + g_return_val_if_fail (store, NULL); + + return (XapianWritableDatabase*)mutable_self(store)->priv()->wdb().get(); +} + + +gboolean +mu_store_remove_path (MuStore *store, const char *msgpath) +{ + g_return_val_if_fail (store, FALSE); + g_return_val_if_fail (msgpath, FALSE); + + try { + const std::string term{(get_uid_term(msgpath))}; + auto wdb = mutable_self(store)->priv()->wdb(); + + wdb->delete_document (term); + //store->inc_processed(); + + return TRUE; + + } MU_XAPIAN_CATCH_BLOCK_RETURN (FALSE); +} + + +gboolean +mu_store_set_dirstamp (MuStore *store, const char* dirpath, + time_t stamp, GError **err) +{ + g_return_val_if_fail (store, FALSE); + g_return_val_if_fail (dirpath, FALSE); + + mutable_self(store)->set_dirstamp(dirpath, stamp); + + return TRUE; +} + +time_t +mu_store_get_dirstamp (const MuStore *store, const char *dirpath, GError **err) +{ + g_return_val_if_fail (store, 0); + g_return_val_if_fail (dirpath, 0); + + return self(store)->dirstamp(dirpath); +} + +void +mu_store_print_info (const MuStore *store, gboolean nocolor) +{ + const auto green{nocolor ? "" : MU_COLOR_GREEN}; + const auto def{nocolor ? "" : MU_COLOR_DEFAULT}; + + std::cout << "database-path : " + << green << self(store)->database_path() << def << "\n" + << "messages in store : " + << green << self(store)->size() << def << "\n" + << "schema-version : " + << green << self(store)->schema_version() << def << "\n"; + + const auto created{mu_store_created (store)}; + const auto tstamp{localtime (&created)}; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wformat-y2k" + char tbuf[64]; + strftime (tbuf, sizeof(tbuf), "%c", tstamp); +#pragma GCC diagnostic pop + + std::cout << "created : " << green << tbuf << def << "\n" + << "maildir : " + << green << self(store)->root_maildir() << def << "\n"; + + std::cout << ("personal-addresses : "); + + auto addrs{mu_store_personal_addresses (store)}; + if (!addrs || g_strv_length(addrs) == 0) + std::cout << green << "<none>" << def << "\n"; + else { + for (auto i = 0U; addrs[i]; ++i) { + std::cout << (i != 0 ? " " : "") + << green << addrs[i] << def << "\n"; + } + } + + g_strfreev(addrs); +} +} diff --git a/lib/mu-store.hh b/lib/mu-store.hh new file mode 100644 index 0000000..f34c70b --- /dev/null +++ b/lib/mu-store.hh @@ -0,0 +1,568 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_STORE_HH__ +#define __MU_STORE_HH__ + +#include <mu-msg.h> + +#ifdef __cplusplus + +#include "mu-contacts.hh" + +#include <xapian.h> + +#include <string> +#include <vector> +#include <ctime> + +#include <utils/mu-utils.hh> + +namespace Mu { + +class Store { +public: + /** + * Construct a store for an existing document database + * + * @param path path to the database + * @param readonly whether to open the database in read-only mode + */ + Store (const std::string& path, bool readonly=true); + + /** + * Construct a store for a not-yet-existing document database + * + * @param path path to the database + * @param maildir maildir to use for this store + * @param personal_addressesaddresses that should be recognized as + * 'personal' for identifying personal messages. + */ + Store (const std::string& path, const std::string& maildir, + const StringVec& personal_addresses); + + /** + * DTOR + */ + ~Store(); + + /** + * Is the store read-only? + * + * @return true or false + */ + bool read_only() const; + + /** + * Path to the database; this is some subdirectory of the path + * passed to the constructor. + * + * @return the database path + */ + const std::string& database_path() const; + + /** + * Path to the top-level Maildir + * + * @return the maildir + */ + const std::string& root_maildir() const; + + /** + * Version of the database-schema + * + * @return the maildir + */ + const std::string& schema_version() const; + + + /** + * Time of creation of the store + * + * @return creation time + */ + std::time_t created() const; + + /** + * Get a vec with the personal addresses + * + * @return personal addresses + */ + const StringVec& personal_addresses() const; + + /** + * Get the Contacts object for this store + * + * @return the Contacts object + */ + const Contacts& contacts() const; + + /** + * Add a message to the store. + * + * @param path the message path. + * + * @return the doc id of the added message + */ + unsigned add_message (const std::string& path); + + /** + * Add a message to the store. + * + * @param path the message path. + * + * @return true if removing happened; false otherwise. + */ + bool remove_message (const std::string& path); + + /** + * Fina message in the store. + * + * @param docid doc id for the message to find + * + * @return a message (owned by caller), or nullptr + */ + MuMsg* find_message (unsigned docid) const; + + /** + * does a certain message exist in the store already? + * + * @param path the message path + * + * @return true if the message exists in the store, false otherwise + */ + bool contains_message (const std::string& path) const; + + /** + * Get the timestamp for some directory + * + * @param path the path + * + * @return the timestamp, or 0 if not found + */ + time_t dirstamp (const std::string& path) const; + + /** + * Set the timestamp for some directory + * + * @param path a filesystem path + * @param tstamp the timestamp for that path + */ + void set_dirstamp (const std::string& path, time_t tstamp); + + /** + * Get the number of documents in the document database + * + * @return the number + */ + std::size_t size() const; + + /** + * Is the database empty? + * + * @return true or false + */ + bool empty() const; + + /** + * Begin a database transaction + */ + void begin_transaction(); + + /** + * Commit a database transaction + * + */ + void commit_transaction(); + + /** + * Are we in a transaction? + * + * @return true or false + */ + bool in_transaction() const; + + + /** + * Get a reference to the private data. For internal use. + * + * @return private reference. + */ + struct Private; + std::unique_ptr<Private>& priv() { return priv_; } + const std::unique_ptr<Private>& priv() const { return priv_; } + +private: + std::unique_ptr<Private> priv_; +}; + +} // namespace Mu + + +#endif /*__cplusplus*/ + +#include <glib.h> +#include <inttypes.h> +#include <utils/mu-util.h> +#include <mu-contacts.hh> + +G_BEGIN_DECLS + +struct MuStore_; +typedef struct MuStore_ MuStore; + +/* http://article.gmane.org/gmane.comp.search.xapian.general/3656 */ +#define MU_STORE_MAX_TERM_LENGTH (240) + + +/** + * create a new read-only Xapian store, for querying documents + * + * @param path the path to the database + * @param err to receive error info or NULL. err->code is MuError value + * + * @return a new MuStore object with ref count == 1, or NULL in case of error; + * free with mu_store_unref + */ +MuStore* mu_store_new_readable (const char* xpath, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; +/** + * create a new writable Xapian store, a place to store documents + * + * @param path the path to the database + * @param err to receive error info or NULL. err->code is MuError value + * + * @return a new MuStore object with ref count == 1, or NULL in case + * of error; free with mu_store_unref + */ +MuStore* mu_store_new_writable (const char *xpath, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * create a new writable Xapian store, a place to store documents, and + * create/overwrite the existing database. + * + * @param path the path to the database + * @param path to the maildir + * @param personal_addressesaddresses that should be recognized as + * 'personal' for identifying personal messages. + * @param err to receive error info or NULL. err->code is MuError value + * + * @return a new MuStore object with ref count == 1, or NULL in case + * of error; free with mu_store_unref + */ +MuStore* mu_store_new_create (const char *xpath, const char *maildir, + const char **personal_addresses, GError **err) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * increase the reference count for this store with 1 + * + * @param store a valid store object + * + * @return the same store with increased ref count, or NULL in case of + * error + */ +MuStore* mu_store_ref (MuStore *store); + +/** + * decrease the reference count for this store with 1 + * + * @param store a valid store object + * + * @return NULL + */ +MuStore* mu_store_unref (MuStore *store); + + +/** + * we need this when using Xapian::(Writable)Database* from C + */ +typedef gpointer XapianWritableDatabase; +typedef gpointer XapianDatabase; + + +/** + * get the underlying writable database object for this store; not + * that this pointer becomes in valid after mu_store_destroy + * + * @param store a valid store + * + * @return a Xapian::WritableDatabase (you'll need to cast in C++), or + * NULL in case of error. + */ +XapianWritableDatabase* mu_store_get_writable_database (MuStore *store); + + +/** + * get the underlying read-only database object for this store; not that this + * pointer becomes in valid after mu_store_destroy + * + * @param store a valid store + * + * @return a Xapian::Database (you'll need to cast in C++), or + * NULL in case of error. + */ +XapianDatabase* mu_store_get_read_only_database (MuStore *store); + +/** + * get the version of the xapian database (ie., the version of the + * 'schema' we are using). If this version != MU_STORE_SCHEMA_VERSION, + * it's means we need to a full reindex. + * + * @param store the store to inspect + * + * @return the version of the database as a newly allocated string + * (free with g_free); if there is no version yet, it will return NULL + */ +const char* mu_store_schema_version (const MuStore* store); + + +/** + * Get the database-path for this message store + * + * @param store the store to inspetc + * + * @return the database-path + */ +const char *mu_store_database_path (const MuStore *store); + + +/** + * Get the root-maildir for this message store. + * + * @param store the store + * + * @return the maildir. + */ +const char *mu_store_root_maildir(const MuStore *store); + + +/** + * Get the time this database was created + * + * @param store the store + * + * @return the maildir. + */ +time_t mu_store_created(const MuStore *store); + +/** + * Get the list of personal addresses from the store + * + * @param store the message store + * + * @return the list of personal addresses, or NULL in case of error. + * + * Free with g_strfreev(). + */ +char** mu_store_personal_addresses (const MuStore *store); + +/** + * Get the a MuContacts* ptr for this store. + * + * @param store a store + * + * @return the contacts ptr + */ +const MuContacts* mu_store_contacts (MuStore *store); + + +/** + * get the numbers of documents in the database + * + * @param index a valid MuStore instance + * @param err to receive error info or NULL. err->code is MuError value + * + * @return the number of documents in the database; (unsigned)-1 in + * case of error + */ +unsigned mu_store_count (const MuStore *store, GError **err); + + +/** + * try to flush/commit all outstanding work to the database and the contacts + * cache. + * + * @param store a valid xapian store + */ +void mu_store_flush (MuStore *store); + +#define MU_STORE_INVALID_DOCID 0 + +/** + * store an email message in the XapianStore + * + * @param store a valid store + * @param msg a valid message + * @param err receives error information, if any, or NULL + * + * @return the docid of the stored message, or 0 + * (MU_STORE_INVALID_DOCID) in case of error + */ +unsigned mu_store_add_msg (MuStore *store, MuMsg *msg, GError **err); + + +/** + * update an email message in the XapianStore + * + * @param store a valid store + * @param the docid for the message + * @param msg a valid message + * @param err receives error information, if any, or NULL + * + * @return the docid of the stored message, or 0 + * (MU_STORE_INVALID_DOCID) in case of error + */ +unsigned mu_store_update_msg (MuStore *store, unsigned docid, MuMsg *msg, + GError **err); + +/** + * store an email message in the XapianStore; similar to + * mu_store_store, but instead takes a path as parameter instead of a + * MuMsg* + * + * @param store a valid store + * @param path full filesystem path to a valid message + * @param err receives error information, if any, or NULL + * + * @return the docid of the stored message, or 0 + * (MU_STORE_INVALID_DOCID) in case of error + */ +unsigned mu_store_add_path (MuStore *store, const char *path, GError **err); + +/** + * remove a message from the database based on its path + * + * @param store a valid store + * @param msgpath path of the message (note, this is only used to + * *identify* the message; a common use of this function is to remove + * a message from the database, for which there is no message anymore + * in the filesystem. + * + * @return TRUE if it succeeded, FALSE otherwise + */ +gboolean mu_store_remove_path (MuStore *store, const char* msgpath); + +/** + * does a certain message exist in the database already? + * + * @param store a store + * @param path the message path + * + * @return TRUE if the message exists, FALSE otherwise + */ +gboolean mu_store_contains_message (const MuStore *store, const char* path); + +/** + * get the docid for message at path + * + * @param store a store + * @param path the message path + * @param err to receive error info or NULL. err->code is MuError value + * + * @return the docid if the message was found, MU_STORE_INVALID_DOCID (0) otherwise + * */ +unsigned mu_store_get_docid_for_path (const MuStore *store, const char* path, + GError **err); + +/** + * store a timestamp for a directory + * + * @param store a valid store + * @param dirpath path to some directory + * @param stamp a timestamp + * @param err to receive error info or NULL. err->code is MuError value + * + * @return TRUE if setting the timestamp succeeded, FALSE otherwise + */ +gboolean mu_store_set_dirstamp (MuStore *store, const char* dirpath, + time_t stamp, GError **err); + +/** + * get the timestamp for a directory + * + * @param store a valid store + * @param msgpath path to some directory + * @param err to receive error info or NULL. err->code is MuError value + * + * @return the timestamp, or 0 in case of error + */ +time_t mu_store_get_dirstamp (const MuStore *store, const char* dirpath, + GError **err); + +/** + * check whether this store is read-only + * + * @param store a store + * + * @return TRUE if the store is read-only, FALSE otherwise (and in + * case of error) + */ +gboolean mu_store_is_read_only (const MuStore *store); + +/** + * call a function for each document in the database + * + * @param self a valid store + * @param func a callback function to to call for each document + * @param user_data a user pointer passed to the callback function + * @param err to receive error info or NULL. err->code is MuError value + * + * @return MU_OK if all went well, MU_STOP if the foreach was interrupted, + * MU_ERROR in case of error + */ +typedef MuError (*MuStoreForeachFunc) (const char* path, gpointer user_data); +MuError mu_store_foreach (MuStore *self, MuStoreForeachFunc func, + void *user_data, GError **err); + +/** + * check if the database is locked for writing + * + * @param xpath path to a xapian database + * + * @return TRUE if it is locked, FALSE otherwise (or in case of error) + */ +gboolean mu_store_database_is_locked (const gchar *xpath); + +/** + * get a specific message, based on its Xapian docid + * + * @param self a valid MuQuery instance + * @param docid the Xapian docid for the wanted message + * @param err receives error information, or NULL + * + * @return a MuMsg instance (use mu_msg_unref when done with it), or + * NULL in case of error + */ +MuMsg* mu_store_get_msg (const MuStore *self, unsigned docid, GError **err) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * Print some information about the store + * + * @param store a store + * @param nocolor whether to _not_ show color + */ +void mu_store_print_info (const MuStore *store, gboolean nocolor); + + +G_END_DECLS + +#endif /* __MU_STORE_HH__ */ diff --git a/lib/mu-threader.c b/lib/mu-threader.c new file mode 100644 index 0000000..80857ef --- /dev/null +++ b/lib/mu-threader.c @@ -0,0 +1,455 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#include <math.h> /* for log, ceil */ +#include <string.h> /* for memset */ + +#include "mu-threader.h" +#include "mu-container.h" +#include "utils/mu-str.h" + +/* msg threading implementation based on JWZ's algorithm, as described in: + * http://www.jwz.org/doc/threading.html + * + * the implementation follows the terminology from that doc, so should + * be understandable from that... I did change things a bit though + * + * the end result of the threading operation is a hashtable which maps + * docids (ie., Xapian documents == messages) to 'thread paths'; a + * thread path is a string denoting the 2-dimensional place of a + * message in a list of messages, + * + * Msg1 => 00000 + * Msg2 => 00001 + * Msg3 (child of Msg2) => 00001:00000 + * Msg4 (child of Msg2) => 00001:00001 + * Msg5 (child of Msg4) => 00001:00001:00000 + * Msg6 => 00002 + * + * the padding-0's are added to make them easy to sort using strcmp; + * the number hexadecimal numbers, and the length of the 'segments' + * (the parts separated by the ':') is equal to ceil(log_16(matchnum)) + * + */ + +/* step 1 */ static GHashTable* create_containers (MuMsgIter *iter); +/* step 2 */ static MuContainer *find_root_set (GHashTable *ids); +static MuContainer* prune_empty_containers (MuContainer *root); +/* static void group_root_set_by_subject (GSList *root_set); */ +GHashTable* create_doc_id_thread_path_hash (MuContainer *root, + size_t match_num); + +/* msg threading algorithm, based on JWZ's algorithm, + * http://www.jwz.org/doc/threading.html */ +GHashTable* +mu_threader_calculate (MuMsgIter *iter, size_t matchnum, + MuMsgFieldId sortfield, gboolean descending) +{ + GHashTable *id_table, *thread_ids; + MuContainer *root_set; + + g_return_val_if_fail (iter, FALSE); + g_return_val_if_fail (mu_msg_field_id_is_valid (sortfield) || + sortfield == MU_MSG_FIELD_ID_NONE, + FALSE); + + /* step 1 */ + id_table = create_containers (iter); + if (matchnum == 0) + return id_table; /* just return an empty table */ + + /* step 2 -- the root_set is the list of children without parent */ + root_set = find_root_set (id_table); + + /* step 3: skip until the end; we still need to containers */ + + /* step 4: prune empty containers */ + root_set = prune_empty_containers (root_set); + + /* sort root set */ + if (sortfield != MU_MSG_FIELD_ID_NONE) + root_set = mu_container_sort (root_set, sortfield, descending, + NULL); + + /* step 5: group root set by subject */ + /* group_root_set_by_subject (root_set); */ + + /* sort */ + mu_msg_iter_reset (iter); /* go all the way back */ + + /* finally, deliver the docid => thread-path hash */ + thread_ids = mu_container_thread_info_hash_new (root_set, + matchnum); + + g_hash_table_destroy (id_table); /* step 3*/ + + return thread_ids; +} + +G_GNUC_UNUSED static void +check_dup (const char *msgid, MuContainer *c, GHashTable *hash) +{ + if (g_hash_table_lookup (hash, c)) { + g_warning ("ALREADY!!"); + mu_container_dump (c, FALSE); + g_assert (0); + } else + g_hash_table_insert (hash, c, GUINT_TO_POINTER(TRUE)); +} + + +G_GNUC_UNUSED static void +assert_no_duplicates (GHashTable *ids) +{ + GHashTable *hash; + + hash = g_hash_table_new (g_direct_hash, g_direct_equal); + + g_hash_table_foreach (ids, (GHFunc)check_dup, hash); + + g_hash_table_destroy (hash); +} + + +/* a referred message is a message that is referred by some other + * message */ +static MuContainer* +find_or_create_referred (GHashTable *id_table, const char *msgid, + gboolean *created) +{ + MuContainer *c; + + g_return_val_if_fail (msgid, NULL); + + c = g_hash_table_lookup (id_table, msgid); + *created = !c; + if (!c) { + c = mu_container_new (NULL, 0, msgid); + g_hash_table_insert (id_table, (gpointer)msgid, c); + /* assert_no_duplicates (id_table); */ + } + + + return c; +} + +/* find a container for the given msgid; if it does not exist yet, + * create a new one, and register it */ +static MuContainer* +find_or_create (GHashTable *id_table, MuMsg *msg, guint docid) +{ + MuContainer *c; + const char* msgid; + char fake[32]; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (docid != 0, NULL); + + msgid = mu_msg_get_msgid (msg); + if (!msgid) + msgid = mu_msg_get_path (msg); /* fake it */ + if (!msgid) { /* no path either? seems to happen... */ + g_warning ("message without path"); + g_snprintf (fake, sizeof(fake), "fake:%p", (gpointer)msg); + msgid = fake; + } + + /* XXX the '<none>' works around a crash; find a better + * solution */ + c = g_hash_table_lookup (id_table, msgid); + + /* If id_table contains an empty MuContainer for this ID: * * + * Store this message in the MuContainer's message slot. */ + if (c) { + if (!c->msg) { + c->msg = mu_msg_ref (msg); + c->docid = docid; + return c; + } else { + /* special case, not in the JWZ algorithm: the + * container exists already and has a message; this + * means that we are seeing *another message* with a + * message-id we already saw... create this message, + * and mark it as a duplicate, and a child of the one + * we saw before; use its path as a fake message-id + * */ + MuContainer *c2; + const char* fake_msgid; + + fake_msgid = mu_msg_get_path (msg); + + c2 = mu_container_new (msg, docid, fake_msgid); + c2->flags = MU_CONTAINER_FLAG_DUP; + /*c = */ mu_container_append_children (c, c2); + + g_hash_table_insert (id_table, (gpointer)fake_msgid, c2); + + return NULL; /* don't process this message further */ + } + } else { /* Else: Create a new MuContainer object holding + this message; Index the MuContainer by + Message-ID in id_table. */ + c = mu_container_new (msg, docid, msgid); + g_hash_table_insert (id_table, (gpointer)msgid, c); + /* assert_no_duplicates (id_table); */ + + return c; + } +} + +static gboolean +child_elligible (MuContainer *parent, MuContainer *child, gboolean created) +{ + if (!parent || !child) + return FALSE; + if (child->parent) + return FALSE; + /* if (created) */ + /* return TRUE; */ + if (mu_container_reachable (parent, child)) + return FALSE; + if (mu_container_reachable (child, parent)) + return FALSE; + + return TRUE; +} + + + +static void /* 1B */ +handle_references (GHashTable *id_table, MuContainer *c) +{ + const GSList *refs, *cur; + MuContainer *parent; + gboolean created; + + refs = mu_msg_get_references (c->msg); + if (!refs) + return; /* nothing to do */ + + /* For each element in the message's References field: + + Find a MuContainer object for the given Message-ID: If + there's one in id_table use that; Otherwise, make (and + index) one with a null Message. */ + + /* go over over our list of refs, until 1 before the last... */ + created = FALSE; + for (parent = NULL, cur = refs; cur; cur = g_slist_next (cur)) { + + MuContainer *child; + child = find_or_create_referred (id_table, (gchar*)cur->data, + &created); + + /* if we find the current message in their own refs, break now + so that parent != c in next step */ + if (child == c) + break; + + /*Link the References field's MuContainers together in + * the order implied by the References header. + + If they are already linked, don't change the existing + links. Do not add a link if adding that link would + introduce a loop: that is, before asserting A->B, + search down the children of B to see if A is + reachable, and also search down the children of A to + see if B is reachable. If either is already reachable + as a child of the other, don't add the link. */ + + if (child_elligible (parent, child, created)) + /*parent =*/ + mu_container_append_children (parent, child); + + parent = child; + } + + /* 'parent' points to the last ref: our direct parent; + + Set the parent of this message to be the last element in + References. Note that this message may have a parent + already: this can happen because we saw this ID in a + References field, and presumed a parent based on the other + entries in that field. Now that we have the actual message, + we can be more definitive, so throw away the old parent and + use this new one. Find this MuContainer in the parent's + children list, and unlink it. + + Note that this could cause this message to now have no + parent, if it has no references field, but some message + referred to it as the non-first element of its + references. (Which would have been some kind of lie...) + + Note that at all times, the various ``parent'' and ``child'' fields + must be kept inter-consistent. */ + + /* optimization: if the the message was newly added, it's by + definition not reachable yet */ + + /* So, we move c and its descendants to become a child of parent if: + * both are not NULL + * parent is not a descendant of c. + * both are different from each other (guaranteed in last loop) */ + + if (parent && c && !(c->child && mu_container_reachable (c->child, parent))) { + + /* if c already has a parent, remove c from its parent children + and reparent it, as now we know who is c's parent reliably */ + if (c->parent) { + mu_container_remove_child(c->parent, c); + c->next = c->last = c->parent = NULL; + } + + /*parent = */mu_container_append_children (parent, c); + } +} + + + +/* step 1: create the containers, connect them, and fill the id_table */ +static GHashTable* +create_containers (MuMsgIter *iter) +{ + GHashTable *id_table; + id_table = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify)mu_container_destroy); + + for (mu_msg_iter_reset (iter); !mu_msg_iter_is_done (iter); + mu_msg_iter_next (iter)) { + + MuContainer *c; + MuMsg *msg; + unsigned docid; + + /* 1.A */ + msg = mu_msg_iter_get_msg_floating (iter); /* don't unref */ + docid = mu_msg_iter_get_docid (iter); + + c = find_or_create (id_table, msg, docid); + + /* 1.B and C */ + if (c) + handle_references (id_table, c); + } + + return id_table; +} + + + +static void +filter_root_set (const gchar *msgid, MuContainer *c, MuContainer **root_set) +{ + /* ignore children */ + if (c->parent) + return; + + /* ignore duplicates */ + if (c->flags & MU_CONTAINER_FLAG_DUP) + return; + + if (*root_set == NULL) { + *root_set = c; + return; + } else + *root_set = mu_container_append_siblings (*root_set, c); +} + + +/* 2. Walk over the elements of id_table, and gather a list of the + MuContainer objects that have no parents, but do have children */ +static MuContainer* +find_root_set (GHashTable *ids) +{ + MuContainer *root_set; + + root_set = NULL; + g_hash_table_foreach (ids, (GHFunc)filter_root_set, &root_set); + + return root_set; +} + + +static gboolean +prune_maybe (MuContainer *c) +{ + MuContainer *cur; + + for (cur = c->child; cur; cur = cur->next) { + if (cur->flags & MU_CONTAINER_FLAG_DELETE) { + c = mu_container_remove_child (c, cur); + } else if (cur->flags & MU_CONTAINER_FLAG_SPLICE) { + c = mu_container_splice_grandchildren (c, cur); + c = mu_container_remove_child (c, cur); + } + } + + g_return_val_if_fail (c, FALSE); + + /* don't touch containers with messages */ + if (c->msg) + return TRUE; + + /* A. If it is an msg-less container with no children, mark it for + * deletion. */ + if (!c->child) { + c->flags |= MU_CONTAINER_FLAG_DELETE; + return TRUE; + } + + /* B. If the MuContainer has no Message, but does have + * children, remove this container but promote its + * children to this level (that is, splice them in to + * the current child list.) + * + * Do not promote the children if doing so would + * promote them to the root set -- unless there is + * only one child, in which case, do. + */ + if (c->child->next) /* ie., > 1 child */ + return TRUE; + + c->flags |= MU_CONTAINER_FLAG_SPLICE; + + return TRUE; +} + + +static MuContainer* +prune_empty_containers (MuContainer *root_set) +{ + MuContainer *cur; + + mu_container_foreach (root_set, + (MuContainerForeachFunc)prune_maybe, + NULL); + + /* and prune the root_set itself... */ + for (cur = root_set; cur; cur = cur->next) { + if (cur->flags & MU_CONTAINER_FLAG_DELETE) { + root_set = mu_container_remove_sibling (root_set, cur); + } else if (cur->flags & MU_CONTAINER_FLAG_SPLICE) { + root_set = mu_container_splice_children (root_set, cur); + root_set = mu_container_remove_sibling (root_set, cur); + } + } + + return root_set; +} diff --git a/lib/mu-threader.h b/lib/mu-threader.h new file mode 100644 index 0000000..dc8d414 --- /dev/null +++ b/lib/mu-threader.h @@ -0,0 +1,56 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_THREADER_H__ +#define __MU_THREADER_H__ + +#include <glib.h> +#include <mu-msg-iter.h> + +G_BEGIN_DECLS + +/** + * takes an iter and the total number of matches, and from this + * generates a hash-table with information about the thread structure + * of these matches. + * + * the algorithm to find this structure is based on JWZ's + * message-threading algorithm, as descrbed in: + * http://www.jwz.org/doc/threading.html + * + * the returned hashtable maps the Xapian docid of iter (msg) to a ptr + * to a MuMsgIterThreadInfo structure (see mu-msg-iter.h) + * + * @param iter an iter; note this function will mu_msgi_iter_reset this iterator + * @param matches the number of matches in the set * + * @param sortfield the field to sort results by, or + * MU_MSG_FIELD_ID_NONE if no sorting should be performed + * @param revert if TRUE, if revert the sorting order + * + * @return a hashtable; free with g_hash_table_destroy when done with it + */ +GHashTable *mu_threader_calculate (MuMsgIter *iter, size_t matches, + MuMsgFieldId sortfield, gboolean revert); + + +G_END_DECLS + +#endif /*__MU_THREADER_H__*/ diff --git a/lib/query/Makefile.am b/lib/query/Makefile.am new file mode 100644 index 0000000..6923d2b --- /dev/null +++ b/lib/query/Makefile.am @@ -0,0 +1,99 @@ +## Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +@VALGRIND_CHECK_RULES@ + +AM_CXXFLAGS= \ + -I$(srcdir)/.. \ + -I$(top_srcdir)/lib \ + $(GLIB_CFLAGS) \ + $(XAPIAN_CXXFLAGS) \ + $(WARN_CXXFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -Wno-inline \ + -Wno-switch-enum + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) \ + $(WARN_LDFLAGS) + +noinst_PROGRAMS= \ + tokenize \ + parse + +noinst_LTLIBRARIES= \ + libmu-query.la + +libmu_query_la_SOURCES= \ + mu-data.hh \ + mu-parser.cc \ + mu-parser.hh \ + mu-proc-iface.hh \ + mu-tokenizer.cc \ + mu-tokenizer.hh \ + mu-tree.hh \ + mu-xapian.cc \ + mu-xapian.hh + +libmu_query_la_LIBADD= \ + $(WARN_LDFLAGS) \ + $(GLIB_LIBS) \ + $(XAPIAN_LIBS) \ + ../utils/libmu-utils.la \ + $(CODE_COVERAGE_LIBS) + +VALGRIND_SUPPRESSIONS_FILES= \ + ${top_srcdir}/mu.supp + +tokenize_SOURCES= \ + tokenize.cc + +tokenize_LDADD= \ + $(WARN_LDFLAGS) \ + libmu-query.la + +parse_SOURCES= \ + parse.cc + +parse_LDADD= \ + $(WARN_LDFLAGS) \ + libmu-query.la + +noinst_PROGRAMS+=$(TEST_PROGS) + +TEST_PROGS+= \ + test-tokenizer +test_tokenizer_SOURCES= \ + test-tokenizer.cc +test_tokenizer_LDADD= \ + libmu-query.la + +TEST_PROGS+= \ + test-parser +test_parser_SOURCES= \ + test-parser.cc +test_parser_LDADD= \ + libmu-query.la + +TESTS=$(TEST_PROGS) + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/query/mu-data.hh b/lib/query/mu-data.hh new file mode 100644 index 0000000..25d3634 --- /dev/null +++ b/lib/query/mu-data.hh @@ -0,0 +1,155 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __DATA_HH__ +#define __DATA_HH__ + +#include <string> +#include <iostream> +#include <regex> + +#include <utils//mu-utils.hh> + +namespace Mu { + +// class representing some data item; either a Value or a Range a Value can still be a Regex (but +// that's not a separate type here) +struct Data { + enum class Type { Value, Range }; + virtual ~Data() = default; + + Type type; /**< type of data */ + std::string field; /**< full name of the field */ + std::string prefix; /**< Xapian prefix for thef field */ + unsigned id; /**< Xapian value no for the field */ + +protected: + Data (Type _type, const std::string& _field, const std::string& _prefix, + unsigned _id): type(_type), field(_field), prefix(_prefix), id(_id) {} +}; + + +/** + * operator<< + * + * @param os an output stream + * @param t a data type + * + * @return the updated output stream + */ +inline std::ostream& +operator<< (std::ostream& os, Data::Type t) +{ + switch (t) { + case Data::Type::Value: os << "value"; break; + case Data::Type::Range: os << "range"; break; + default: os << "bug"; break; + } + return os; +} + + +/** + * Range type -- [a..b] + */ +struct Range: public Data { + /** + * Construct a range + * + * @param _field the field + * @param _prefix the xapian prefix + * @param _id xapian value number + * @param _lower lower bound + * @param _upper upper bound + */ + Range (const std::string& _field, const std::string& _prefix, + unsigned _id, + const std::string& _lower,const std::string& _upper): + + Data(Data::Type::Range, _field, _prefix, _id), + lower(_lower), upper(_upper) {} + + std::string lower; /**< lower bound */ + std::string upper; /**< upper bound */ +}; + + +/** + * Basic value + * + */ +struct Value: public Data { + /** + * Construct a Value + * + * @param _field the field + * @param _prefix the xapian prefix + * @param _id xapian value number + * @param _value the value + */ + Value (const std::string& _field, const std::string& _prefix, + unsigned _id, const std::string& _value, bool _phrase = false): + Data(Value::Type::Value, _field, _prefix, _id), + value(_value), phrase(_phrase) {} + + std::string value; /**< the value */ + bool phrase; +}; + + +/** + * operator<< + * + * @param os an output stream + * @param v a data ptr + * + * @return the updated output stream + */ +inline std::ostream& +operator<< (std::ostream& os, const std::unique_ptr<Data>& v) +{ + switch (v->type) { + case Data::Type::Value: { + const auto bval = dynamic_cast<Value*> (v.get()); + os << ' ' << quote(v->field) << ' ' + << quote(utf8_flatten(bval->value)); + if (bval->phrase) + os << " (ph)"; + + break; + } + case Data::Type::Range: { + const auto rval = dynamic_cast<Range*> (v.get()); + os << ' ' << quote(v->field) << ' ' + << quote(rval->lower) << ' ' + << quote(rval->upper); + break; + } + default: + os << "unexpected type"; + break; + } + + return os; +} + +} // namespace Mu + + +#endif /* __DATA_HH__ */ diff --git a/lib/query/mu-parser.cc b/lib/query/mu-parser.cc new file mode 100644 index 0000000..8becb1d --- /dev/null +++ b/lib/query/mu-parser.cc @@ -0,0 +1,344 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ +#include "mu-parser.hh" +#include "mu-tokenizer.hh" +#include "utils/mu-utils.hh" +#include "utils/mu-error.hh" + +using namespace Mu; + +// 3 precedence levels: units (NOT,()) > factors (OR) > terms (AND) + +// query -> <term-1> | ε +// <term-1> -> <factor-1> <term-2> | ε +// <term-2> -> OR|XOR <term-1> | ε +// <factor-1> -> <unit> <factor-2> | ε +// <factor-2> -> [AND]|AND NOT <factor-1> | ε +// <unit> -> [NOT] <term-1> | ( <term-1> ) | <data> +// <data> -> <value> | <range> | <regex> +// <value> -> [field:]value +// <range> -> [field:][lower]..[upper] +// <regex> -> [field:]/regex/ + + +#define BUG(...) Mu::Error (Error::Code::Internal, format("%u: BUG: ",__LINE__) \ + + format(__VA_ARGS__)) + +static Token +look_ahead (const Mu::Tokens& tokens) +{ + return tokens.front(); +} + +static Mu::Tree +empty() +{ + return {{Node::Type::Empty}}; +} + +static Mu::Tree term_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings); + + +static Mu::Tree +value (const ProcIface::FieldInfoVec& fields, const std::string& v, + size_t pos, ProcPtr proc, WarningVec& warnings) +{ + auto val = utf8_flatten(v); + + if (fields.empty()) + throw BUG("expected one or more fields"); + + if (fields.size() == 1) { + const auto item = fields.front(); + return Tree({Node::Type::Value, + std::make_unique<Value>( + item.field, item.prefix, item.id, + proc->process_value(item.field, val), + item.supports_phrase)}); + } + + // a 'multi-field' such as "recip:" + Tree tree(Node{Node::Type::OpOr}); + for (const auto& item: fields) + tree.add_child (Tree({Node::Type::Value, + std::make_unique<Value>( + item.field, item.prefix, item.id, + proc->process_value(item.field, val), + item.supports_phrase)})); + return tree; +} + +static Mu::Tree +regex (const ProcIface::FieldInfoVec& fields, const std::string& v, + size_t pos, ProcPtr proc, WarningVec& warnings) +{ + if (v.length() < 2) + throw BUG("expected regexp, got '%s'", v.c_str()); + + const auto rxstr = utf8_flatten(v.substr(1, v.length()-2)); + + try { + Tree tree(Node{Node::Type::OpOr}); + const auto rx = std::regex (rxstr); + for (const auto& field: fields) { + const auto terms = proc->process_regex (field.field, rx); + for (const auto& term: terms) { + tree.add_child (Tree( + {Node::Type::Value, + std::make_unique<Value>(field.field, "", + field.id, term)})); + } + } + + if (tree.children.empty()) + return empty(); + else + return tree; + + } catch (...) { + // fallback + warnings.push_back ({pos, "invalid regexp"}); + return value (fields, v, pos, proc, warnings); + } +} + + + +static Mu::Tree +range (const ProcIface::FieldInfoVec& fields, const std::string& lower, + const std::string& upper, size_t pos, ProcPtr proc, + WarningVec& warnings) +{ + if (fields.empty()) + throw BUG("expected field"); + + const auto& field = fields.front(); + if (!proc->is_range_field(field.field)) + return value (fields, lower + ".." + upper, pos, proc, warnings); + + auto prange = proc->process_range (field.field, lower, upper); + if (prange.lower > prange.upper) + prange = proc->process_range (field.field, upper, lower); + + return Tree({Node::Type::Range, + std::make_unique<Range>(field.field, field.prefix, field.id, + prange.lower, prange.upper)}); +} + + +static Mu::Tree +data (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings) +{ + const auto token = look_ahead(tokens); + if (token.type != Token::Type::Data) + warnings.push_back ({token.pos, "expected: value"}); + + tokens.pop_front(); + + std::string field, val; + const auto col = token.str.find (":"); + if (col != 0 && col != std::string::npos && col != token.str.length()-1) { + field = token.str.substr(0, col); + val = token.str.substr(col + 1); + } else + val = token.str; + + auto fields = proc->process_field (field); + if (fields.empty()) {// not valid field... + warnings.push_back ({token.pos, format ("invalid field '%s'", field.c_str())}); + fields = proc->process_field (""); + // fallback, treat the whole of foo:bar as a value + return value (fields, field + ":" + val, token.pos, proc, warnings); + } + + // does it look like a regexp? + if (val.length() >=2 ) + if (val[0] == '/' && val[val.length()-1] == '/') + return regex (fields, val, token.pos, proc, warnings); + + // does it look like a range? + const auto dotdot = val.find(".."); + if (dotdot != std::string::npos) + return range(fields, val.substr(0, dotdot), val.substr(dotdot + 2), + token.pos, proc, warnings); + else if (proc->is_range_field(fields.front().field)) { + // range field without a range - treat as field:val..val + return range (fields, val, val, token.pos, proc, warnings); + } + + // if nothing else, it's a value. + return value (fields, val, token.pos, proc, warnings); +} + +static Mu::Tree +unit (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings) +{ + if (tokens.empty()) { + warnings.push_back ({0, "expected: unit"}); + return empty(); + } + + const auto token = look_ahead (tokens); + + if (token.type == Token::Type::Not) { + tokens.pop_front(); + Tree tree{{Node::Type::OpNot}}; + tree.add_child(unit (tokens, proc, warnings)); + return tree; + } + + if (token.type == Token::Type::Open) { + tokens.pop_front(); + auto tree = term_1 (tokens, proc, warnings); + if (tokens.empty()) + warnings.push_back({token.pos, "expected: ')'"}); + else { + const auto token2 = look_ahead(tokens); + if (token2.type == Token::Type::Close) + tokens.pop_front(); + else { + warnings.push_back( + {token2.pos, + std::string("expected: ')' but got ") + + token2.str}); + } + + } + return tree; + } + + return data (tokens, proc, warnings); +} + +static Mu::Tree factor_1 (Mu::Tokens& tokens, ProcPtr proc, + WarningVec& warnings); + +static Mu::Tree +factor_2 (Mu::Tokens& tokens, Node::Type& op, ProcPtr proc, + WarningVec& warnings) +{ + if (tokens.empty()) + return empty(); + + const auto token = look_ahead(tokens); + + switch (token.type) { + case Token::Type::And: { + tokens.pop_front(); + op = Node::Type::OpAnd; + } break; + + case Token::Type::Open: + case Token::Type::Data: + case Token::Type::Not: + op = Node::Type::OpAnd; // implicit AND + break; + + default: + return empty(); + } + + return factor_1 (tokens, proc, warnings); +} + +static Mu::Tree +factor_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings) +{ + Node::Type op { Node::Type::Invalid }; + + auto t = unit (tokens, proc, warnings); + auto a2 = factor_2 (tokens, op, proc, warnings); + + if (a2.empty()) + return t; + + Tree tree {{op}}; + tree.add_child(std::move(t)); + tree.add_child(std::move(a2)); + + return tree; +} + + +static Mu::Tree +term_2 (Mu::Tokens& tokens, Node::Type& op, ProcPtr proc, + WarningVec& warnings) +{ + if (tokens.empty()) + return empty(); + + const auto token = look_ahead (tokens); + + switch (token.type) { + case Token::Type::Or: + op = Node::Type::OpOr; + break; + case Token::Type::Xor: + op = Node::Type::OpXor; + break; + default: + if (token.type != Token::Type::Close) + warnings.push_back({token.pos, "expected OR|XOR"}); + return empty(); + } + + tokens.pop_front(); + + return term_1 (tokens, proc, warnings); +} + +static Mu::Tree +term_1 (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings) +{ + Node::Type op { Node::Type::Invalid }; + + auto t = factor_1 (tokens, proc, warnings); + auto o2 = term_2 (tokens, op, proc, warnings); + + if (o2.empty()) + return t; + else { + Tree tree {{op}}; + tree.add_child(std::move(t)); + tree.add_child(std::move(o2)); + return tree; + } +} + +static Mu::Tree +query (Mu::Tokens& tokens, ProcPtr proc, WarningVec& warnings) +{ + if (tokens.empty()) + return empty (); + else + return term_1 (tokens, proc, warnings); +} + +Mu::Tree +Mu::parse (const std::string& expr, WarningVec& warnings, ProcPtr proc) +{ + try { + auto tokens = tokenize (expr); + return query (tokens, proc, warnings); + + } catch (const std::runtime_error& ex) { + std::cerr << ex.what() << std::endl; + return empty(); + } +} diff --git a/lib/query/mu-parser.hh b/lib/query/mu-parser.hh new file mode 100644 index 0000000..0c2ffe9 --- /dev/null +++ b/lib/query/mu-parser.hh @@ -0,0 +1,89 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + + +#ifndef __PARSER_HH__ +#define __PARSER_HH__ + +#include <string> +#include <vector> +#include <memory> + +#include <query/mu-data.hh> +#include <query/mu-tree.hh> +#include <query/mu-proc-iface.hh> + +// A simple recursive-descent parser for queries. Follows the Xapian syntax, +// but better handles non-alphanum; also implements regexp + +namespace Mu { + +/** + * A parser warning + * + */ +struct Warning { + size_t pos{}; /**< pos in string */ + const std::string msg; /**< warning message */ + + /** + * operator== + * + * @param rhs right-hand side + * + * @return true if rhs is equal to this; false otherwise + */ + bool operator==(const Warning& rhs) const { + return pos == rhs.pos && msg == rhs.msg; + } +}; + + +/** + * operator<< + * + * @param os an output stream + * @param w a warning + * + * @return the updated output stream + */ +inline std::ostream& +operator<< (std::ostream& os, const Warning& w) +{ + os << w.pos << ":" << w.msg; + return os; +} + +/** + * Parse a query string + * + * @param query a query string + * @param warnings vec to receive warnings + * @param proc a Processor object + * + * @return a parse-tree + */ +using WarningVec=std::vector<Warning>; +using ProcPtr = const std::unique_ptr<ProcIface>&; +Tree parse (const std::string& query, WarningVec& warnings, + ProcPtr proc = std::make_unique<DummyProc>()); + +} // namespace Mu + +#endif /* __PARSER_HH__ */ diff --git a/lib/query/mu-proc-iface.hh b/lib/query/mu-proc-iface.hh new file mode 100644 index 0000000..b43ef41 --- /dev/null +++ b/lib/query/mu-proc-iface.hh @@ -0,0 +1,132 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ +#ifndef __PROC_IFACE_HH__ +#define __PROC_IFACE_HH__ + +#include <string> +#include <vector> +#include <tuple> +#include <regex> + +namespace Mu { + +struct ProcIface { + + virtual ~ProcIface() = default; + + /** + * Get the "shortcut"/internal fields for the the given fieldstr or empty if there is none + * + * @param fieldstr a fieldstr, e.g "subject" or "s" for the subject field + * + * @return a vector with "exploded" values, with a code and a fullname. E.g. "s" might map + * to [<"S","subject">], while "recip" could map to [<"to", "T">, <"cc", "C">, <"bcc", "B">] + */ + struct FieldInfo { + const std::string field; + const std::string prefix; + bool supports_phrase; + unsigned id; + }; + using FieldInfoVec = std::vector<FieldInfo>; + + virtual FieldInfoVec process_field (const std::string& field) const = 0; + + /** + * Process a value + * + * @param field a field name + * @param value a value + * + * @return the processed value + */ + virtual std::string process_value ( + const std::string& field, const std::string& value) const = 0; + + /** + * Is this a range field? + * + * @param field some field + * + * @return true if it is a range-field; false otherwise. + */ + virtual bool is_range_field (const std::string& field) const = 0; + + + /** + * Process a range field + * + * @param fieldstr a fieldstr, e.g "date" or "d" for the date field + * @param lower lower bound or empty + * @param upper upper bound or empty + * + * @return the processed range + */ + struct Range { + std::string lower; + std::string upper; + }; + virtual Range process_range (const std::string& field, const std::string& lower, + const std::string& upper) const = 0; + + /** + * + * + * @param field + * @param rx + * + * @return + */ + virtual std::vector<std::string> + process_regex (const std::string& field, const std::regex& rx) const = 0; + +}; // ProcIface + + +struct DummyProc: public ProcIface { // For testing + + std::vector<FieldInfo> + process_field (const std::string& field) const override { + return {{ field, "x", false, 0 }}; + } + + std::string + process_value (const std::string& field, const std::string& value) const override { + return value; + } + + bool is_range_field (const std::string& field) const override { + return field == "range"; + } + + Range process_range (const std::string& field, const std::string& lower, + const std::string& upper) const override { + return { lower, upper }; + } + + std::vector<std::string> + process_regex (const std::string& field, const std::regex& rx) const override { + return {}; + } +}; //Dummy + + +} // Mu + +#endif /* __PROC_IFACE_HH__ */ diff --git a/lib/query/mu-tokenizer.cc b/lib/query/mu-tokenizer.cc new file mode 100644 index 0000000..f314d04 --- /dev/null +++ b/lib/query/mu-tokenizer.cc @@ -0,0 +1,133 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include "mu-tokenizer.hh" +#include "utils/mu-utils.hh" + +#include <cctype> +#include <iostream> +#include <algorithm> + +using namespace Mu; + +static bool +is_separator (char c) +{ + if (isblank(c)) + return true; + + const auto seps = std::string ("()"); + return seps.find(c) != std::string::npos; +} + + +static Mu::Token +op_or_value (size_t pos, const std::string& val) +{ + auto s = val; + std::transform(s.begin(), s.end(), s.begin(), ::tolower); + + if (s == "and") + return Token{pos, Token::Type::And, val}; + else if (s == "or") + return Token{pos, Token::Type::Or, val}; + else if (s == "xor") + return Token{pos, Token::Type::Xor, val}; + else if (s == "not") + return Token{pos, Token::Type::Not, val}; + else + return Token{pos, Token::Type::Data, val}; +} + +static void +unread_char (std::string& food, char kar, size_t& pos) +{ + food = kar + food; + --pos; +} + +static Mu::Token +eat_token (std::string& food, size_t& pos) +{ + bool quoted{}; + bool escaped{}; + std::string value {}; + + while (!food.empty()) { + + const auto kar = food[0]; + food.erase(0, 1); + ++pos; + + if (kar == '\\') { + escaped = !escaped; + if (escaped) + continue; + } + + if (kar == '"') { + if (!escaped && quoted) + return Token{pos, Token::Type::Data, value}; + else { + quoted = true; + continue; + } + } + + if (!quoted && !escaped && is_separator(kar)) { + + if (!value.empty() && kar != ':') { + unread_char (food, kar, pos); + return op_or_value(pos, value); + } + + if (quoted || isblank(kar)) + continue; + + switch (kar) { + case '(': return {pos, Token::Type::Open, "("}; + case ')': return {pos, Token::Type::Close,")"}; + default: break; + } + } + + value += kar; + escaped = false; + } + + return {pos, Token::Type::Data, value}; +} + + +Mu::Tokens +Mu::tokenize (const std::string& s) +{ + Tokens tokens{}; + + std::string food = utf8_clean(s); + size_t pos{0}; + + if (s.empty()) + return {}; + + while (!food.empty()) + tokens.emplace_back(eat_token (food, pos)); + + return tokens; +} diff --git a/lib/query/mu-tokenizer.hh b/lib/query/mu-tokenizer.hh new file mode 100644 index 0000000..ac083c8 --- /dev/null +++ b/lib/query/mu-tokenizer.hh @@ -0,0 +1,140 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __TOKENIZER_HH__ +#define __TOKENIZER_HH__ + +#include <string> +#include <vector> +#include <deque> +#include <ostream> +#include <stdexcept> + +// A simple tokenizer, which turns a string into a deque of tokens +// +// It recognizes '(', ')', '*' 'and', 'or', 'xor', 'not' +// +// Note that even if we recognizes those at the lexical level, they might be demoted to mere strings +// when we're creating the parse tree. +// +// Furthermore, we detect ranges ("a..b") and regexps (/../) at the parser level, since we need a +// bit more context to resolve ambiguities. + +namespace Mu { + +// A token +struct Token { + enum class Type { + Data, /**< e .g., banana or date:..456 */ + + // Brackets + Open, /**< ( */ + Close, /**< ) */ + + // Unops + Not, /**< logical not*/ + + // Binops + And, /**< logical and */ + Or, /**< logical not */ + Xor, /**< logical xor */ + + Empty, /**< nothing */ + }; + + size_t pos{}; /**< position in string */ + Type type{}; /**< token type */ + const std::string str{}; /**< data for this token */ + + /** + * operator== + * + * @param rhs right-hand side + * + * @return true if rhs is equal to this; false otherwise + */ + bool operator==(const Token& rhs) const { + return pos == rhs.pos && + type == rhs.type && + str == rhs.str; + } +}; + +/** + * operator<< + * + * @param os an output stream + * @param t a token type + * + * @return the updated output stream + */ +inline std::ostream& +operator<< (std::ostream& os, Token::Type t) +{ + switch (t) { + case Token::Type::Data: os << "<data>"; break; + + case Token::Type::Open: os << "<open>"; break; + case Token::Type::Close: os << "<close>";break; + + case Token::Type::Not: os << "<not>"; break; + case Token::Type::And: os << "<and>"; break; + case Token::Type::Or: os << "<or>"; break; + case Token::Type::Xor: os << "<xor>"; break; + + default: // can't happen, but pacify compiler + throw std::runtime_error ("<<bug>>"); + } + + return os; +} + +/** + * operator<< + * + * @param os an output stream + * @param t a token + * + * @return the updated output stream + */ +inline std::ostream& +operator<< (std::ostream& os, const Token& t) +{ + os << t.pos << ": " << t.type; + + if (!t.str.empty()) + os << " [" << t.str << "]"; + + return os; +} + +/** + * Tokenize a string into a vector of tokens. The tokenization always succeeds, ie., ignoring errors + * such a missing end-". + * + * @param s a string + * + * @return a deque of tokens + */ +using Tokens = std::deque<Token>; +Tokens tokenize (const std::string& s); + +} // namespace Mu + +#endif /* __TOKENIZER_HH__ */ diff --git a/lib/query/mu-tree.hh b/lib/query/mu-tree.hh new file mode 100644 index 0000000..eacda98 --- /dev/null +++ b/lib/query/mu-tree.hh @@ -0,0 +1,111 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef TREE_HH__ +#define TREE_HH__ + +#include <vector> +#include <string> +#include <iostream> + +#include <query/mu-data.hh> +#include <utils/mu-error.hh> + +namespace Mu { + +// A node in the parse tree +struct Node { + enum class Type { + Empty, // only for empty trees + OpAnd, + OpOr, + OpXor, + OpAndNot, + OpNot, + Value, + Range, + Invalid + }; + + Node(Type _type, std::unique_ptr<Data>&& _data): + type{_type}, data{std::move(_data)} {} + Node(Type _type): type{_type} {} + Node(Node&& rhs) = default; + + Type type; + std::unique_ptr<Data> data; + + static const char* type_name (Type t) { + switch (t) { + case Type::Empty: return ""; break; + case Type::OpAnd: return "and"; break; + case Type::OpOr: return "or"; break; + case Type::OpXor: return "xor"; break; + case Type::OpAndNot: return "andnot"; break; + case Type::OpNot: return "not"; break; + case Type::Value: return "value"; break; + case Type::Range: return "range"; break; + case Type::Invalid: return "<invalid>"; break; + default: + throw Mu::Error(Error::Code::Internal, "unexpected type"); + } + } + + static constexpr bool is_binop(Type t) { + return t == Type::OpAnd || t == Type::OpAndNot || + t == Type::OpOr || t == Type::OpXor; + } +}; + +inline std::ostream& +operator<< (std::ostream& os, const Node& t) +{ + os << Node::type_name(t.type); + if (t.data) + os << t.data; + + return os; +} + +struct Tree { + Tree(Node&& _node): node(std::move(_node)) {} + Tree(Tree&& rhs) = default; + + void add_child (Tree&& child) { children.emplace_back(std::move(child)); } + bool empty() const { return node.type == Node::Type::Empty; } + + Node node; + std::vector<Tree> children; +}; + +inline std::ostream& +operator<< (std::ostream& os, const Tree& tree) +{ + os << '(' << tree.node; + for (const auto& subtree : tree.children) + os << subtree; + os << ')'; + + return os; +} + +} // namespace Mu + + +#endif /* TREE_HH__ */ diff --git a/lib/query/mu-xapian.cc b/lib/query/mu-xapian.cc new file mode 100644 index 0000000..5112594 --- /dev/null +++ b/lib/query/mu-xapian.cc @@ -0,0 +1,116 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif /*HAVE_CONFIG_H*/ + +#include <xapian.h> +#include "mu-xapian.hh" +#include <utils/mu-error.hh> + +using namespace Mu; + +static Xapian::Query +xapian_query_op (const Mu::Tree& tree) +{ + Xapian::Query::op op; + + switch (tree.node.type) { + case Node::Type::OpNot: // OpNot x ::= <all> AND NOT x + if (tree.children.size() != 1) + throw std::runtime_error ("invalid # of children"); + return Xapian::Query (Xapian::Query::OP_AND_NOT, + Xapian::Query::MatchAll, + xapian_query(tree.children.front())); + case Node::Type::OpAnd: op = Xapian::Query::OP_AND; break; + case Node::Type::OpOr: op = Xapian::Query::OP_OR; break; + case Node::Type::OpXor: op = Xapian::Query::OP_XOR; break; + case Node::Type::OpAndNot: op = Xapian::Query::OP_AND_NOT; break; + default: throw Mu::Error (Error::Code::Internal, "invalid op"); // bug + } + + std::vector<Xapian::Query> childvec; + for (const auto& subtree: tree.children) + childvec.emplace_back(xapian_query(subtree)); + + return Xapian::Query(op, childvec.begin(), childvec.end()); +} + +static Xapian::Query +make_query (const Value* val, const std::string& str, bool maybe_wildcard) +{ + const auto vlen{str.length()}; + if (!maybe_wildcard || vlen <= 1 || str[vlen - 1] != '*') + return Xapian::Query(val->prefix + str); + else + return Xapian::Query(Xapian::Query::OP_WILDCARD, + val->prefix + str.substr(0, vlen - 1)); +} + +static Xapian::Query +xapian_query_value (const Mu::Tree& tree) +{ + const auto v = dynamic_cast<Value*> (tree.node.data.get()); + if (!v->phrase) + return make_query(v, v->value, true/*maybe-wildcard*/); + + const auto parts = split (v->value, " "); + if (parts.empty()) + return Xapian::Query::MatchNothing; // shouldn't happen + + if (parts.size() == 1) + return make_query(v, parts.front(), true/*maybe-wildcard*/); + + std::vector<Xapian::Query> phvec; + for (const auto p: parts) + phvec.emplace_back(make_query(v, p, false/*no wildcards*/)); + + return Xapian::Query (Xapian::Query::OP_PHRASE, phvec.begin(), phvec.end()); +} + +static Xapian::Query +xapian_query_range (const Mu::Tree& tree) +{ + const auto r { dynamic_cast<Range *>(tree.node.data.get()) }; + + return Xapian::Query(Xapian::Query::OP_VALUE_RANGE, (Xapian::valueno)r->id, + r->lower, r->upper); +} + +Xapian::Query +Mu::xapian_query (const Mu::Tree& tree) +{ + switch (tree.node.type) { + case Node::Type::Empty: + return Xapian::Query(); + case Node::Type::OpNot: + case Node::Type::OpAnd: + case Node::Type::OpOr: + case Node::Type::OpXor: + case Node::Type::OpAndNot: + return xapian_query_op (tree); + case Node::Type::Value: + return xapian_query_value (tree); + case Node::Type::Range: + return xapian_query_range (tree); + default: + throw Mu::Error (Error::Code::Internal, "invalid query"); // bug + } +} diff --git a/lib/query/mu-xapian.hh b/lib/query/mu-xapian.hh new file mode 100644 index 0000000..503d9eb --- /dev/null +++ b/lib/query/mu-xapian.hh @@ -0,0 +1,40 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + + +#ifndef __XAPIAN_HH__ +#define __XAPIAN_HH__ + +#include <xapian.h> +#include <query/mu-parser.hh> + +namespace Mu { + +/** + * Transform a parse-tree into a Xapian query object + * + * @param tree a parse tree + * + * @return a Xapian query object + */ +Xapian::Query xapian_query (const Mu::Tree& tree); + +} // namespace Mu + +#endif /* __XAPIAN_H__ */ diff --git a/lib/query/parse.cc b/lib/query/parse.cc new file mode 100644 index 0000000..d988d6e --- /dev/null +++ b/lib/query/parse.cc @@ -0,0 +1,41 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <string> +#include <iostream> +#include "mu-parser.hh" + +int +main (int argc, char *argv[]) +{ + std::string s; + + for (auto i = 1; i < argc; ++i) + s += " " + std::string(argv[i]); + + Mu::WarningVec warnings; + + const auto tree = Mu::parse (s, warnings); + for (const auto& w: warnings) + std::cerr << "1:" << w.pos << ": " << w.msg << std::endl; + + std::cout << tree << std::endl; + + return 0; +} diff --git a/lib/query/test-parser.cc b/lib/query/test-parser.cc new file mode 100644 index 0000000..2eadda9 --- /dev/null +++ b/lib/query/test-parser.cc @@ -0,0 +1,146 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <vector> +#include <glib.h> + +#include <iostream> +#include <sstream> + +#include "mu-parser.hh" +using namespace Mu; + +struct Case { + const std::string expr; + const std::string expected; + WarningVec warnings; +}; + +using CaseVec = std::vector<Case>; + +static void +test_cases(const CaseVec& cases) +{ + for (const auto& casus : cases ) { + + WarningVec warnings; + const auto tree = parse (casus.expr, warnings); + + std::stringstream ss; + ss << tree; + + if (g_test_verbose()) { + std::cout << "\n"; + std::cout << casus.expr << std::endl; + std::cout << "exp:" << casus.expected << std::endl; + std::cout << "got:" << ss.str() << std::endl; + } + g_assert_true (casus.expected == ss.str()); + + // g_assert_cmpuint (casus.warnings.size(), ==, warnings.size()); + // for (auto i = 0; i != (int)casus.warnings.size(); ++i) { + // std::cout << "exp:" << casus.warnings[i] << std::endl; + // std::cout << "got:" << warnings[i] << std::endl; + // g_assert_true (casus.warnings[i] == warnings[i]); + // } + } +} + +static void +test_basic () +{ + CaseVec cases = { + //{ "", R"#((atom :value ""))#"}, + { "foo", R"#((value "" "foo"))#", }, + { "foo or bar", + R"#((or(value "" "foo")(value "" "bar")))#" }, + { "foo and bar", + R"#((and(value "" "foo")(value "" "bar")))#"}, + }; + + test_cases (cases); +} + +static void +test_complex () +{ + CaseVec cases = { + { "foo and bar or cuux", + R"#((or(and(value "" "foo")(value "" "bar")))#" + + std::string(R"#((value "" "cuux")))#") }, + + { "a and not b", + R"#((and(value "" "a")(not(value "" "b"))))#" + }, + { "a and b and c", + R"#((and(value "" "a")(and(value "" "b")(value "" "c"))))#" + }, + { "(a or b) and c", + R"#((and(or(value "" "a")(value "" "b"))(value "" "c")))#" + }, + { "a b", // implicit and + R"#((and(value "" "a")(value "" "b")))#" + }, + { "a not b", // implicit and not + R"#((and(value "" "a")(not(value "" "b"))))#" + }, + { "not b", // implicit and not + R"#((not(value "" "b")))#" + } + }; + + test_cases (cases); +} + + +static void +test_range () +{ + CaseVec cases = { + { "range:a..b", // implicit and + R"#((range "range" "a" "b"))#" + }, + }; + + test_cases (cases); +} + + +static void +test_flatten () +{ + CaseVec cases = { + { " Mötørhęåđ", R"#((value "" "motorhead"))#" } + }; + + test_cases (cases); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/parser/basic", test_basic); + g_test_add_func ("/parser/complex", test_complex); + g_test_add_func ("/parser/range", test_range); + g_test_add_func ("/parser/flatten", test_flatten); + + return g_test_run (); +} diff --git a/lib/query/test-tokenizer.cc b/lib/query/test-tokenizer.cc new file mode 100644 index 0000000..62fa7ef --- /dev/null +++ b/lib/query/test-tokenizer.cc @@ -0,0 +1,158 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <vector> +#include <glib.h> +#include <iostream> +#include <sstream> + +#include "mu-tokenizer.hh" + +struct Case { + const char *str; + const Mu::Tokens tokens; +}; + +using CaseVec = std::vector<Case>; + +using namespace Mu; +using TT = Token::Type; + +static void +test_cases(const CaseVec& cases) +{ + for (const auto& casus : cases ) { + const auto tokens = tokenize (casus.str); + + g_assert_cmpuint ((guint)tokens.size(),==,(guint)casus.tokens.size()); + for (size_t u = 0; u != tokens.size(); ++u) { + if (g_test_verbose()) { + std::cerr << "case " << u << " " << casus.str << std::endl; + std::cerr << "exp: '" << casus.tokens[u] << "'" << std::endl; + std::cerr << "got: '" << tokens[u] << "'" << std::endl; + + } + g_assert_true (tokens[u] == casus.tokens[u]); + } + } +} + +static void +test_basic () +{ + CaseVec cases = { + { "", {} }, + + { "foo", Tokens{Token{3, TT::Data, "foo"}}}, + + { "foo bar cuux", Tokens{Token{3, TT::Data, "foo"}, + Token{7, TT::Data, "bar"}, + Token{12, TT::Data, "cuux"}}}, + + { "\"foo bar\"", Tokens{ Token{9, TT::Data, "foo bar"}}}, + + // ie. ignore missing closing '"' + { "\"foo bar", Tokens{ Token{8, TT::Data, "foo bar"}}}, + + }; + + test_cases (cases); +} + +static void +test_specials () +{ + CaseVec cases = { + { ")*(", Tokens{Token{1, TT::Close, ")"}, + Token{2, TT::Data, "*"}, + Token{3, TT::Open, "("}}}, + { "\")*(\"", Tokens{Token{5, TT::Data, ")*("}}}, + }; + + test_cases (cases); +} + + +static void +test_ops () +{ + CaseVec cases = { + { "foo and bar oR cuux XoR fnorb", + Tokens{Token{3, TT::Data, "foo"}, + Token{7, TT::And, "and"}, + Token{11, TT::Data, "bar"}, + Token{14, TT::Or, "oR"}, + Token{19, TT::Data, "cuux"}, + Token{23, TT::Xor, "XoR"}, + Token{29, TT::Data, "fnorb"}}}, + { "NOT (aap or mies)", + Tokens{Token{3, TT::Not, "NOT"}, + Token{5, TT::Open, "("}, + Token{8, TT::Data, "aap"}, + Token{11, TT::Or, "or"}, + Token{16, TT::Data, "mies"}, + Token{17, TT::Close, ")"}}} + }; + + + test_cases (cases); +} + + +static void +test_escape () +{ + CaseVec cases = { + { "foo\"bar\"", Tokens{Token{8, TT::Data, "foobar"}}}, + { "\"fnorb\"", Tokens{Token{7, TT::Data, "fnorb"}}}, + { "\\\"fnorb\\\"", Tokens{Token{9, TT::Data, "fnorb"}}}, + { "foo\\\"bar\\\"", Tokens{Token{10, TT::Data, "foobar"}}} + }; + + test_cases (cases); +} + + +static void +test_to_string () +{ + std::stringstream ss; + for (const auto t: tokenize ("foo and bar xor not cuux or fnorb")) + ss << t << ' '; + + g_assert_true (ss.str() == + "3: <data> [foo] 7: <and> [and] 11: <data> [bar] " + "15: <xor> [xor] 19: <not> [not] 24: <data> [cuux] " + "27: <or> [or] 33: <data> [fnorb] "); +} + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/tokens/basic", test_basic); + g_test_add_func ("/tokens/specials", test_specials); + g_test_add_func ("/tokens/ops", test_ops); + g_test_add_func ("/tokens/escape", test_escape); + g_test_add_func ("/tokens/to-string", test_to_string); + + return g_test_run (); +} diff --git a/lib/query/tokenize.cc b/lib/query/tokenize.cc new file mode 100644 index 0000000..81f6ef0 --- /dev/null +++ b/lib/query/tokenize.cc @@ -0,0 +1,38 @@ +/* +** Copyright (C) 2017-2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include <string> +#include <iostream> + +#include "mu-tokenizer.hh" + +int +main (int argc, char *argv[]) +{ + std::string s; + + for (auto i = 1; i < argc; ++i) + s += " " + std::string(argv[i]); + + const auto tvec = Mu::tokenize (s); + for (const auto& t : tvec) + std::cout << t << std::endl; + + return 0; +} diff --git a/lib/test-mu-common.c b/lib/test-mu-common.c new file mode 100644 index 0000000..5cabb76 --- /dev/null +++ b/lib/test-mu-common.c @@ -0,0 +1,93 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <glib/gstdio.h> + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +#include <langinfo.h> +#include <locale.h> + +#include "test-mu-common.h" + +char* +test_mu_common_get_random_tmpdir (void) +{ + char*dir; + int res; + + dir = g_strdup_printf ( + "%s%cmu-test-%d%ctest-%x", + g_get_tmp_dir(), + G_DIR_SEPARATOR, + getuid(), + G_DIR_SEPARATOR, + (int)random()*getpid()*(int)time(NULL)); + + res = g_mkdir_with_parents (dir, 0700); + g_assert (res != -1); + + return dir; +} + + +const char* +set_tz (const char* tz) +{ + static const char* oldtz; + + oldtz = getenv ("TZ"); + if (tz) + setenv ("TZ", tz, 1); + else + unsetenv ("TZ"); + + tzset (); + return oldtz; +} + + +gboolean +set_en_us_utf8_locale (void) +{ + setenv ("LC_ALL", "en_US.UTF-8", 1); + setlocale (LC_ALL, "en_US.UTF-8"); + + if (strcmp (nl_langinfo(CODESET), "UTF-8") != 0) { + g_print ("Note: Unit tests require the en_US.utf8 locale. " + "Ignoring test cases.\n"); + return FALSE; + } + + return TRUE; +} + + +void +black_hole (void) +{ + return; /* do nothing */ +} diff --git a/lib/test-mu-common.h b/lib/test-mu-common.h new file mode 100644 index 0000000..31f4d3a --- /dev/null +++ b/lib/test-mu-common.h @@ -0,0 +1,61 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __TEST_MU_COMMON_H__ +#define __TEST_MU_COMMON_H__ + +#include <glib.h> + +G_BEGIN_DECLS + +/** + * get a dir name for a random temporary directory to do tests + * + * @return a random dir name, g_free when it's no longer needed + */ +char* test_mu_common_get_random_tmpdir (void); + + + +/** + * set the output to /dev/null + * + */ +void black_hole (void); + +/** + * set the timezone + * + * @param tz timezone + * + * @return the old timezone + */ +const char* set_tz (const char* tz); + + +/** + * switch the locale to en_US.utf8, return TRUE if it succeeds + * + * @return TRUE if the switch succeeds, FALSE otherwise + */ +gboolean set_en_us_utf8_locale (void); + +G_END_DECLS + +#endif /*__TEST_MU_COMMON_H__*/ diff --git a/lib/test-mu-contacts.cc b/lib/test-mu-contacts.cc new file mode 100644 index 0000000..141f8b9 --- /dev/null +++ b/lib/test-mu-contacts.cc @@ -0,0 +1,91 @@ +/* +** Copyright (C) 2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "config.h" + +#include <glib.h> +#include "test-mu-common.h" +#include "mu-contacts.hh" + +static void +test_mu_contacts_01() +{ + Mu::Contacts contacts (""); + + g_assert_true (contacts.empty()); + g_assert_cmpuint (contacts.size(), ==, 0); + + contacts.add(std::move(Mu::ContactInfo ("Foo <foo.bar@example.com>", + "foo.bar@example.com", "Foo", + false, 12345))); + g_assert_false (contacts.empty()); + g_assert_cmpuint (contacts.size(), ==, 1); + + contacts.add(std::move(Mu::ContactInfo ("Cuux <cuux.fnorb@example.com>", + "cuux@example.com", "Cuux", true, + 54321))); + + g_assert_cmpuint (contacts.size(), ==, 2); + + contacts.add(std::move(Mu::ContactInfo ("foo.bar@example.com", + "foo.bar@example.com", "Foo", + false, 77777))); + g_assert_cmpuint (contacts.size(), ==, 2); + + contacts.add(std::move(Mu::ContactInfo ("Foo.Bar@Example.Com", + "Foo.Bar@Example.Com", "Foo", + false, 88888))); + g_assert_cmpuint (contacts.size(), ==, 2); + // note: replaces first. + + { + const auto info = contacts._find("bla@example.com"); + g_assert_false (info); + } + + + { + const auto info = contacts._find("foo.BAR@example.com"); + g_assert_true (info); + + g_assert_cmpstr(info->email.c_str(), ==, "Foo.Bar@Example.Com"); + } + + contacts.clear(); + g_assert_true (contacts.empty()); + g_assert_cmpuint (contacts.size(), ==, 0); +} + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/mu-contacts/01", test_mu_contacts_01); + + g_log_set_handler (NULL, + (GLogLevelFlags) + (G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION), + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/test-mu-container.c b/lib/test-mu-container.c new file mode 100644 index 0000000..475e302 --- /dev/null +++ b/lib/test-mu-container.c @@ -0,0 +1,83 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2014 Jakub Sitnicki <jsitnicki@gmail.com> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> + +#include "test-mu-common.h" +#include "mu-container.h" + +static gboolean +container_has_children (const MuContainer *c) +{ + return c && c->child; +} + +static gboolean +container_is_sibling_of (const MuContainer *c, const MuContainer *sibling) +{ + const MuContainer *cur; + + for (cur = c; cur; cur = cur->next) { + if (cur == sibling) + return TRUE; + } + + return container_is_sibling_of (sibling, c); +} + +static void +test_mu_container_splice_children_when_parent_has_no_siblings (void) +{ + MuContainer *child, *parent, *root_set; + + child = mu_container_new (NULL, 0, "child"); + parent = mu_container_new (NULL, 0, "parent"); + parent = mu_container_append_children (parent, child); + + root_set = parent; + root_set = mu_container_splice_children (root_set, parent); + + g_assert (root_set != NULL); + g_assert (!container_has_children(parent)); + g_assert (container_is_sibling_of (root_set, child)); + + mu_container_destroy(parent); + mu_container_destroy(child); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/mu-container/mu-container-splice-children-when-parent-has-no-siblings", + test_mu_container_splice_children_when_parent_has_no_siblings); + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/test-mu-date.c b/lib/test-mu-date.c new file mode 100644 index 0000000..5cb6d5f --- /dev/null +++ b/lib/test-mu-date.c @@ -0,0 +1,93 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> + +#include <locale.h> + +#include "test-mu-common.h" +#include "mu-date.h" + + + + + +static void +test_mu_date_interpret_begin (void) +{ + time_t now; + now = time (NULL); + + g_assert_cmpstr (mu_date_interpret_s ("now", TRUE) , ==, + mu_date_str_s("%Y%m%d%H%M%S", now)); + + g_assert_cmpstr (mu_date_interpret_s ("today", TRUE) , ==, + mu_date_str_s("%Y%m%d000000", now)); +} + +static void +test_mu_date_interpret_end (void) +{ + time_t now; + now = time (NULL); + + g_assert_cmpstr (mu_date_interpret_s ("now", FALSE) , ==, + mu_date_str_s("%Y%m%d%H%M%S", now)); + + g_assert_cmpstr (mu_date_interpret_s ("today", FALSE) , ==, + mu_date_str_s("%Y%m%d235959", now)); +} + + + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/mu-str/mu_date_parse_hdwmy", + test_mu_date_parse_hdwmy); + g_test_add_func ("/mu-str/mu_date_complete_begin", + test_mu_date_complete_begin); + g_test_add_func ("/mu-str/mu_date_complete_end", + test_mu_date_complete_end); + + g_test_add_func ("/mu-str/mu_date_interpret_begin", + test_mu_date_interpret_begin); + g_test_add_func ("/mu-str/mu_date_interpret_end", + test_mu_date_interpret_end); + + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/test-mu-flags.c b/lib/test-mu-flags.c new file mode 100644 index 0000000..36db9c8 --- /dev/null +++ b/lib/test-mu-flags.c @@ -0,0 +1,193 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include "mu-flags.h" +#include "test-mu-common.h" + + +static void +test_mu_flag_char (void) +{ + g_assert_cmpuint (mu_flag_char (MU_FLAG_DRAFT), ==, 'D'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_FLAGGED), ==, 'F'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_PASSED), ==, 'P'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_REPLIED), ==, 'R'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_SEEN), ==, 'S'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_TRASHED), ==, 'T'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_NEW), ==, 'N'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_SIGNED), ==, 'z'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_ENCRYPTED), ==, 'x'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_HAS_ATTACH), ==, 'a'); + g_assert_cmpuint (mu_flag_char (MU_FLAG_UNREAD), ==, 'u'); + g_assert_cmpuint (mu_flag_char (12345), ==, 0); +} + + + +static void +test_mu_flag_name (void) +{ + g_assert_cmpstr (mu_flag_name (MU_FLAG_DRAFT), ==, "draft"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_FLAGGED), ==, "flagged"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_PASSED), ==, "passed"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_REPLIED), ==, "replied"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_SEEN), ==, "seen"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_TRASHED), ==, "trashed"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_NEW), ==, "new"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_SIGNED), ==, "signed"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_ENCRYPTED), ==, "encrypted"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_HAS_ATTACH), ==, "attach"); + g_assert_cmpstr (mu_flag_name (MU_FLAG_UNREAD), ==, "unread"); + g_assert_cmpstr (mu_flag_name (12345), ==, NULL); +} + +static void +test_mu_flags_to_str_s (void) +{ + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_PASSED|MU_FLAG_SIGNED, + MU_FLAG_TYPE_ANY), + ==, "Pz"); + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NEW, MU_FLAG_TYPE_ANY), + ==, "N"); + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_HAS_ATTACH|MU_FLAG_TRASHED, + MU_FLAG_TYPE_ANY), + ==, "Ta"); + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NONE, MU_FLAG_TYPE_ANY), + ==, ""); + + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_PASSED|MU_FLAG_SIGNED, + MU_FLAG_TYPE_CONTENT), + ==, "z"); + + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NEW, MU_FLAG_TYPE_MAILDIR), + ==, "N"); + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_HAS_ATTACH|MU_FLAG_TRASHED, + MU_FLAG_TYPE_MAILFILE), + ==, "T"); + + g_assert_cmpstr (mu_flags_to_str_s(MU_FLAG_NONE, MU_FLAG_TYPE_PSEUDO), + ==, ""); +} + + +static void +test_mu_flags_from_str (void) +{ + /* note, the 3rd arg to mu_flags_from_str determines whether + * invalid flags will be ignored (if TRUE) or MU_FLAG_INVALID (if FALSE) + */ + + g_assert_cmpuint (mu_flags_from_str ("RP", MU_FLAG_TYPE_ANY, TRUE), ==, + MU_FLAG_REPLIED | MU_FLAG_PASSED); + g_assert_cmpuint (mu_flags_from_str ("Nz", MU_FLAG_TYPE_ANY, TRUE), ==, + MU_FLAG_NEW | MU_FLAG_SIGNED); + g_assert_cmpuint (mu_flags_from_str ("axD", MU_FLAG_TYPE_ANY, TRUE), ==, + MU_FLAG_HAS_ATTACH | MU_FLAG_ENCRYPTED | MU_FLAG_DRAFT); + + g_assert_cmpuint (mu_flags_from_str ("RP", MU_FLAG_TYPE_MAILFILE, TRUE), ==, + MU_FLAG_REPLIED | MU_FLAG_PASSED); + g_assert_cmpuint (mu_flags_from_str ("Nz", MU_FLAG_TYPE_MAILFILE, TRUE), ==, + MU_FLAG_NONE); + + /* ignore errors or not */ + g_assert_cmpuint (mu_flags_from_str ("qwi", MU_FLAG_TYPE_MAILFILE, FALSE), ==, + MU_FLAG_INVALID); + g_assert_cmpuint (mu_flags_from_str ("qwi", MU_FLAG_TYPE_MAILFILE, TRUE), ==, + 0); + + +} + +static void +test_mu_flags_from_str_delta (void) +{ + g_assert_cmpuint (mu_flags_from_str_delta ("+S-R", + MU_FLAG_REPLIED | MU_FLAG_DRAFT, + MU_FLAG_TYPE_ANY),==, + MU_FLAG_SEEN | MU_FLAG_DRAFT); + + g_assert_cmpuint (mu_flags_from_str_delta ("", + MU_FLAG_REPLIED | MU_FLAG_DRAFT, + MU_FLAG_TYPE_ANY),==, + MU_FLAG_REPLIED | MU_FLAG_DRAFT); + + g_assert_cmpuint (mu_flags_from_str_delta ("-N+P+S-D", + MU_FLAG_SIGNED | MU_FLAG_DRAFT, + MU_FLAG_TYPE_ANY),==, + MU_FLAG_PASSED | MU_FLAG_SEEN | MU_FLAG_SIGNED); +} + + +static void +test_mu_flags_custom_from_str (void) +{ + unsigned u; + + struct { + const char *str; + const char *expected; + } cases[] = { + { "ABC", "ABC" }, + { "PAF", "A" }, + { "ShelloPwoFrDldR123", "helloworld123" }, + { "SPD", NULL } + }; + + for (u = 0; u != G_N_ELEMENTS(cases); ++u) { + char *cust; + cust = mu_flags_custom_from_str (cases[u].str); + if (g_test_verbose()) + g_print ("%s: str:%s; got:%s; expected:%s\n", + __func__, cases[u].str, cust, cases[u].expected); + g_assert_cmpstr (cust, ==, cases[u].expected); + g_free (cust); + } +} + + + +int +main (int argc, char *argv[]) +{ + int rv; + g_test_init (&argc, &argv, NULL); + + /* mu_msg_str_date */ + g_test_add_func ("/mu-flags/test-mu-flag-char", test_mu_flag_char); + g_test_add_func ("/mu-flags/test-mu-flag-name",test_mu_flag_name); + g_test_add_func ("/mu-flags/test-mu-flags-to-str-s",test_mu_flags_to_str_s); + g_test_add_func ("/mu-flags/test-mu-flags-from-str",test_mu_flags_from_str); + g_test_add_func ("/mu-flags/test-mu-flags-from-str-delta",test_mu_flags_from_str_delta ); + g_test_add_func ("/mu-flags/test-mu-flags-custom-from-str", + test_mu_flags_custom_from_str); + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + rv = g_test_run (); + + return rv; +} diff --git a/lib/test-mu-maildir.c b/lib/test-mu-maildir.c new file mode 100644 index 0000000..3952127 --- /dev/null +++ b/lib/test-mu-maildir.c @@ -0,0 +1,693 @@ +/* +** Copyright (C) 2008-2016 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <glib/gstdio.h> + +#include <stdlib.h> +#include <unistd.h> +#include <string.h> + +#include "test-mu-common.h" +#include "mu-maildir.h" +#include "utils/mu-util.h" + +static void +test_mu_maildir_mkdir_01 (void) +{ + int i; + gchar *tmpdir, *mdir, *tmp; + const gchar *subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir (); + mdir = g_strdup_printf ("%s%c%s", tmpdir, G_DIR_SEPARATOR, + "cuux"); + + g_assert_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL), + ==, TRUE); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, + subs[i]); + g_assert_cmpuint (g_access (dir, R_OK), ==, 0); + g_assert_cmpuint (g_access (dir, W_OK), ==, 0); + g_free (dir); + } + + tmp = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint (g_access (tmp, F_OK), !=, 0); + + g_free (tmp); + g_free (tmpdir); + g_free (mdir); + +} + + +static void +test_mu_maildir_mkdir_02 (void) +{ + int i; + gchar *tmpdir, *mdir, *tmp; + const gchar *subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir (); + mdir = g_strdup_printf ("%s%c%s", tmpdir, G_DIR_SEPARATOR, + "cuux"); + + g_assert_cmpuint (mu_maildir_mkdir (mdir, 0755, TRUE, NULL), + ==, TRUE); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, + subs[i]); + g_assert_cmpuint (g_access (dir, R_OK), ==, 0); + + g_assert_cmpuint (g_access (dir, W_OK), ==, 0); + g_free (dir); + } + + tmp = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint (g_access (tmp, F_OK), ==, 0); + + g_free (tmp); + g_free (tmpdir); + g_free (mdir); +} + + +static void +test_mu_maildir_mkdir_03 (void) +{ + int i; + gchar *tmpdir, *mdir, *tmp; + const gchar *subs[] = {"tmp", "cur", "new"}; + + tmpdir = test_mu_common_get_random_tmpdir (); + mdir = g_strdup_printf ("%s%c%s", tmpdir, G_DIR_SEPARATOR, + "cuux"); + + /* create part of the structure already... */ + { + gchar *dir; + dir = g_strdup_printf ("%s%ccur", mdir, G_DIR_SEPARATOR); + g_assert_cmpuint (g_mkdir_with_parents (dir, 0755), ==, 0); + g_free (dir); + } + + /* this should still work */ + g_assert_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL), + ==, TRUE); + + for (i = 0; i != G_N_ELEMENTS(subs); ++i) { + gchar* dir; + + dir = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, + subs[i]); + g_assert_cmpuint (g_access (dir, R_OK), ==, 0); + g_assert_cmpuint (g_access (dir, W_OK), ==, 0); + g_free (dir); + } + + tmp = g_strdup_printf ("%s%c%s", mdir, G_DIR_SEPARATOR, ".noindex"); + g_assert_cmpuint (g_access (tmp, F_OK), !=, 0); + + g_free (tmp); + g_free (tmpdir); + g_free (mdir); + +} + + + +static void +test_mu_maildir_mkdir_04 (void) +{ + gchar *tmpdir, *mdir; + + tmpdir = test_mu_common_get_random_tmpdir (); + mdir = g_strdup_printf ("%s%c%s", tmpdir, G_DIR_SEPARATOR, + "cuux"); + + /* create part of the structure already... */ + { + gchar *dir; + g_assert_cmpuint (g_mkdir_with_parents (mdir, 0755), ==, 0); + dir = g_strdup_printf ("%s%ccur", mdir, G_DIR_SEPARATOR); + g_assert_cmpuint (g_mkdir_with_parents (dir, 0000), ==, 0); + g_free (dir); + } + + /* this should fail now, because cur is not read/writable */ + g_assert_cmpuint (mu_maildir_mkdir (mdir, 0755, FALSE, NULL), + ==, (geteuid()==0 ? TRUE : FALSE)); + g_free (tmpdir); + g_free (mdir); +} + + + + +static gboolean +ignore_error (const char* log_domain, GLogLevelFlags log_level, const gchar* msg, + gpointer user_data) +{ + return FALSE; /* don't abort */ +} + +static void +test_mu_maildir_mkdir_05 (void) +{ + /* this must fail */ + g_test_log_set_fatal_handler ((GTestLogFatalFunc)ignore_error, NULL); + + g_assert_cmpuint (mu_maildir_mkdir (NULL, 0755, TRUE, NULL), + ==, FALSE); +} + + +static gchar* +copy_test_data (void) +{ + gchar *dir, *cmd; + + dir = test_mu_common_get_random_tmpdir(); + cmd = g_strdup_printf ("mkdir -m 0700 %s", dir); + if (g_test_verbose()) + g_print ("cmd: %s\n", cmd); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + + cmd = g_strdup_printf ("cp -R %s %s", MU_TESTMAILDIR, dir); + if (g_test_verbose()) + g_print ("cmd: %s\n", cmd); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + + return dir; +} + + +typedef struct { + int _file_count; + int _dir_entered; + int _dir_left; +} WalkData; + +static MuError +dir_cb (const char *fullpath, gboolean enter, WalkData *data) +{ + if (enter) + ++data->_dir_entered; + else + ++data->_dir_left; + + if (g_test_verbose()) + g_print ("%s: %s: %s (%u)\n", __func__, enter ? "entering" : "leaving", + fullpath, enter ? data->_dir_entered : data->_dir_left); + + return MU_OK; +} + + +static MuError +msg_cb (const char *fullpath, const char* mdir, struct stat *statinfo, + WalkData *data) +{ + ++data->_file_count; + return MU_OK; +} + + +static void +test_mu_maildir_walk_01 (void) +{ + char *tmpdir; + WalkData data; + MuError rv; + + tmpdir = copy_test_data (); + memset (&data, 0, sizeof(WalkData)); + + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + TRUE, + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 19); + + g_assert_cmpuint (data._dir_entered,==, 5); + g_assert_cmpuint (data._dir_left,==, 5); + + g_free (tmpdir); +} + + +static void +test_mu_maildir_walk (void) +{ + char *tmpdir, *cmd, *dir; + WalkData data; + MuError rv; + + tmpdir = copy_test_data (); + memset (&data, 0, sizeof(WalkData)); + + /* mark the 'new' dir with '.noindex', to ignore it */ + dir = g_strdup_printf ("%s%ctestdir%cnew", tmpdir, + G_DIR_SEPARATOR, G_DIR_SEPARATOR); + cmd = g_strdup_printf ("chmod 700 %s", dir); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + + cmd = g_strdup_printf ("touch %s%c.noindex", dir, G_DIR_SEPARATOR); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + g_free (dir); + + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + TRUE, + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 15); + + g_assert_cmpuint (data._dir_entered,==, 4); + g_assert_cmpuint (data._dir_left,==, 4); + + g_free (tmpdir); +} + +static void +test_mu_maildir_walk_with_noupdate (void) +{ + char *tmpdir, *cmd, *dir; + WalkData data; + MuError rv; + + tmpdir = copy_test_data (); + + /* mark the 'new' dir with '.noindex', to ignore it */ + dir = g_strdup_printf ("%s%ctestdir%cnew", tmpdir, + G_DIR_SEPARATOR, G_DIR_SEPARATOR); + cmd = g_strdup_printf ("chmod 700 %s", dir); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + + memset (&data, 0, sizeof(WalkData)); + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + FALSE, /* ie., non-full update */ + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 19); + g_assert_cmpuint (data._dir_entered,==, 5); + g_assert_cmpuint (data._dir_left,==, 5); + + /* again, full update. results should be the same, since there + * is no noupdate yet */ + memset (&data, 0, sizeof(WalkData)); + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + TRUE, /* ie., full update */ + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 19); + g_assert_cmpuint (data._dir_entered,==, 5); + g_assert_cmpuint (data._dir_left,==, 5); + + /* add a '.noupdate' file; this affects the outcome when the + * 4th arg to mu_maildir_walk is FALSE */ + cmd = g_strdup_printf ("touch %s%c.noupdate", dir, G_DIR_SEPARATOR); + g_assert (g_spawn_command_line_sync (cmd, NULL, NULL, NULL, NULL)); + g_free (cmd); + + memset (&data, 0, sizeof(WalkData)); + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + FALSE, /* non-full update */ + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 15); + + g_assert_cmpuint (data._dir_entered,==, 4); + g_assert_cmpuint (data._dir_left,==, 4); + + /* now run again, but do a full update */ + memset (&data, 0, sizeof(WalkData)); + rv = mu_maildir_walk (tmpdir, + (MuMaildirWalkMsgCallback)msg_cb, + (MuMaildirWalkDirCallback)dir_cb, + TRUE, /* full update */ + &data); + + g_assert_cmpuint (MU_OK, ==, rv); + g_assert_cmpuint (data._file_count, ==, 19); + + g_assert_cmpuint (data._dir_entered,==, 5); + g_assert_cmpuint (data._dir_left,==, 5); + + g_free (dir); + g_free (tmpdir); +} + + + + + + +static void +test_mu_maildir_get_flags_from_path (void) +{ + int i; + struct { + const char *path; + MuFlags flags; + } paths[] = { + { + "/home/foo/Maildir/test/cur/123456:2,FSR", + MU_FLAG_REPLIED | MU_FLAG_SEEN | MU_FLAG_FLAGGED + }, + { + "/home/foo/Maildir/test/new/123456", + MU_FLAG_NEW + }, + { + /* NOTE: when in new/, the :2,.. stuff is ignored */ + "/home/foo/Maildir/test/new/123456:2,FR", + MU_FLAG_NEW + }, + { + "/home/foo/Maildir/test/cur/123456:2,DTP", + MU_FLAG_DRAFT | MU_FLAG_TRASHED | + MU_FLAG_PASSED + }, + { + "/home/foo/Maildir/test/cur/123456:2,S", + MU_FLAG_SEEN + } + }; + + for (i = 0; i != G_N_ELEMENTS(paths); ++i) { + MuFlags flags; + flags = mu_maildir_get_flags_from_path(paths[i].path); + g_assert_cmpuint(flags, ==, paths[i].flags); + } +} + + + +static void +assert_matches_regexp (const char *str, const char *rx) +{ + if (!g_regex_match_simple (rx, str, 0, 0)) { + if (g_test_verbose ()) + g_print ("%s does not match %s", str, rx); + g_assert (0); + } +} + + + +static void +test_mu_maildir_get_new_path_new (void) +{ + int i; + + struct { + const char *oldpath; + MuFlags flags; + const char *newpath; + } paths[] = { + { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_REPLIED, + "/home/foo/Maildir/test/cur/123456:2,R" + }, { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_NEW, + "/home/foo/Maildir/test/new/123456" + }, { + "/home/foo/Maildir/test/new/123456:2,FR", + MU_FLAG_SEEN | MU_FLAG_REPLIED, + "/home/foo/Maildir/test/cur/123456:2,RS" + }, { + "/home/foo/Maildir/test/new/1313038887_0.697:2,", + MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED, + "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS" + }, { + "/home/djcb/Maildir/trash/new/1312920597.2206_16.cthulhu", + MU_FLAG_SEEN, + "/home/djcb/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S" + } + }; + + for (i = 0; i != G_N_ELEMENTS(paths); ++i) { + char *str, *newbase; + str = mu_maildir_get_new_path (paths[i].oldpath, NULL, + paths[i].flags, TRUE); + newbase = g_path_get_basename (str); + assert_matches_regexp (newbase, + "\\d+\\." + "[[:xdigit:]]{16}\\." + "[[:alnum:]][[:alnum:]-]+(:2,.*)?"); + g_free (newbase); + g_free(str); + } +} + + + + +static void +test_mu_maildir_get_new_path_01 (void) +{ + int i; + + struct { + const char *oldpath; + MuFlags flags; + const char *newpath; + } paths[] = { + { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_REPLIED, + "/home/foo/Maildir/test/cur/123456:2,R" + }, { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_NEW, + "/home/foo/Maildir/test/new/123456" + }, { + "/home/foo/Maildir/test/new/123456:2,FR", + MU_FLAG_SEEN | MU_FLAG_REPLIED, + "/home/foo/Maildir/test/cur/123456:2,RS" + }, { + "/home/foo/Maildir/test/new/1313038887_0.697:2,", + MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED, + "/home/foo/Maildir/test/cur/1313038887_0.697:2,FPS" + }, { + "/home/djcb/Maildir/trash/new/1312920597.2206_16.cthulhu", + MU_FLAG_SEEN, + "/home/djcb/Maildir/trash/cur/1312920597.2206_16.cthulhu:2,S" + } + }; + + for (i = 0; i != G_N_ELEMENTS(paths); ++i) { + gchar *str; + str = mu_maildir_get_new_path(paths[i].oldpath, NULL, + paths[i].flags, FALSE); + g_assert_cmpstr(str, ==, paths[i].newpath); + g_free(str); + } +} + + +static void +test_mu_maildir_get_new_path_02 (void) +{ + int i; + + struct { + const char *oldpath; + MuFlags flags; + const char *targetdir; + const char *newpath; + } paths[] = { + { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_REPLIED, "/home/foo/Maildir/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,R" + }, { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_NEW, "/home/bar/Maildir/coffee", + "/home/bar/Maildir/coffee/new/123456" + }, { + "/home/foo/Maildir/test/new/123456", + MU_FLAG_SEEN | MU_FLAG_REPLIED, + "/home/cuux/Maildir/tea", + "/home/cuux/Maildir/tea/cur/123456:2,RS" + }, { + "/home/foo/Maildir/test/new/1313038887_0.697:2,", + MU_FLAG_SEEN | MU_FLAG_FLAGGED | MU_FLAG_PASSED, + "/home/boy/Maildir/stuff", + "/home/boy/Maildir/stuff/cur/1313038887_0.697:2,FPS" + } + }; + + for (i = 0; i != G_N_ELEMENTS(paths); ++i) { + gchar *str; + str = mu_maildir_get_new_path(paths[i].oldpath, + paths[i].targetdir, + paths[i].flags, FALSE); + g_assert_cmpstr(str, ==, paths[i].newpath); + g_free(str); + } +} + + +static void +test_mu_maildir_get_new_path_custom (void) +{ + int i; + + struct { + const char *oldpath; + MuFlags flags; + const char *targetdir; + const char *newpath; + } paths[] = { + { + "/home/foo/Maildir/test/cur/123456:2,FR", + MU_FLAG_REPLIED, "/home/foo/Maildir/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,R" + }, { + "/home/foo/Maildir/test/cur/123456:2,hFeRllo123", + MU_FLAG_FLAGGED, "/home/foo/Maildir/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,Fhello123" + }, { + "/home/foo/Maildir/test/cur/123456:2,abc", + MU_FLAG_PASSED, "/home/foo/Maildir/blabla", + "/home/foo/Maildir/blabla/cur/123456:2,Pabc" + } + }; + + for (i = 0; i != G_N_ELEMENTS(paths); ++i) { + gchar *str; + str = mu_maildir_get_new_path(paths[i].oldpath, + paths[i].targetdir, + paths[i].flags, FALSE); + g_assert_cmpstr(str, ==, paths[i].newpath); + g_free(str); + } +} + + + +static void +test_mu_maildir_get_maildir_from_path (void) +{ + unsigned u; + + struct { + const char *path, *exp; + } cases[] = { + {"/home/foo/Maildir/test/cur/123456:2,FR", + "/home/foo/Maildir/test"}, + {"/home/foo/Maildir/lala/new/1313038887_0.697:2,", + "/home/foo/Maildir/lala"} + }; + + + for (u = 0; u != G_N_ELEMENTS(cases); ++u) { + gchar *mdir; + mdir = mu_maildir_get_maildir_from_path (cases[u].path); + g_assert_cmpstr(mdir,==,cases[u].exp); + g_free (mdir); + } +} + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* mu_util_maildir_mkmdir */ + g_test_add_func ("/mu-maildir/mu-maildir-mkdir-01", + test_mu_maildir_mkdir_01); + g_test_add_func ("/mu-maildir/mu-maildir-mkdir-02", + test_mu_maildir_mkdir_02); + g_test_add_func ("/mu-maildir/mu-maildir-mkdir-03", + test_mu_maildir_mkdir_03); + g_test_add_func ("/mu-maildir/mu-maildir-mkdir-04", + test_mu_maildir_mkdir_04); + g_test_add_func ("/mu-maildir/mu-maildir-mkdir-05", + test_mu_maildir_mkdir_05); + + + /* mu_util_maildir_walk */ + g_test_add_func ("/mu-maildir/mu-maildir-walk-01", + test_mu_maildir_walk_01); + g_test_add_func ("/mu-maildir/mu-maildir-walk", + test_mu_maildir_walk); + g_test_add_func ("/mu-maildir/mu-maildir-walk-with-noupdate", + test_mu_maildir_walk_with_noupdate); + + /* get/set flags */ + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-new", + test_mu_maildir_get_new_path_new); + + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-01", + test_mu_maildir_get_new_path_01); + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-02", + test_mu_maildir_get_new_path_02); + g_test_add_func("/mu-maildir/mu-maildir-get-new-path-custom", + test_mu_maildir_get_new_path_custom); + g_test_add_func("/mu-maildir/mu-maildir-get-flags-from-path", + test_mu_maildir_get_flags_from_path); + + + g_test_add_func("/mu-maildir/mu-maildir-get-maildir-from-path", + test_mu_maildir_get_maildir_from_path); + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| + G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/test-mu-msg-fields.c b/lib/test-mu-msg-fields.c new file mode 100644 index 0000000..27864d8 --- /dev/null +++ b/lib/test-mu-msg-fields.c @@ -0,0 +1,134 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> + +#include <locale.h> + +#include "test-mu-common.h" +#include "mu-msg-fields.h" + +static void +test_mu_msg_field_body (void) +{ + MuMsgFieldId field; + + field = MU_MSG_FIELD_ID_BODY_TEXT; + + g_assert_cmpstr (mu_msg_field_name(field),==, "body"); + g_assert_cmpuint (mu_msg_field_shortcut(field),==, 'b'); + g_assert_cmpuint (mu_msg_field_xapian_prefix(field),==, 'B'); + + g_assert_cmpuint (mu_msg_field_is_numeric(field), ==, FALSE); +} + +static void +test_mu_msg_field_subject (void) +{ + MuMsgFieldId field; + + field = MU_MSG_FIELD_ID_SUBJECT; + + g_assert_cmpstr (mu_msg_field_name(field),==, "subject"); + g_assert_cmpuint (mu_msg_field_shortcut(field),==, 's'); + g_assert_cmpuint (mu_msg_field_xapian_prefix(field),==, 'S'); + + g_assert_cmpuint (mu_msg_field_is_numeric(field), ==,FALSE); +} + +static void +test_mu_msg_field_to (void) +{ + MuMsgFieldId field; + + field = MU_MSG_FIELD_ID_TO; + + g_assert_cmpstr (mu_msg_field_name(field),==, "to"); + g_assert_cmpuint (mu_msg_field_shortcut(field),==, 't'); + g_assert_cmpuint (mu_msg_field_xapian_prefix(field),==, 'T'); + + g_assert_cmpuint (mu_msg_field_is_numeric(field), ==, FALSE); +} + + +static void +test_mu_msg_field_prio (void) +{ + MuMsgFieldId field; + + field = MU_MSG_FIELD_ID_PRIO; + + g_assert_cmpstr (mu_msg_field_name(field),==, "prio"); + g_assert_cmpuint (mu_msg_field_shortcut(field),==, 'p'); + g_assert_cmpuint (mu_msg_field_xapian_prefix(field),==, 'P'); + + g_assert_cmpuint (mu_msg_field_is_numeric(field), ==, TRUE); +} + +static void +test_mu_msg_field_flags (void) +{ + MuMsgFieldId field; + + field = MU_MSG_FIELD_ID_FLAGS; + + g_assert_cmpstr (mu_msg_field_name(field),==, "flag"); + g_assert_cmpuint (mu_msg_field_shortcut(field),==, 'g'); + g_assert_cmpuint (mu_msg_field_xapian_prefix(field),==, 'G'); + + g_assert_cmpuint (mu_msg_field_is_numeric(field),==, TRUE); +} + + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* mu_msg_str_date */ + g_test_add_func ("/mu-msg-fields/mu-msg-field-body", + test_mu_msg_field_body); + g_test_add_func ("/mu-msg-fields/mu-msg-field-subject", + test_mu_msg_field_subject); + g_test_add_func ("/mu-msg-fields/mu-msg-field-to", + test_mu_msg_field_to); + g_test_add_func ("/mu-msg-fields/mu-msg-field-prio", + test_mu_msg_field_prio); + g_test_add_func ("/mu-msg-fields/mu-msg-field-flags", + test_mu_msg_field_flags); + + /* FIXME: add tests for mu_msg_str_flags; but note the + * function simply calls mu_msg_field_str */ + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | + G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/test-mu-msg.c b/lib/test-mu-msg.c new file mode 100644 index 0000000..fef368b --- /dev/null +++ b/lib/test-mu-msg.c @@ -0,0 +1,598 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> + +#include <locale.h> + +#include "test-mu-common.h" +#include "mu-msg.h" +#include "utils/mu-str.h" + + +static MuMsg* +get_msg (const char *path) +{ + GError *err; + MuMsg *msg; + + if (g_test_verbose ()) + g_print (">> %s\n", path); + + err = NULL; + msg = mu_msg_new_from_file (path, NULL, &err); + + if (!msg) { + g_printerr ("failed to load %s: %s\n", + path, err ? err->message : "something went wrong"); + g_clear_error (&err); + g_assert (0); + } + + return msg; +} + + + +static gboolean +check_contact_01 (MuMsgContact *contact, int *idx) +{ + switch (*idx) { + case 0: + g_assert_cmpstr (mu_msg_contact_name (contact), + ==, "Mickey Mouse"); + g_assert_cmpstr (mu_msg_contact_email (contact), + ==, "anon@example.com"); + break; + case 1: + g_assert_cmpstr (mu_msg_contact_name (contact), + ==, "Donald Duck"); + g_assert_cmpstr (mu_msg_contact_email (contact), + ==, "gcc-help@gcc.gnu.org"); + break; + default: + g_assert_not_reached (); + } + ++(*idx); + + return TRUE; +} + + +static void +test_mu_msg_01 (void) +{ + MuMsg *msg; + gint i; + + msg = get_msg (MU_TESTMAILDIR4 "/1220863042.12663_1.mindcrime!2,S"); + + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "Donald Duck <gcc-help@gcc.gnu.org>"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "gcc include search order"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "Mickey Mouse <anon@example.com>"); + g_assert_cmpstr (mu_msg_get_msgid(msg), + ==, "3BE9E6535E3029448670913581E7A1A20D852173@" + "emss35m06.us.lmco.com"); + g_assert_cmpstr (mu_msg_get_header(msg, "Mailing-List"), + ==, + "contact gcc-help-help@gcc.gnu.org; run by ezmlm"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'klub' */ + ==, MU_MSG_PRIO_NORMAL); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 1217530645); + + i = 0; + mu_msg_contact_foreach (msg, (MuMsgContactForeachFunc)check_contact_01, + &i); + g_assert_cmpint (i,==,2); + + mu_msg_unref (msg); +} + + + + + + +static gboolean +check_contact_02 (MuMsgContact *contact, int *idx) +{ + switch (*idx) { + case 0: + g_assert_cmpstr (mu_msg_contact_name (contact), + ==, NULL); + g_assert_cmpstr (mu_msg_contact_email (contact), + ==, "anon@example.com"); + break; + case 1: + g_assert_cmpstr (mu_msg_contact_name (contact), + ==, NULL); + g_assert_cmpstr (mu_msg_contact_email (contact), + ==, "help-gnu-emacs@gnu.org"); + break; + default: + g_assert_not_reached (); + } + ++(*idx); + + return TRUE; +} + + + +static void +test_mu_msg_02 (void) +{ + MuMsg *msg; + int i; + + msg = get_msg (MU_TESTMAILDIR4 "/1220863087.12663_19.mindcrime!2,S"); + + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "help-gnu-emacs@gnu.org"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "Re: Learning LISP; Scheme vs elisp."); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "anon@example.com"); + g_assert_cmpstr (mu_msg_get_msgid(msg), + ==, "r6bpm5-6n6.ln1@news.ducksburg.com"); + g_assert_cmpstr (mu_msg_get_header(msg, "Errors-To"), + ==, "help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_LOW); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 1218051515); + + i = 0; + mu_msg_contact_foreach (msg, + (MuMsgContactForeachFunc)check_contact_02, + &i); + g_assert_cmpint (i,==,2); + g_assert_cmpuint (mu_msg_get_flags(msg), ==, MU_FLAG_SEEN|MU_FLAG_LIST); + + mu_msg_unref (msg); +} + +static void +test_mu_msg_03 (void) +{ + MuMsg *msg; + const GSList *params; + + msg = get_msg (MU_TESTMAILDIR4 "/1283599333.1840_11.cthulhu!2,"); + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "Bilbo Baggins <bilbo@anotherexample.com>"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "Greetings from Lothlórien"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "Frodo Baggins <frodo@example.com>"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_NORMAL); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 0); + g_assert_cmpstr (mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE), + ==, + "\nLet's write some fünkÿ text\nusing umlauts.\n\nFoo.\n"); + + params = mu_msg_get_body_text_content_type_parameters( + msg, MU_MSG_OPTION_NONE); + g_assert_cmpuint (g_slist_length ((GSList*)params), ==, 2); + + g_assert_cmpstr ((char*)params->data,==, "charset"); + params = g_slist_next(params); + g_assert_cmpstr ((char*)params->data,==,"UTF-8"); + + g_assert_cmpuint (mu_msg_get_flags(msg), + ==, MU_FLAG_UNREAD); /* not seen => unread */ + + mu_msg_unref (msg); +} + + +static void +test_mu_msg_04 (void) +{ + MuMsg *msg; + + msg = get_msg (MU_TESTMAILDIR4 "/mail5"); + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "George Custer <gac@example.com>"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "pics for you"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "Sitting Bull <sb@example.com>"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_NORMAL); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 0); + g_assert_cmpuint (mu_msg_get_flags(msg), + ==, MU_FLAG_HAS_ATTACH|MU_FLAG_UNREAD); + mu_msg_unref (msg); +} + + +static void +test_mu_msg_multimime (void) +{ + MuMsg *msg; + + msg = get_msg (MU_TESTMAILDIR4 "/multimime!2,FS"); + /* ie., are text parts properly concatenated? */ + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "multimime"); + g_assert_cmpstr (mu_msg_get_body_text(msg, MU_MSG_OPTION_NONE), + ==, "abcdef"); + g_assert_cmpuint (mu_msg_get_flags(msg), + ==, MU_FLAG_FLAGGED | MU_FLAG_SEEN | + MU_FLAG_HAS_ATTACH); + mu_msg_unref (msg); +} + + + +static void +test_mu_msg_flags (void) +{ + unsigned u; + + struct { + const char *path; + MuFlags flags; + } msgflags [] = { + { MU_TESTMAILDIR4 "/multimime!2,FS", + MU_FLAG_FLAGGED | MU_FLAG_SEEN | + MU_FLAG_HAS_ATTACH }, + { MU_TESTMAILDIR4 "/special!2,Sabc", + MU_FLAG_SEEN } + + }; + + for (u = 0; u != G_N_ELEMENTS(msgflags); ++u) { + MuMsg *msg; + MuFlags flags; + + g_assert ((msg = get_msg (msgflags[u].path))); + flags = mu_msg_get_flags (msg); + + if (g_test_verbose()) + g_print ("=> %s [ %s, %u] <=> [ %s, %u]\n", + msgflags[u].path, + mu_flags_to_str_s(msgflags[u].flags, + MU_FLAG_TYPE_ANY), + (unsigned)msgflags[u].flags, + mu_flags_to_str_s(flags, MU_FLAG_TYPE_ANY), + (unsigned)flags); + g_assert_cmpuint (flags ,==, msgflags[u].flags); + mu_msg_unref (msg); + } +} + + +static void +test_mu_msg_umlaut (void) +{ + MuMsg *msg; + + msg = get_msg (MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,"); + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "Helmut Kröger <hk@testmu.xxx>"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "Motörhead"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "Mü <testmu@testmu.xx>"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_NORMAL); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 0); + + mu_msg_unref (msg); +} + + +static void +test_mu_msg_references (void) +{ + MuMsg *msg; + const GSList *refs; + + msg = get_msg (MU_TESTMAILDIR4 "/1305664394.2171_402.cthulhu!2,"); + refs = mu_msg_get_references(msg); + + g_assert_cmpuint (g_slist_length ((GSList*)refs), ==, 4); + + g_assert_cmpstr ((char*)refs->data,==, "non-exist-01@msg.id"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, "non-exist-02@msg.id"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, "non-exist-03@msg.id"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, "non-exist-04@msg.id"); + refs = g_slist_next (refs); + + mu_msg_unref (msg); +} + + + +static void +test_mu_msg_references_dups (void) +{ + MuMsg *msg; + const GSList *refs; + const char *mlist; + + msg = get_msg (MU_TESTMAILDIR4 "/1252168370_3.14675.cthulhu!2,S"); + refs = mu_msg_get_references(msg); + + /* make sure duplicate msg-ids are filtered out */ + + g_assert_cmpuint (g_slist_length ((GSList*)refs), ==, 6); + + g_assert_cmpstr ((char*)refs->data,==, + "439C1136.90504@euler.org"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, + "4399DD94.5070309@euler.org"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, + "20051209233303.GA13812@gauss.org"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, + "439B41ED.2080402@euler.org"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, + "439A1E03.3090604@euler.org"); + refs = g_slist_next (refs); + g_assert_cmpstr ((char*)refs->data,==, + "20051211184308.GB13513@gauss.org"); + refs = g_slist_next (refs); + + mlist = mu_msg_get_mailing_list (msg); + g_assert_cmpstr (mlist ,==, "Example of List Id"); + + mu_msg_unref (msg); +} + + +static void +test_mu_msg_references_many (void) +{ + MuMsg *msg; + unsigned u; + const GSList *refs, *cur; + const char* expt_refs[] = { + "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt.googlegroups.com", + "87hbblwelr.fsf@sapphire.mobileactivedefense.com", + "pql248-4va.ln1@wilbur.25thandClement.com", + "ikns6r$li3$1@Iltempo.Update.UU.SE", + "8762s0jreh.fsf@sapphire.mobileactivedefense.com", + "ikqqp1$jv0$1@Iltempo.Update.UU.SE", + "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com", + "ikr0na$lru$1@Iltempo.Update.UU.SE", + "tO8cp.1228$GE6.370@news.usenetserver.com", + "ikr6ks$nlf$1@Iltempo.Update.UU.SE", + "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk" + }; + + msg = get_msg (MU_TESTMAILDIR2 "/bar/cur/181736.eml"); + refs = mu_msg_get_references(msg); + + g_assert_cmpuint (G_N_ELEMENTS(expt_refs), ==, + g_slist_length((GSList*)refs)); + + for (cur = refs, u = 0; cur; cur = g_slist_next(cur), ++u) { + if (g_test_verbose()) + g_print ("%u. '%s' =? '%s'\n", + u, (char*)cur->data, + expt_refs[u]); + + g_assert_cmpstr ((char*)cur->data, ==, expt_refs[u]); + } + + mu_msg_unref (msg); +} + +static void +test_mu_msg_tags (void) +{ + MuMsg *msg; + const GSList *tags; + + msg = get_msg (MU_TESTMAILDIR4 "/mail1"); + + g_assert_cmpstr (mu_msg_get_to(msg), + ==, "Julius Caesar <jc@example.com>"); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "Fere libenter homines id quod volunt credunt"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "John Milton <jm@example.com>"); + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_HIGH); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 1217530645); + + tags = mu_msg_get_tags (msg); + g_assert_cmpstr ((char*)tags->data,==,"Paradise"); + g_assert_cmpstr ((char*)tags->next->data,==,"losT"); + g_assert_cmpstr ((char*)tags->next->next->data,==,"john"); + g_assert_cmpstr ((char*)tags->next->next->next->data,==,"milton"); + + g_assert (!tags->next->next->next->next); + + mu_msg_unref (msg); +} + + +static void +test_mu_msg_comp_unix_programmer (void) +{ + MuMsg *msg; + char *refs; + + msg = get_msg (MU_TESTMAILDIR4 "/181736.eml"); + g_assert_cmpstr (mu_msg_get_to(msg), + ==, NULL); + g_assert_cmpstr (mu_msg_get_subject(msg), + ==, "Re: Are writes \"atomic\" to readers of the file?"); + g_assert_cmpstr (mu_msg_get_from(msg), + ==, "Jimbo Foobarcuux <jimbo@slp53.sl.home>"); + g_assert_cmpstr (mu_msg_get_msgid(msg), + ==, "oktdp.42997$Te.22361@news.usenetserver.com"); + + refs = mu_str_from_list (mu_msg_get_references(msg), ','); + g_assert_cmpstr (refs, ==, + "e9065dac-13c1-4103-9e31-6974ca232a89@t15g2000prt" + ".googlegroups.com," + "87hbblwelr.fsf@sapphire.mobileactivedefense.com," + "pql248-4va.ln1@wilbur.25thandClement.com," + "ikns6r$li3$1@Iltempo.Update.UU.SE," + "8762s0jreh.fsf@sapphire.mobileactivedefense.com," + "ikqqp1$jv0$1@Iltempo.Update.UU.SE," + "87hbbjc5jt.fsf@sapphire.mobileactivedefense.com," + "ikr0na$lru$1@Iltempo.Update.UU.SE," + "tO8cp.1228$GE6.370@news.usenetserver.com," + "ikr6ks$nlf$1@Iltempo.Update.UU.SE," + "8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk"); + g_free (refs); + + //"jimbo@slp53.sl.home (Jimbo Foobarcuux)"; + g_assert_cmpuint (mu_msg_get_prio(msg), /* 'low' */ + ==, MU_MSG_PRIO_NORMAL); + g_assert_cmpuint (mu_msg_get_date(msg), + ==, 1299603860); + + mu_msg_unref (msg); +} + + + +static void +test_mu_str_prio_01 (void) +{ + g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_LOW), ==, "low"); + g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_NORMAL), ==, "normal"); + g_assert_cmpstr(mu_msg_prio_name(MU_MSG_PRIO_HIGH), ==, "high"); +} + + +static gboolean +ignore_error (const char* log_domain, GLogLevelFlags log_level, + const gchar* msg, gpointer user_data) +{ + return FALSE; /* don't abort */ +} + + +static void +test_mu_str_prio_02 (void) +{ + /* this must fail */ + g_test_log_set_fatal_handler ((GTestLogFatalFunc)ignore_error, NULL); + g_assert_cmpstr (mu_msg_prio_name(666), ==, NULL); +} + + + +static void +test_mu_str_display_contact (void) +{ + int i; + struct { + const char* word; + const char* disp; + } words [] = { + { "\"Foo Bar\" <aap@noot.mies>", "Foo Bar"}, + { "Foo Bar <aap@noot.mies>", "Foo Bar" }, + { "<aap@noot.mies>", "aap@noot.mies" }, + { "foo@bar.nl", "foo@bar.nl" } + }; + + for (i = 0; i != G_N_ELEMENTS(words); ++i) + g_assert_cmpstr (mu_str_display_contact_s (words[i].word), ==, + words[i].disp); +} + + +int +main (int argc, char *argv[]) +{ + int rv; + + g_test_init (&argc, &argv, NULL); + + /* mu_msg_str_date */ + g_test_add_func ("/mu-msg/mu-msg-01", + test_mu_msg_01); + g_test_add_func ("/mu-msg/mu-msg-02", + test_mu_msg_02); + g_test_add_func ("/mu-msg/mu-msg-03", + test_mu_msg_03); + g_test_add_func ("/mu-msg/mu-msg-04", + test_mu_msg_04); + g_test_add_func ("/mu-msg/mu-msg-multimime", + test_mu_msg_multimime); + + g_test_add_func ("/mu-msg/mu-msg-flags", + test_mu_msg_flags); + + g_test_add_func ("/mu-msg/mu-msg-tags", + test_mu_msg_tags); + g_test_add_func ("/mu-msg/mu-msg-references", + test_mu_msg_references); + g_test_add_func ("/mu-msg/mu-msg-references_dups", + test_mu_msg_references_dups); + g_test_add_func ("/mu-msg/mu-msg-references_many", + test_mu_msg_references_many); + + g_test_add_func ("/mu-msg/mu-msg-umlaut", + test_mu_msg_umlaut); + g_test_add_func ("/mu-msg/mu-msg-comp-unix-programmer", + test_mu_msg_comp_unix_programmer); + + /* mu_str_prio */ + g_test_add_func ("/mu-str/mu-str-prio-01", + test_mu_str_prio_01); + g_test_add_func ("/mu-str/mu-str-prio-02", + test_mu_str_prio_02); + + + g_test_add_func ("/mu-str/mu-str-display_contact", + test_mu_str_display_contact); + + + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| + G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + rv = g_test_run (); + + return rv; +} diff --git a/lib/test-mu-store.c b/lib/test-mu-store.c new file mode 100644 index 0000000..3276d57 --- /dev/null +++ b/lib/test-mu-store.c @@ -0,0 +1,206 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl> +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include <glib.h> +#include <stdlib.h> +#include <unistd.h> +#include <time.h> + +#include <locale.h> + +#include "test-mu-common.h" +#include "mu-store.hh" + +static void +test_mu_store_new_destroy (void) +{ + MuStore *store; + gchar* tmpdir; + GError *err; + + tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + err = NULL; + store = mu_store_new_create (tmpdir, "/tmp", NULL, &err); + g_assert_no_error (err); + g_assert (store); + + g_assert_cmpuint (0,==,mu_store_count (store, NULL)); + + mu_store_flush (store); + mu_store_unref (store); + + g_free (tmpdir); +} + + +static void +test_mu_store_version (void) +{ + MuStore *store; + gchar* tmpdir; + GError *err; + + tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + err = NULL; + store = mu_store_new_create (tmpdir, "/tmp", NULL, &err); + g_assert (store); + mu_store_unref (store); + store = mu_store_new_readable (tmpdir, &err); + g_assert (store); + + g_assert (err == NULL); + + g_assert_cmpuint (0,==,mu_store_count (store, NULL)); + g_assert_cmpstr (MU_STORE_SCHEMA_VERSION,==, + mu_store_schema_version(store)); + + mu_store_unref (store); + g_free (tmpdir); +} + + +G_GNUC_UNUSED static void +test_mu_store_store_msg_and_count (void) +{ + MuMsg *msg; + MuStore *store; + gchar* tmpdir; + + tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL); + g_assert (store); + g_free (tmpdir); + + g_assert_cmpuint (0,==,mu_store_count (store, NULL)); + + /* add one */ + /* XXX this passes, but not make-dist; investigate */ + msg = mu_msg_new_from_file ( + MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", + NULL, NULL); + g_assert (msg); + g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), + !=, MU_STORE_INVALID_DOCID); + g_assert_cmpuint (1,==,mu_store_count (store, NULL)); + g_assert_cmpuint (TRUE,==,mu_store_contains_message + (store, + MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,")); + mu_msg_unref (msg); + + /* add another one */ + msg = mu_msg_new_from_file (MU_TESTMAILDIR2 + "/bar/cur/mail3", NULL, NULL); + g_assert (msg); + g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), + !=, MU_STORE_INVALID_DOCID); + g_assert_cmpuint (2,==,mu_store_count (store, NULL)); + g_assert_cmpuint (TRUE,==, + mu_store_contains_message (store, MU_TESTMAILDIR2 + "/bar/cur/mail3")); + mu_msg_unref (msg); + + /* try to add the first one again. count should be 2 still */ + msg = mu_msg_new_from_file + (MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", + NULL, NULL); + g_assert (msg); + g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), + !=, MU_STORE_INVALID_DOCID); + g_assert_cmpuint (2,==,mu_store_count (store, NULL)); + + mu_msg_unref (msg); + mu_store_unref (store); +} + + +G_GNUC_UNUSED static void +test_mu_store_store_msg_remove_and_count (void) +{ + MuMsg *msg; + MuStore *store; + gchar* tmpdir; + GError *err; + + tmpdir = test_mu_common_get_random_tmpdir(); + g_assert (tmpdir); + + store = mu_store_new_create (tmpdir, MU_TESTMAILDIR, NULL, NULL); + g_assert (store); + + g_assert_cmpuint (0,==,mu_store_count (store, NULL)); + + /* add one */ + err = NULL; + msg = mu_msg_new_from_file ( + MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,", + NULL, &err); + g_assert (msg); + g_assert_cmpuint (mu_store_add_msg (store, msg, NULL), + !=, MU_STORE_INVALID_DOCID); + g_assert_cmpuint (1,==,mu_store_count (store, NULL)); + mu_msg_unref (msg); + + /* remove one */ + mu_store_remove_path (store, + MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,"); + g_assert_cmpuint (0,==,mu_store_count (store, NULL)); + g_assert_cmpuint (FALSE,==,mu_store_contains_message + (store, + MU_TESTMAILDIR "/cur/1283599333.1840_11.cthulhu!2,")); + g_free (tmpdir); + mu_store_unref (store); +} + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* mu_runtime_init/uninit */ + g_test_add_func ("/mu-store/mu-store-new-destroy", + test_mu_store_new_destroy); + g_test_add_func ("/mu-store/mu-store-version", + test_mu_store_version); +#if 0 + /* XXX this passes, but not make-dist; investigate */ + g_test_add_func ("/mu-store/mu-store-store-and-count", + test_mu_store_store_msg_and_count); + g_test_add_func ("/mu-store/mu-store-store-remove-and-count", + test_mu_store_store_msg_remove_and_count); +#endif + if (!g_test_verbose()) + g_log_set_handler (NULL, + G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL| G_LOG_FLAG_RECURSION, + (GLogFunc)black_hole, NULL); + + return g_test_run (); +} diff --git a/lib/testdir/cur/1220863042.12663_1.mindcrime!2,S b/lib/testdir/cur/1220863042.12663_1.mindcrime!2,S new file mode 100644 index 0000000..ab1500f --- /dev/null +++ b/lib/testdir/cur/1220863042.12663_1.mindcrime!2,S @@ -0,0 +1,146 @@ +Return-Path: <gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX, + RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:19 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008 + 20:15:17 -0700 (PDT) +Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06 + Aug 2008 20:15:16 -0700 (PDT) +Received: from sourceware.org (sourceware.org [209.132.176.174]) by + mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug + 2008 20:15:16 -0700 (PDT) +Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor + denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + client-ip=209.132.176.174; +Authentication-Results: mx.google.com; spf=neutral (google.com: + 209.132.176.174 is neither permitted nor denied by domain of + gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org +Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000 +Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000 +Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7) + by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000 +Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by + mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for + <gcc-help@gcc.gnu.org>; Wed, 6 Aug 2008 21:14:25 -0600 (MDT) +Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) + id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 + 21:14:25 -0600 (MDT) +Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF + V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for + gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT) +Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by + EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug + 2008 23:14:20 -0400 +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Mickey Mouse" <anon@example.com> +Subject: gcc include search order +To: "Donald Duck" <gcc-help@gcc.gnu.org> +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Content-class: urn:content-classes:message +Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm +Precedence: klub +List-Id: <gcc-help.gcc.gnu.org> +List-Unsubscribe: <mailto:gcc-help-unsubscribe-xxxx.klub=gmail.com@gcc.gnu.org> +List-Archive: <http://gcc.gnu.org/ml/gcc-help/> +List-Post: <mailto:gcc-help@gcc.gnu.org> +List-Help: <mailto:gcc-help-help@gcc.gnu.org> +Sender: gcc-help-owner@gcc.gnu.org +Delivered-To: mailing list gcc-help@gcc.gnu.org +Content-Length: 3024 + + +Hi. +In my unit testing I need to change some header files (target is +vxWorks, which supports some things that the sun does not). +So, what I do is fetch the development tree, and then in a new unit test +directory I attempt to compile the unit under test. Since this is NOT +vxworks, I use sed to change some of the .h files and put them in a +./changed directory. + +When I try to compile the file, it is still using the .h file from the +original location, even though I have listed the include path for +./changed before the include path for the development tree. + +Here is a partial output from gcc using the -v option + +GNU CPP version 3.1 (cpplib) (sparc ELF) +GNU C++ version 3.1 (sparc-sun-solaris2.8) + compiled by GNU C version 3.1. +ignoring nonexistent directory "NONE/include" +#include "..." search starts here: +#include <...> search starts here: + . + changed + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface + /usr/local/include/g++-v3 + /usr/local/include/g++-v3/sparc-sun-solaris2.8 + /usr/local/include/g++-v3/backward + /usr/local/include + /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include + /usr/local/sparc-sun-solaris2.8/include + /usr/include +End of search list. + +I know the changed file is correct and that the include is not working +as expected, because when I copy the file from ./changed, back into the +development tree, the compilation works as expected. + +One more bit of information. The source that I cam compiling is in +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app +And it is including files from +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +These include files should be including the files from ./changed (when +they exist) but they are ignoring the .h files in the ./changed +directory and are instead using other, unchanged files in the +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +directory. + +The gcc command line is something like + + TEST_DIR="." + + CHANGED_DIR_NAME=changed + CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME} + + CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I +${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}" + + HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}" + DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST" + + CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow +-Wmissing-prototypes -Wmissing-declarations" + + printf "Compiling the UUT File\n" + gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES} +${AP_APP_FILES}/unitUnderTest.cpp + + +I hope this explanation is clear. If anyone knows how to fix the command +line so that it gets the .h files in the "changed" directory are used +instead of files in the other include directories. + +Thanks +Joe + +---------------------------------------------------- +Time Flies like an Arrow. Fruit Flies like a Banana + + diff --git a/lib/testdir/cur/1220863060.12663_3.mindcrime!2,S b/lib/testdir/cur/1220863060.12663_3.mindcrime!2,S new file mode 100644 index 0000000..d0ff0d7 --- /dev/null +++ b/lib/testdir/cur/1220863060.12663_3.mindcrime!2,S @@ -0,0 +1,230 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00,HTML_MESSAGE + autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D724F6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:27 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:27 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs86537wfy; Mon, 4 Aug 2008 00:38:51 + -0700 (PDT) +Received: by 10.151.113.5 with SMTP id q5mr272266ybm.37.1217835529913; Mon, 04 + Aug 2008 00:38:49 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 5si5754915ywd.8.2008.08.04.00.38.30; Mon, 04 Aug 2008 00:38:50 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 765A511C46; Mon, 4 Aug 2008 03:38:27 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from ik-out-1112.google.com (ik-out-1112.google.com [66.249.90.176]) + by sqlite.org (Postfix) with ESMTP id 4C59511C41 for <sqlite-dev@sqlite.org>; + Mon, 4 Aug 2008 03:38:23 -0400 (EDT) +Received: by ik-out-1112.google.com with SMTP id b32so2163423ika.0 for + <sqlite-dev@sqlite.org>; Mon, 04 Aug 2008 00:38:23 -0700 (PDT) +Received: by 10.210.54.19 with SMTP id c19mr14589042eba.107.1217835502549; + Mon, 04 Aug 2008 00:38:22 -0700 (PDT) +Received: by 10.210.115.10 with HTTP; Mon, 4 Aug 2008 00:38:22 -0700 (PDT) +Message-ID: <477821040808040038s381bf382p7411451e3c1a2e4e@mail.gmail.com> +Date: Mon, 4 Aug 2008 10:38:22 +0300 +From: anon@example.com +To: sqlite-dev@sqlite.org +In-Reply-To: <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com> +MIME-Version: 1.0 +References: <477821040808030533y41f1501dq32447b568b6e6ca5@mail.gmail.com> + <73d4fc50808030747g303a170ieac567723c2d4f24@mail.gmail.com> +Subject: Re: [sqlite-dev] SQLite exception A&B +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Priority: normal +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: multipart/mixed; boundary="===============2123623832==" +Mime-version: 1.0 +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 8475 + +--===============2123623832== +Content-Type: multipart/alternative; + boundary="----=_Part_29556_25702991.1217835502493" + +------=_Part_29556_25702991.1217835502493 +Content-Type: text/plain; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +Hi Grant, + +Thanks for your reply. +I am using a different session for each thread, whenever a thread wishes to +access the database it gets a session from the session pool and works with +that session until its work is done. + +Most of the actions the threads are doing on the database are quite +complicated and are required to be fully committed or completely ignored, so +yes, I am (most of the time) explicitly beginning and committing my +transactions. + +Regarding the SQLiteStatementImpl, I believe the Poco manual explains that +sessions and statements for that matter cannot be shared between threads, +therefore if you are using a session via one thread only it should work +fine. + +My first impression was that the problem was in the Poco infrastructure (I +have found several Poco related bugs in the past), but the problem ALWAYS +occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco +related bug, I would expect to see it here and there without any relation to +this specific statement, but that is not the case. + +None the less, I will also post my question on the Poco forums. + +Nadav. + +On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <grant.gatchel@gmail.com>wrote: + +> Are you using the same Poco::Session for every thread or does each call +> create a new session/handle to the database? +> +> Are you explicitly BEGINning and COMMITting your transactions? +> +> In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a +> race condition in the SQLiteStatementImpl::next() method in which the member +> _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() +> method has a chance to interpret that value and throw an exception. +> +> This question might be more suitable in the Poco forums or mailinglist. +> +> - Grant +> +> On Sun, Aug 3, 2008 at 8:33 AM, nadav g <nadav.gr@gmail.com> wrote: +> +>> Hi All, +>> +>> I have been using SQLite with Poco (www.appinf.com) as my infrastructure. +>> The program is running several threads that access this database very +>> often and are synchronized by SQLite itself. +>> Everything seems to work just fine most of time (usually days - weeks) but +>> I do get an occasional exception: +>> +>> Exception: SQL error or missing database: Iterator Error: trying to check +>> if there is a next value +>> +>> The backtrace leads to this statement: +>> *"BEGIN IMMEDIATE"* +>> +>> This specific code runs numerous times before an exception occurs (if +>> occurs at all) and I cannot think of any reason for it to fail later rather +>> than sooner. +>> It is pretty obvious that this situation occurs due to some rare thread +>> state, but I could not find any information that gives me any hint as to +>> what this state might be. +>> +>> So what I am asking is: +>> 1) Does anyone know why this sort of exception occurs? +>> 2) Can anyone think of a reason for such an exception to occur in the +>> situation I have described? +>> +>> Thanks in advance, +>> Nadav. +>> +>> +>> _______________________________________________ +>> sqlite-dev mailing list +>> sqlite-dev@sqlite.org +>> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +>> +>> +> +> _______________________________________________ +> sqlite-dev mailing list +> sqlite-dev@sqlite.org +> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +> +> + +------=_Part_29556_25702991.1217835502493 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +<div dir="ltr">Hi Grant,<br><br>Thanks for your reply.<br>I am using a different session for each thread, whenever a thread wishes to access the database it gets a session from the session pool and works with that session until its work is done.<br> +<br>Most of the actions the threads are doing on the database are quite complicated and are required to be fully committed or completely ignored, so yes, I am (most of the time) explicitly beginning and committing my transactions.<br> +<br>Regarding the SQLiteStatementImpl, I believe the Poco manual explains that sessions and statements for that matter cannot be shared between threads, therefore if you are using a session via one thread only it should work fine.<br> +<br>My first impression was that the problem was in the Poco infrastructure (I have found several Poco related bugs in the past), but the problem ALWAYS occurs when I perform the "BEGIN IMMEDIATE" action, if it were a Poco related bug, I would expect to see it here and there without any relation to this specific statement, but that is not the case.<br> +<br>None the less, I will also post my question on the Poco forums.<br><br>Nadav.<br><br><div class="gmail_quote">On Sun, Aug 3, 2008 at 5:47 PM, Grant Gatchel <span dir="ltr"><<a href="mailto:grant.gatchel@gmail.com">grant.gatchel@gmail.com</a>></span> wrote:<br> +<blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"><div dir="ltr">Are you using the same Poco::Session for every thread or does each call create a new session/handle to the database?<br> +<br>Are you explicitly BEGINning and COMMITting your transactions?<br><br>In looking at the 1.3.2 branch of Poco::Data::SQLite, there appears to be a race condition in the SQLiteStatementImpl::next() method in which the member _nextResponse is being accessed before the SQLiteStatementImpl::hasNext() method has a chance to interpret that value and throw an exception.<br> + +<br>This question might be more suitable in the Poco forums or mailinglist.<br><br>- Grant<br> +<br><div class="gmail_quote"><div><div></div><div class="Wj3C7c"> +On Sun, Aug 3, 2008 at 8:33 AM, nadav g <span dir="ltr"><<a href="http://nadav.gr" target="_blank">nadav.gr</a>@<a href="http://gmail.com" target="_blank">gmail.com</a>></span> wrote:<br></div></div><blockquote class="gmail_quote" style="border-left: 1px solid rgb(204, 204, 204); margin: 0pt 0pt 0pt 0.8ex; padding-left: 1ex;"> +<div><div></div><div class="Wj3C7c"> + + +<div dir="ltr">Hi All,<br><br>I have been using SQLite with Poco (<a href="http://www.appinf.com" target="_blank">www.appinf.com</a>) as my infrastructure.<br>The program is running several threads that access this database very often and are synchronized by SQLite itself.<br> + + + + +Everything seems to work just fine most of time (usually days - weeks) but I do get an occasional exception:<br><br>Exception: SQL error or missing database: Iterator Error: trying to check if there is a next value<br><br> + + + + +The backtrace leads to this statement:<br><b>"BEGIN IMMEDIATE"</b><br><br>This specific code runs numerous times before an exception occurs (if occurs at all) and I cannot think of any reason for it to fail later rather than sooner.<br> + + + + +It is pretty obvious that this situation occurs due to some rare thread state, but I could not find any information that gives me any hint as to what this state might be.<br><br>So what I am asking is:<br>1) Does anyone know why this sort of exception occurs?<br> + + + + +2) Can anyone think of a reason for such an exception to occur in the situation I have described?<br><br>Thanks in advance,<br>Nadav.<br><br></div> +<br></div></div>_______________________________________________<br> +sqlite-dev mailing list<br> +<a href="mailto:sqlite-dev@sqlite.org" target="_blank">sqlite-dev@sqlite.org</a><br> +<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br> +<br></blockquote></div><br></div> +<br>_______________________________________________<br> +sqlite-dev mailing list<br> +<a href="mailto:sqlite-dev@sqlite.org">sqlite-dev@sqlite.org</a><br> +<a href="http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev" target="_blank">http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev</a><br> +<br></blockquote></div><br></div> + +------=_Part_29556_25702991.1217835502493-- + +--===============2123623832== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--===============2123623832==-- + diff --git a/lib/testdir/cur/1220863087.12663_15.mindcrime!2,PS b/lib/testdir/cur/1220863087.12663_15.mindcrime!2,PS new file mode 100644 index 0000000..d6487c0 --- /dev/null +++ b/lib/testdir/cur/1220863087.12663_15.mindcrime!2,PS @@ -0,0 +1,136 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS,WHOIS_NETSOLPR autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 1A6CD69CB6 + for <xxxx@localhost>; Tue, 12 Aug 2008 21:42:38 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Tue, 12 Aug 2008 21:42:38 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs123119wfh; Sun, 10 Aug 2008 + 22:06:31 -0700 (PDT) +Received: by 10.100.166.10 with SMTP id o10mr9327844ane.0.1218431190107; Sun, + 10 Aug 2008 22:06:30 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id c29si10110392anc.13.2008.08.10.22.06.29; Sun, 10 Aug 2008 + 22:06:30 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:45637 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KSPbx-0006dj-96 for + xxxx.klub@gmail.com; Mon, 11 Aug 2008 01:06:29 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KSPbE-0006cQ-Nd for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KSPbD-0006bs-Px for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:44 -0400 +Received: from [199.232.76.173] (port=37426 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KSPbD-0006bk-HT for + help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400 +Received: from main.gmane.org ([80.91.229.2]:46446 helo=ciao.gmane.org) by + monty-python.gnu.org with esmtps (TLS-1.0:RSA_AES_256_CBC_SHA1:32) (Exim + 4.60) (envelope-from <geh-help-gnu-emacs@m.gmane.org>) id 1KSPbD-0003Kl-CA + for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 01:05:43 -0400 +Received: from list by ciao.gmane.org with local (Exim 4.43) id + 1KSPb9-00080r-CX for help-gnu-emacs@gnu.org; Mon, 11 Aug 2008 05:05:39 +0000 +Received: from bas2-toronto63-1088792724.dsl.bell.ca ([64.229.168.148]) by + main.gmane.org with esmtp (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for + <help-gnu-emacs@gnu.org>; Mon, 11 Aug 2008 05:05:39 +0000 +Received: from cpchan by bas2-toronto63-1088792724.dsl.bell.ca with local + (Gmexim 0.1 (Debian)) id 1AlnuQ-0007hv-00 for <help-gnu-emacs@gnu.org>; Mon, + 11 Aug 2008 05:05:39 +0000 +X-Injected-Via-Gmane: http://gmane.org/ +To: help-gnu-emacs@gnu.org +From: anon@example.com +Date: Mon, 11 Aug 2008 01:03:22 -0400 +Organization: Linux Private Site +Message-ID: <87bq00nnxh.fsf@MagnumOpus.Mercurius> +References: <877iav5s49.fsf@163.com> <86hc9yc5sj.fsf@timbral.net> + <877iat7udd.fsf@163.com> <87fxphcsxi.fsf@lion.rapttech.com.au> + <8504ddd4-5e3b-4ed5-bf77-aa9cce81b59a@1g2000pre.googlegroups.com> + <87k5es59we.fsf@lion.rapttech.com.au> + <63c824e3-62b1-4a93-8fa8-2813e1f9397f@v13g2000pro.googlegroups.com> + <874p5vsgg8.fsf@nonospaz.fatphil.org> + <8250972e-1886-4021-80bc-376e34881c80@v39g2000pro.googlegroups.com> + <87zlnnqvvs.fsf@nonospaz.fatphil.org> + <57add0e0-b39d-4c71-8d2c-d3b9ddfaa1a9@1g2000pre.googlegroups.com> + <87sktfnz5p.fsf@atthis.clsnet.nl> + <562e1111-d9e7-4b6a-b661-3f9af13fea17@b30g2000prf.googlegroups.com> + <87d4khoq97.fsf@atthis.clsnet.nl> + <0fe404c5-cab8-4692-8a27-532e737a7813@i24g2000prf.googlegroups.com> +Mime-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" +X-Complaints-To: usenet@ger.gmane.org +X-Gmane-NNTP-Posting-Host: bas2-toronto63-1088792724.dsl.bell.ca +X-Face: G; + Z,`sm>)4t4LB/GUrgH$W`!AmfHMj,LG)Z}X0ax@s9:0>0)B&@vcm{v-le)wng)?|o]D<V6&ay<F=H{M5?$T%p!dPdJeF,au\E@TA"v22K!Zl\\mzpU4]6$ZnAI3_L)h; + fpd}mn2py/7gv^|*85-D_f:07cT>\Z}0:6X +User-Agent: Gnus/5.110011 (No Gnus v0.11) Emacs/23.0.60 (gnu/linux) +Cancel-Lock: sha1:IKyfrl5drOw6HllHFSmWHAKEeC8= +X-detected-kernel: by monty-python.gnu.org: Linux 2.6, seldom 2.4 (older, 4) +Subject: Re: Can anybody tell me how to send HTML-format mail in gnus +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 1229 +Lines: 36 + +--=-=-= +Content-Type: text/plain + +Xah <xahlee@gmail.com> writes: + +> So, i was reading about it in Wikipedia. Although i don't have a TV, +> and haven't had since 2000, but i still enjoyed the festive spirits +> anyhow. After all, i'm Chinese by blood. So, in my wandering, i ran +> into this welcome song on youtube: +> +> http://www.youtube.com/watch?v=1HEndNYVhZo + +What is your point? Your email is in plain text and I can click on the +link just fine- it is not exactly rocket science to implement parsing of +URL's to workable links in an Email program (a lot of programs does +that, including Gnus). Images can be included inline if you want. Also +mail markups such as *this*, **this** and _this_ have been around since +the Usenet days and displayed appropriately by a number of mailers. Like +others have said, most html messages that I have seen either contains +useless information, or are plain spam and can introduce a host of +security problems in some mailers. + +Charles + + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v2.0.4-svn0 (GNU/Linux) + +iD8DBQFIn8gm3epPyyKbwPYRApbvAKDRirXwzMzI+NHV77+QcP3EgTPaCgCfb/6m +GtNVKdYAeftaYm1nwRVoCDA= +=ULo3 +-----END PGP SIGNATURE----- +--=-=-=-- + + + diff --git a/lib/testdir/cur/1220863087.12663_19.mindcrime!2,S b/lib/testdir/cur/1220863087.12663_19.mindcrime!2,S new file mode 100644 index 0000000..78efa2a --- /dev/null +++ b/lib/testdir/cur/1220863087.12663_19.mindcrime!2,S @@ -0,0 +1,77 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:08 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008 + 13:40:29 -0700 (PDT) +Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed, + 06 Aug 2008 13:40:28 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008 + 13:40:28 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400 +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Wed, 6 Aug 2008 20:38:35 +0100 +Message-ID: <r6bpm5-6n6.ln1@news.ducksburg.com> +References: <55dbm5-qcl.ln1@news.ducksburg.com> + <mailman.15710.1217599959.18990.help-gnu-emacs@gnu.org> +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv +X-Orig-Path: news.ducksburg.com!news +Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94= + sha1:oepBoM0tJBLN52DotWmBBvW5wbg= +User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy) +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail +Xref: news.stanford.edu gnu.emacs.help:160868 +To: help-gnu-emacs@gnu.org +Subject: Re: Learning LISP; Scheme vs elisp. +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 417 +Lines: 11 + +On 2008-08-01, Thien-Thi Nguyen wrote: + +> warriors attack, felling foe after foe, +> few growing old til they realize: to know +> what deceit is worth deflection; +> such receipt reversed rejection! +> then their heavy arms, e'er transformed to shields: +> balanced hooked charms, ploughed deep, rich yields. + +Aha: the exercise for the reader is to place the parens correctly. +Might take me a while to solve this puzzle. + diff --git a/lib/testdir/cur/1220863087.12663_5.mindcrime!2,S b/lib/testdir/cur/1220863087.12663_5.mindcrime!2,S new file mode 100644 index 0000000..de46cc8 --- /dev/null +++ b/lib/testdir/cur/1220863087.12663_5.mindcrime!2,S @@ -0,0 +1,84 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 32F276963F + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:34 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:34 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs89397wfy; Mon, 4 Aug 2008 02:41:16 + -0700 (PDT) +Received: by 10.150.156.20 with SMTP id d20mr963580ybe.104.1217842875596; Mon, + 04 Aug 2008 02:41:15 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 6si3605185ywi.1.2008.08.04.02.40.57; Mon, 04 Aug 2008 02:41:15 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 7147F11C45; Mon, 4 Aug 2008 05:40:55 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from relay00.pair.com (relay00.pair.com [209.68.5.9]) by sqlite.org + (Postfix) with SMTP id B5F901192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug + 2008 05:40:52 -0400 (EDT) +Received: (qmail 59961 invoked from network); 4 Aug 2008 09:40:50 -0000 +Received: from unknown (HELO ?192.168.0.17?) (unknown) by unknown with SMTP; 4 + Aug 2008 09:40:50 -0000 +X-pair-Authenticated: 87.13.75.164 +Message-Id: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: sqlite-dev@sqlite.org +Mime-Version: 1.0 (Apple Message framework v926) +Date: Mon, 4 Aug 2008 11:40:49 +0200 +X-Mailer: Apple Mail (2.926) +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: text/plain; charset="us-ascii" +Content-Transfer-Encoding: 7bit +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 639 + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html + +With a properly defined "instructions" array, instead of the switch +statement you can use something like: +goto * instructions[pOp->opcode]; +--- +Marco Bambini +http://www.sqlabs.net +http://www.sqlabs.net/blog/ +http://www.sqlabs.net/realsqlserver/ + + + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + diff --git a/lib/testdir/cur/1220863087.12663_7.mindcrime!2,RS b/lib/testdir/cur/1220863087.12663_7.mindcrime!2,RS new file mode 100644 index 0000000..b5c0651 --- /dev/null +++ b/lib/testdir/cur/1220863087.12663_7.mindcrime!2,RS @@ -0,0 +1,138 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 3EBAB6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:35 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:35 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs89536wfy; Mon, 4 Aug 2008 02:48:56 + -0700 (PDT) +Received: by 10.150.134.21 with SMTP id h21mr7950048ybd.181.1217843335665; + Mon, 04 Aug 2008 02:48:55 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 6si5897081ywi.1.2008.08.04.02.48.35; Mon, 04 Aug 2008 02:48:55 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id ED01611C4E; Mon, 4 Aug 2008 05:48:31 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from mx0.security.ro (mx0.security.ro [80.96.72.194]) by sqlite.org + (Postfix) with ESMTP id EB3F51192C for <sqlite-dev@sqlite.org>; Mon, 4 Aug + 2008 05:48:28 -0400 (EDT) +Received: (qmail 348 invoked from network); 4 Aug 2008 12:48:03 +0300 +Received: from dev.security.ro (HELO ?192.168.1.70?) (192.168.1.70) by + mx0.security.ro with SMTP; 4 Aug 2008 12:48:03 +0300 +Message-ID: <4896D06A.8000901@security.ro> +Date: Mon, 04 Aug 2008 12:48:26 +0300 +From: anon@example.com +User-Agent: Thunderbird 2.0.0.16 (Windows/20080708) +MIME-Version: 1.0 +To: sqlite-dev@sqlite.org +References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +In-Reply-To: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +Content-Type: multipart/mixed; boundary="------------000207070200050102060301" +X-BitDefender-Scanner: Clean, Agent: BitDefender qmail 2.0.0 on + mx0.security.ro +X-BitDefender-Spam: No (0) +X-BitDefender-SpamStamp: v1, whitelisted, total: 0 +Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: high +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 2212 + +This is a multi-part message in MIME format. +--------------000207070200050102060301 +Content-Type: text/plain; charset=ISO-8859-1; format=flowed +Content-Transfer-Encoding: 7bit + +Marco Bambini wrote: +> Inside sqlite3VdbeExec there is a very big switch statement. +> In order to increase performance with few modifications to the +> original code, why not use this technique ? +> http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html +> +> With a properly defined "instructions" array, instead of the switch +> statement you can use something like: +> goto * instructions[pOp->opcode]; +> --- +> Marco Bambini +> http://www.sqlabs.net +> http://www.sqlabs.net/blog/ +> http://www.sqlabs.net/realsqlserver/ +> +> +> +> _______________________________________________ +> sqlite-dev mailing list +> sqlite-dev@sqlite.org +> http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev +> +All the world's not a VAX. This technique is GCC-specific. The SQLite +source must be as portable as possible thus tying it to a specific +compiler is out of the question. While one could conceivably use some +preprocessor magic to provide alternate implementations, that would be +impractical considering the sheer size of the code affected. +On the other hand - perhaps you could benchmark the change and provide +some data on whether this actually improves performance? + + +--------------000207070200050102060301 +Content-Type: text/x-vcard; charset=utf-8; + name="mihailim.vcf" +Content-Transfer-Encoding: 7bit +Content-Disposition: attachment; + filename="mihailim.vcf" + +begin:vcard +fn:Mihai Limbasan +n:Limbasan;Mihai +org:SC SECPRAL COM SRL +adr:;;str. Actorului nr. 9;Cluj-Napoca;Cluj;400441;Romania +email;internet:mihailim@security.ro +title:SoftwareDeveloper +tel;work:+40 264 449579 +tel;fax:+40 264 418594 +tel;cell:+40 729 038302 +url:http://secpral.ro/ +version:2.1 +end:vcard + + +--------------000207070200050102060301 +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--------------000207070200050102060301-- + diff --git a/lib/testdir/cur/1252168370_3.14675.cthulhu!2,S b/lib/testdir/cur/1252168370_3.14675.cthulhu!2,S new file mode 100644 index 0000000..4fad706 --- /dev/null +++ b/lib/testdir/cur/1252168370_3.14675.cthulhu!2,S @@ -0,0 +1,21 @@ +Return-Path: <dfgh@floppydisk.nl> +X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime +X-Spam-Level: +Delivered-To: dfgh@floppydisk.nl +Message-ID: <43A09C49.9040902@euler.org> +Date: Wed, 14 Dec 2005 23:27:21 +0100 +From: Fred Flintstone <fred@euler.org> +User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010) +X-Accept-Language: nl-NL, nl, en +MIME-Version: 1.0 +To: dfgh@floppydisk.nl +Subject: Re: xyz +References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org> +In-Reply-To: <20051211184308.GB13513@gauss.org> +X-Enigmail-Version: 0.92.0.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-UIDL: T<?"!%LG"!cAK"!_j(#! +Content-Length: 1879 + +Test 123. diff --git a/lib/testdir/cur/1283599333.1840_11.cthulhu!2, b/lib/testdir/cur/1283599333.1840_11.cthulhu!2, new file mode 100644 index 0000000..25c7180 --- /dev/null +++ b/lib/testdir/cur/1283599333.1840_11.cthulhu!2, @@ -0,0 +1,16 @@ +From: Frodo Baggins <frodo@example.com> +To: Bilbo Baggins <bilbo@anotherexample.com> +Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +Organization: The Fellowship of the Ring +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit +Message-Id: <abcd$efgh@example.com> + + +Let's write some fünkÿ text +using umlauts. + +Foo. diff --git a/lib/testdir/cur/1305664394.2171_402.cthulhu!2, b/lib/testdir/cur/1305664394.2171_402.cthulhu!2, new file mode 100644 index 0000000..863f714 --- /dev/null +++ b/lib/testdir/cur/1305664394.2171_402.cthulhu!2, @@ -0,0 +1,17 @@ +From: =?UTF-8?B?TcO8?= <testmu@testmu.xx> +To: Helmut =?UTF-8?B?S3LDtmdlcg==?= <hk@testmu.xxx> +Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +References: <non-exist-01@msg.id> <non-exist-02@msg.id> <non-exist-03@msg.id> <non-exist-04@msg.id> +1n-Reply-To: <non-exist-04@msg.id> +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Test for issue #38, where apparently searching for accented words in subject, +to etc. fails. + +What about here? Queensrÿche. Mötley Crüe. + + diff --git a/lib/testdir/cur/encrypted!2,S b/lib/testdir/cur/encrypted!2,S new file mode 100644 index 0000000..f75fd40 --- /dev/null +++ b/lib/testdir/cur/encrypted!2,S @@ -0,0 +1,56 @@ +Return-path: <> +Envelope-to: peter@example.com +Delivery-date: Fri, 11 May 2012 16:22:03 +0300 +Received: from localhost.example.com ([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-Ux + for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300 +Delivered-To: peter@example.com +From: Brian <brian@example.com> +To: Peter <peter@example.com> +Subject: encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:21:42 +0300 +Message-ID: <!&!AAAAAAAAAYAAAAAAAAAOH1+8mkk+lLn7Gg5fke7FbCgAAAEAAAAJ7eBDgcactKhXL6r8cEnJ8BAAAAAA==@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav +gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d +Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/ +gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v +cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4 +mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy +O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz +gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR +mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym +Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc +4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP +/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l +KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy +gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf +QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref +Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d +gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9 +we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM +qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU +9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY +HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM +0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG +2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ +IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH +=0Paa +-----END PGP MESSAGE----- +--=-=-=-- diff --git a/lib/testdir/cur/multimime!2,FS b/lib/testdir/cur/multimime!2,FS new file mode 100644 index 0000000..84f85aa --- /dev/null +++ b/lib/testdir/cur/multimime!2,FS @@ -0,0 +1,27 @@ +Return-path: <> +Envelope-to: djcb@localhost +Delivery-date: Sun, 20 May 2012 09:59:51 +0300 +From: Steve Jobs <jobs@example.com> +To: Bill Gates <bg@example.com> +Subject: multimime +User-agent: mu4e 0.9.8.4; emacs 23.3.1 +Date: Sat, 19 May 2012 20:57:56 +0100 +Message-ID: <m2fwaw2baz.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +abc +--=-=-= +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test1.C" +Content-Transfer-Encoding: base64 + +aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg== +--=-=-= +Content-Type: text/plain + +def +--=-=-=-- diff --git a/lib/testdir/cur/multirecip!2,S b/lib/testdir/cur/multirecip!2,S new file mode 100644 index 0000000..c997503 --- /dev/null +++ b/lib/testdir/cur/multirecip!2,S @@ -0,0 +1,11 @@ +Date: Thu, 15 May 2016 14:57:25 -0200 +From: +To: a@example.com,b@example.com,c@example.com +Cc: d@example.com,e@example.com +Subject: test with multi to and cc +Message-id: <3BE9E652343245@emss35m06.us.lmco.com> + +Message with multi cc and to. + + + diff --git a/lib/testdir/cur/signed!2,S b/lib/testdir/cur/signed!2,S new file mode 100644 index 0000000..a2e7e21 --- /dev/null +++ b/lib/testdir/cur/signed!2,S @@ -0,0 +1,36 @@ +Return-path: <> +Envelope-to: skipio@localhost +Delivery-date: Fri, 11 May 2012 16:21:57 +0300 +Received: from localhost.roma.net([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-55 + for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300 +Delivered-To: diggler@gmail.com +From: Skipio <skipio@roma.net> +To: Hannibal <hanni@carthago.net> +Subject: signed +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:20:45 +0300 +Message-ID: <878vgy97ma.fsf@roma.net> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p +DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs +=blXz +-----END PGP SIGNATURE----- +--=-=-=-- + diff --git a/lib/testdir/cur/signed-encrypted!2,S b/lib/testdir/cur/signed-encrypted!2,S new file mode 100644 index 0000000..a3910e6 --- /dev/null +++ b/lib/testdir/cur/signed-encrypted!2,S @@ -0,0 +1,54 @@ +Return-path: <> +Envelope-to: karjala@localhost +Delivery-date: Fri, 11 May 2012 16:37:57 +0300 +From: karjala@example.com +To: lapinkulta@example.com +Subject: signed + encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:36:08 +0300 +Message-ID: <874nrm96wn.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16 +uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb +L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J +P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj +cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg +zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW +C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e +YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ +QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA +zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ +GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ +AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v +bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx +ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg +cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM +DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR +w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx +lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs +YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni +Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ +IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI +JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB +t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY +6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr +kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I +C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA= +=pv03 +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/testdir/cur/special!2,Sabc b/lib/testdir/cur/special!2,Sabc new file mode 100644 index 0000000..7f1de8e --- /dev/null +++ b/lib/testdir/cur/special!2,Sabc @@ -0,0 +1,10 @@ +Date: Thu, 1 Jun 2012 14:57:25 -0200 +From: "Rocky Balboa" <rocky@example.com> +To: "Ivan Drago" <ivan@example.com> +Subject: currying and tail optimization +Message-id: <3BE9E653ef345@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT + +Test 123. I'm a special message with special flags. diff --git a/lib/testdir/new/1220863087.12663_21.mindcrime b/lib/testdir/new/1220863087.12663_21.mindcrime new file mode 100644 index 0000000..4101716 --- /dev/null +++ b/lib/testdir/new/1220863087.12663_21.mindcrime @@ -0,0 +1,111 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 6389969CB2 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:07 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:07 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34769wfh; Wed, 6 Aug 2008 + 13:38:53 -0700 (PDT) +Received: by 10.100.6.13 with SMTP id 13mr4103508anf.83.1218055131215; Wed, 06 + Aug 2008 13:38:51 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id b32si10199298ana.34.2008.08.06.13.38.49; Wed, 06 Aug 2008 + 13:38:51 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +DomainKey-Status: good (test mode) +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; domainkeys=pass + (test mode) header.From=juanma_bellon@yahoo.es +Received: from localhost ([127.0.0.1]:55648 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpmT-0005W9-AQ for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:38:49 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KQplz-0005U5-Pk for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KQplw-0005Nw-OG for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:19 -0400 +Received: from [199.232.76.173] (port=45465 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQplw-0005NX-I6 for + help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:38:16 -0400 +Received: from n74a.bullet.mail.sp1.yahoo.com ([98.136.45.21]:29868) by + monty-python.gnu.org with smtp (Exim 4.60) (envelope-from + <juanma_bellon@yahoo.es>) id 1KQplw-0007EF-7Z for help-gnu-emacs@gnu.org; + Wed, 06 Aug 2008 16:38:16 -0400 +Received: from [216.252.122.216] by n74.bullet.mail.sp1.yahoo.com with NNFMP; + 06 Aug 2008 20:38:14 -0000 +Received: from [68.142.237.89] by t1.bullet.sp1.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +Received: from [69.147.75.180] by t5.bullet.re3.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +Received: from [127.0.0.1] by omp101.mail.re1.yahoo.com with NNFMP; 06 Aug + 2008 20:38:14 -0000 +X-Yahoo-Newman-Id: 778995.62909.bm@omp101.mail.re1.yahoo.com +Received: (qmail 43643 invoked from network); 6 Aug 2008 20:38:14 -0000 +DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws; s=s1024; d=yahoo.es; + h=Received:X-YMail-OSG:X-Yahoo-Newman-Property:From:To:Subject:Date:User-Agent:References:In-Reply-To:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-Disposition:Message-Id; + b=ThdHlND5CNUsLPGuk+XhCWkdUA9w7lg4hiAgx8F8egsmQteMpwUlV/Y5tfe6K3O2jzHjtsklkzWqm7WY3VAcxxD/QgxLnianK5ZQHoelDAiGaFRqu8Y42XMZso2ccCBFWUQaKo9C+KIfa3e3ci73qehVxTtmr7bxLjurcSYEBPo= + ; +Received: from unknown (HELO 212251170160.customer.cdi.no) + (juanma_bellon@212.251.170.160 with plain) by smtp109.plus.mail.re1.yahoo.com + with SMTP; 6 Aug 2008 20:38:14 -0000 +X-YMail-OSG: k86L54kVM1kiZbUlYx7gayoBrCLYMFIRDL.KJLBKetNucAbwU4RjeeE1vhjw33hREaUig0CCjG7BTwIfbeZZpRmUcHbxl6gR0z6Sd3lYqA-- +X-Yahoo-Newman-Property: ymail-3 +From: anon@example.com +To: help-gnu-emacs@gnu.org +Date: Wed, 6 Aug 2008 22:38:15 +0200 +User-Agent: KMail/1.9.6 (enterprise 0.20070907.709405) +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> +In-Reply-To: <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> +MIME-Version: 1.0 +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: quoted-printable +Content-Disposition: inline +Message-Id: <200808062238.15634.juanma_bellon@yahoo.es> +X-detected-kernel: by monty-python.gnu.org: FreeBSD 6.x (1) +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 361 + +On Thursday 31 July 2008, Xah wrote: +> what's the logic of =E2=80=9COK=E2=80=9D? + +=46or all I know, it comes from "0 Knock-outs" (from USA civil war times, +IIRC), i.e., all went really well. + +But this is really off-topic. +=2D-=20 +Juanma + +"Having a smoking section in a restaurant is like + having a peeing section in a swimming pool." + -- Edward Burr + + + + + diff --git a/lib/testdir/new/1220863087.12663_23.mindcrime b/lib/testdir/new/1220863087.12663_23.mindcrime new file mode 100644 index 0000000..ca46f2b --- /dev/null +++ b/lib/testdir/new/1220863087.12663_23.mindcrime @@ -0,0 +1,105 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C3EF069CB3 + for <xxxx@localhost>; Thu, 7 Aug 2008 08:10:10 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Thu, 07 Aug 2008 08:10:10 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs35153wfh; Wed, 6 Aug 2008 + 13:58:17 -0700 (PDT) +Received: by 10.100.166.10 with SMTP id o10mr4182182ane.0.1218056296101; Wed, + 06 Aug 2008 13:58:16 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d34si13875743and.3.2008.08.06.13.58.14; Wed, 06 Aug 2008 + 13:58:16 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org; dkim=pass (test + mode) header.i=@gmail.com +Received: from localhost ([127.0.0.1]:33418 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQq5G-0001aY-Cr for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:58:14 -0400 +Received: from mailman by lists.gnu.org with tmda-scanned (Exim 4.43) id + 1KQq4n-0001Z9-06 for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:45 -0400 +Received: from exim by lists.gnu.org with spam-scanned (Exim 4.43) id + 1KQq4l-0001V8-6c for help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:44 -0400 +Received: from [199.232.76.173] (port=46438 helo=monty-python.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQq4k-0001Un-V2 for + help-gnu-emacs@gnu.org; Wed, 06 Aug 2008 16:57:42 -0400 +Received: from ik-out-1112.google.com ([66.249.90.180]:17562) by + monty-python.gnu.org with esmtp (Exim 4.60) (envelope-from + <lekktu@gmail.com>) id 1KQq4k-0001fk-OW for help-gnu-emacs@gnu.org; Wed, 06 + Aug 2008 16:57:42 -0400 +Received: by ik-out-1112.google.com with SMTP id c21so94956ika.2 for + <help-gnu-emacs@gnu.org>; Wed, 06 Aug 2008 13:57:41 -0700 (PDT) +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma; + h=domainkey-signature:received:received:message-id:date:from:to + :subject:cc:in-reply-to:mime-version:content-type + :content-transfer-encoding:content-disposition:references; + bh=TTNY9749hpg1+TXOwdaCr+zbQGhBUt3IvsjLWp+pxp0=; + b=BOfudUT/SiW9V4e9+k3dXDzwm+ogdrq4m5OlO+f1H+oE6OAYGIm8dbdqDAOwUewBoS + jRpfZo07YamP9rkko79SeFdQnf7UAPFAw9x7DFCm3x6muSlCcJBR7vYs1rgHOSINAn2B + vQx2//lKR4fXfKNURNu+B30KrvoEmw6m2C8dI= +DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma; + h=message-id:date:from:to:subject:cc:in-reply-to:mime-version + :content-type:content-transfer-encoding:content-disposition :references; + b=UMDBulH/LwxDywEH0pfK3DbJ4u2kIZCVDLIM++PqrdcR82HjcS/O3Jhf5OFrf7Fnyj + GH76xmc7zkTG/3aQy2WY6DeWCJaFarEItmhxy3h/xS+kUKeDARzNox0OzK6lIv/u9bdy + f2LnFlYRJ7Q5vy3lxpxAWB4v0qCwtF9LjWFg4= +Received: by 10.210.47.7 with SMTP id u7mr3100239ebu.30.1218056261587; Wed, 06 + Aug 2008 13:57:41 -0700 (PDT) +Received: by 10.210.71.14 with HTTP; Wed, 6 Aug 2008 13:57:41 -0700 (PDT) +Message-ID: <f7ccd24b0808061357t453f5962w8b61f9a453b684d0@mail.gmail.com> +Date: Wed, 6 Aug 2008 22:57:41 +0200 +From: anon@example.com +To: Juanma <juanma_bellon@yahoo.es> +In-Reply-To: <200808062238.15634.juanma_bellon@yahoo.es> +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> +X-detected-kernel: by monty-python.gnu.org: Linux 2.6 (newer, 2) +Cc: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 309 + +On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote: + +> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +> IIRC), i.e., all went really well. + +See http://en.wikipedia.org/wiki/Okay#Etymology + +"0 knock-outs" is among the "Improbable or refuted etymologies". + + Juanma + + diff --git a/lib/testdir/new/1220863087.12663_25.mindcrime b/lib/testdir/new/1220863087.12663_25.mindcrime new file mode 100644 index 0000000..588ace1 --- /dev/null +++ b/lib/testdir/new/1220863087.12663_25.mindcrime @@ -0,0 +1,98 @@ +Return-Path: <help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5 + for <xxxx@localhost>; Fri, 8 Aug 2008 20:56:25 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008 + 07:40:46 -0700 (PDT) +Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri, + 08 Aug 2008 07:40:46 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008 + 07:40:46 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for + xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400 +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Fri, 08 Aug 2008 10:07:30 -0400 +Organization: PANIX Public Access Internet and UNIX, NYC +Message-ID: <uwsireh25.fsf@one.dot.net> +References: <mailman.15123.1216681940.18990.help-gnu-emacs@gnu.org> + <mailman.15143.1216715014.18990.help-gnu-emacs@gnu.org> + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> + <mailman.15958.1218056266.18990.help-gnu-emacs@gnu.org> +NNTP-Posting-Host: panix5.panix.com +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19 + GMT) +X-Complaints-To: abuse@panix.com +NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC) +User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt) +Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs= +Xref: news.stanford.edu gnu.emacs.help:160963 +To: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor <help-gnu-emacs.gnu.org> +List-Unsubscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=unsubscribe> +List-Archive: <http://lists.gnu.org/pipermail/help-gnu-emacs> +List-Post: <mailto:help-gnu-emacs@gnu.org> +List-Help: <mailto:help-gnu-emacs-request@gnu.org?subject=help> +List-Subscribe: <http://lists.gnu.org/mailman/listinfo/help-gnu-emacs>, + <mailto:help-gnu-emacs-request@gnu.org?subject=subscribe> +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 710 +Lines: 27 + +I seem to remember from my early school days it was a campaign slogan +for someone nick-named Kinderhook that went something like + +Old Kinderhook is OK + +- Chris + +"Juanma Barranquero" <lekktu@gmail.com> writes: + +> On Wed, Aug 6, 2008 at 22:38, Juanma <juanma_bellon@yahoo.es> wrote: +> +>> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +>> IIRC), i.e., all went really well. +> +> See http://en.wikipedia.org/wiki/Okay#Etymology +> +> "0 knock-outs" is among the "Improbable or refuted etymologies". +> +> Juanma +> +> + +-- + (. .) + =ooO=(_)=Ooo===================================== + Chris McMahan | first_initiallastname@one.dot.net + ================================================= + diff --git a/lib/testdir/new/1220863087.12663_9.mindcrime b/lib/testdir/new/1220863087.12663_9.mindcrime new file mode 100644 index 0000000..734ee35 --- /dev/null +++ b/lib/testdir/new/1220863087.12663_9.mindcrime @@ -0,0 +1,209 @@ +Return-Path: <sqlite-dev-bounces@sqlite.org> +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-1.2 required=3.0 tests=BAYES_00,HTML_MESSAGE, + MIME_QP_LONG_LINE autolearn=no version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 4E3CF6963B + for <xxxx@localhost>; Mon, 4 Aug 2008 21:49:37 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for <xxxx@localhost> (single-drop); Mon, 04 Aug 2008 21:49:37 +0300 (EEST) +Received: by 10.142.51.12 with SMTP id y12cs94317wfy; Mon, 4 Aug 2008 05:48:28 + -0700 (PDT) +Received: by 10.150.152.17 with SMTP id z17mr1245909ybd.194.1217854107583; + Mon, 04 Aug 2008 05:48:27 -0700 (PDT) +Received: from sqlite.org (sqlite.org [67.18.92.124]) by mx.google.com with + ESMTP id 9si6334793yws.5.2008.08.04.05.47.57; Mon, 04 Aug 2008 05:48:27 -0700 + (PDT) +Received-SPF: pass (google.com: best guess record for domain of + sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as permitted sender) + client-ip=67.18.92.124; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record + for domain of sqlite-dev-bounces@sqlite.org designates 67.18.92.124 as + permitted sender) smtp.mail=sqlite-dev-bounces@sqlite.org +Received: from sqlite.org (localhost [127.0.0.1]) by sqlite.org (Postfix) with + ESMTP id 4FBC111C6F; Mon, 4 Aug 2008 08:47:54 -0400 (EDT) +X-Original-To: sqlite-dev@sqlite.org +Delivered-To: sqlite-dev@sqlite.org +Received: from cpsmtpo-eml02.kpnxchange.com (cpsmtpo-eml02.kpnxchange.com + [213.75.38.151]) by sqlite.org (Postfix) with ESMTP id AA4F111C10 for + <sqlite-dev@sqlite.org>; Mon, 4 Aug 2008 08:47:51 -0400 (EDT) +Received: from hpsmtp-eml21.kpnxchange.com ([213.75.38.121]) by + cpsmtpo-eml02.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 + Aug 2008 14:47:50 +0200 +Received: from cpbrm-eml13.kpnsp.local ([195.121.247.250]) by + hpsmtp-eml21.kpnxchange.com with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 + Aug 2008 14:47:50 +0200 +Received: from hpsmtp-eml30.kpnxchange.com ([10.94.53.250]) by + cpbrm-eml13.kpnsp.local with Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug + 2008 14:47:50 +0200 +Received: from localhost ([10.94.53.250]) by hpsmtp-eml30.kpnxchange.com with + Microsoft SMTPSVC(6.0.3790.1830); Mon, 4 Aug 2008 14:47:49 +0200 +Content-class: urn:content-classes:message +MIME-Version: 1.0 +X-MimeOLE: Produced By Microsoft Exchange V6.5 +Date: Mon, 4 Aug 2008 14:46:06 +0200 +Message-ID: <F687EC042917A94E8BB4B0902946453AE17D6C@CPEXBE-EML18.kpnsp.local> +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +Thread-Topic: [sqlite-dev] VM optimization inside sqlite3VdbeExec +Thread-Index: Acj2FjkWvteFtLHTTYeVz4ES7E2ggAAGRxeI +References: <83B5AF40-DBFA-4578-A043-04C80276E195@sqlabs.net> +From: anon@example.com +To: <sqlite-dev@sqlite.org> +X-OriginalArrivalTime: 04 Aug 2008 12:47:49.0650 (UTC) + FILETIME=[4D577720:01C8F630] +Subject: Re: [sqlite-dev] VM optimization inside sqlite3VdbeExec +X-BeenThere: sqlite-dev@sqlite.org +X-Mailman-Version: 2.1.9 +Precedence: list +Reply-To: sqlite-dev@sqlite.org +List-Id: <sqlite-dev.sqlite.org> +List-Unsubscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=unsubscribe> +List-Archive: <http://sqlite.org:8080/cgi-bin/mailman/private/sqlite-dev> +List-Post: <mailto:sqlite-dev@sqlite.org> +List-Help: <mailto:sqlite-dev-request@sqlite.org?subject=help> +List-Subscribe: <http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>, + <mailto:sqlite-dev-request@sqlite.org?subject=subscribe> +Content-Type: multipart/mixed; boundary="===============1911358387==" +Mime-version: 1.0 +Sender: sqlite-dev-bounces@sqlite.org +Errors-To: sqlite-dev-bounces@sqlite.org +Content-Length: 5318 + +This is a multi-part message in MIME format. + +--===============1911358387== +Content-class: urn:content-classes:message +Content-Type: multipart/alternative; + boundary="----_=_NextPart_001_01C8F630.0FC2EC1E" + +This is a multi-part message in MIME format. + +------_=_NextPart_001_01C8F630.0FC2EC1E +Content-Type: text/plain; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +Actually, almost every C compiler will already do what you suggest: if = +the range of case labels is compact, the switch will be compiled using a = +jump table. Only if the range is limited and/or sparse other techniques = +will be used, such as linear search and binary search. +=20 +I'm pretty sure, if you perform the tests suggested by Mihai, that you = +will find zero performance difference, neither better, nor worse. +=20 +Paul +=20 +________________________________ + +From: anon@example.com +Sent: Mon 8/4/2008 11:40 AM +To: sqlite-dev@sqlite.org +Subject: [sqlite-dev] VM optimization inside sqlite3VdbeExec + + + +Inside sqlite3VdbeExec there is a very big switch statement. +In order to increase performance with few modifications to the=20 +original code, why not use this technique ? +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html = +<http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html>=20 + +With a properly defined "instructions" array, instead of the switch=20 +statement you can use something like: +goto * instructions[pOp->opcode]; +--- +Marco Bambini +http://www.sqlabs.net <http://www.sqlabs.net/>=20 +http://www.sqlabs.net/blog/ <http://www.sqlabs.net/blog/>=20 +http://www.sqlabs.net/realsqlserver/ = +<http://www.sqlabs.net/realsqlserver/>=20 + + + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev = +<http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev>=20 + + + +------_=_NextPart_001_01C8F630.0FC2EC1E +Content-Type: text/html; + charset="iso-8859-1" +Content-Transfer-Encoding: quoted-printable + +<HTML dir=3Dltr><HEAD><TITLE>[sqlite-dev] VM optimization inside = +sqlite3VdbeExec=0A= +=0A= +=0A= +=0A= +
=0A= +
Actually, = +almost every C compiler will already do what you suggest: if the range = +of case labels is compact, the switch will be compiled using a jump = +table. Only if the range is limited and/or sparse other techniques will = +be used, such as linear search and binary search.
=0A= +
 
=0A= +
I'm pretty sure, if you = +perform the tests suggested by Mihai, that you will find zero = +performance difference, neither better, nor worse.
=0A= +
 
=0A= +
Paul
=0A= +
 
=0A= +
=0A= +
=0A= +
=0A= +
From: = +sqlite-dev-bounces@sqlite.org on behalf of Marco Bambini
Sent: = +Mon 8/4/2008 11:40 AM
To: = +sqlite-dev@sqlite.org
Subject: [sqlite-dev] VM optimization = +inside sqlite3VdbeExec

=0A= +
=0A= +

Inside sqlite3VdbeExec there is a very = +big switch statement.
In order to increase performance with few = +modifications to the 
original code, why not use this technique = +?
= +http://docs.freebsd.org/info/gcc/gcc.info.Labels_as_Values.html<= +/FONT>

With a properly defined = +"instructions" array, instead of the switch 
statement you can = +use something like:
goto * = +instructions[pOp->opcode];
---
Marco Bambini
http://www.sqlabs.net
http://www.sqlabs.net/blog/
http://www.sqlabs.net/realsqlserver/



<= +FONT face=3DArial = +size=3D2>_______________________________________________
sqlite-dev = +mailing list
sqlite-dev@sqlite.org
http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev

+------_=_NextPart_001_01C8F630.0FC2EC1E-- + + +--===============1911358387== +Content-Type: text/plain; charset="us-ascii" +MIME-Version: 1.0 +Content-Transfer-Encoding: 7bit +Content-Disposition: inline + +_______________________________________________ +sqlite-dev mailing list +sqlite-dev@sqlite.org +http://sqlite.org:8080/cgi-bin/mailman/listinfo/sqlite-dev + +--===============1911358387==-- + diff --git a/lib/testdir/tmp/1220863087.12663.ignore b/lib/testdir/tmp/1220863087.12663.ignore new file mode 100644 index 0000000..588ace1 --- /dev/null +++ b/lib/testdir/tmp/1220863087.12663.ignore @@ -0,0 +1,98 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-3.6 required=3.0 tests=BAYES_00,RCVD_IN_DNSWL_LOW, + SPF_PASS autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id D68E769CB5 + for ; Fri, 8 Aug 2008 20:56:25 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [72.14.221.111] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Fri, 08 Aug 2008 20:56:25 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs71287wfh; Fri, 8 Aug 2008 + 07:40:46 -0700 (PDT) +Received: by 10.100.122.8 with SMTP id u8mr3824321anc.77.1218206446062; Fri, + 08 Aug 2008 07:40:46 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d35si2718351and.38.2008.08.08.07.40.45; Fri, 08 Aug 2008 + 07:40:46 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:47349 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KRT93-0006Po-A3 for + xxxx.klub@gmail.com; Fri, 08 Aug 2008 10:40:45 -0400 +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!news-out.readnews.com!news-xxxfer.readnews.com!panix!not-for-mail +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Fri, 08 Aug 2008 10:07:30 -0400 +Organization: PANIX Public Access Internet and UNIX, NYC +Message-ID: +References: + + <9bc17528-8ea9-49f7-8e9d-07f5ede91415@p31g2000prf.googlegroups.com> + <200808062238.15634.juanma_bellon@yahoo.es> + +NNTP-Posting-Host: panix5.panix.com +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +X-Trace: reader1.panix.com 1218204439 22850 166.84.1.5 (8 Aug 2008 14:07:19 + GMT) +X-Complaints-To: abuse@panix.com +NNTP-Posting-Date: Fri, 8 Aug 2008 14:07:19 +0000 (UTC) +User-Agent: Gnus/5.11 (Gnus v5.11) Emacs/22.2 (windows-nt) +Cancel-Lock: sha1:Ckkp5oJPIMuAVgEHGnS/9MkZsEs= +Xref: news.stanford.edu gnu.emacs.help:160963 +To: help-gnu-emacs@gnu.org +Subject: Re: basic question: going back to dired +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 710 +Lines: 27 + +I seem to remember from my early school days it was a campaign slogan +for someone nick-named Kinderhook that went something like + +Old Kinderhook is OK + +- Chris + +"Juanma Barranquero" writes: + +> On Wed, Aug 6, 2008 at 22:38, Juanma wrote: +> +>> For all I know, it comes from "0 Knock-outs" (from USA civil war times, +>> IIRC), i.e., all went really well. +> +> See http://en.wikipedia.org/wiki/Okay#Etymology +> +> "0 knock-outs" is among the "Improbable or refuted etymologies". +> +> Juanma +> +> + +-- + (. .) + =ooO=(_)=Ooo===================================== + Chris McMahan | first_initiallastname@one.dot.net + ================================================= + diff --git a/lib/testdir2/Foo/cur/arto.eml b/lib/testdir2/Foo/cur/arto.eml new file mode 100644 index 0000000..ffa0526 --- /dev/null +++ b/lib/testdir2/Foo/cur/arto.eml @@ -0,0 +1,448 @@ +Return-Path: <> +X-Original-To: f00f@localhost +Delivered-To: f00f@localhost +Received: from puppet (puppet [127.0.0.1]) + by f00fmachines.nl (Postfix) with ESMTP id A534D39C7F1 + for ; Mon, 23 May 2011 20:30:05 +0300 (EEST) +Delivered-To: diggler@gmail.com +Received: from ew-in-f109.1e100.net [174.15.27.101] + by puppet with POP3 (fetchmail-6.3.18) + for (single-drop); Mon, 23 May 2011 20:30:05 +0300 (EEST) +Received: by 10.142.147.13 with SMTP id u13cs87252wfd; + Mon, 23 May 2011 01:54:10 -0700 (PDT) +Received: by 10.204.7.74 with SMTP id c10mr1984197bkc.104.1306140849326; + Mon, 23 May 2011 01:54:09 -0700 (PDT) +Received: from MTX4.mbn1.net (mtx4.mbn1.net [213.188.129.252]) + by mx.google.com with ESMTP id e6si18117551bkw.39.2011.05.23.01.54.07; + Mon, 23 May 2011 01:54:08 -0700 (PDT) +Received-SPF: pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) client-ip=213.188.129.252; +Authentication-Results: mx.google.com; spf=pass (google.com: best guess record for domain of MTX4.mbn1.net designates 213.188.129.252 as permitted sender) smtp.mail= +Resent-From: +X-Default-Received-SPF: pass (skip=forwardok (res=PASS)) x-ip-name=192.168.10.123; +From: ArtOlive +To: "f00f@f00fmachines.nl" +Reply-To: +Date: Mon, 23 May 2011 10:53:45 +0200 +Subject: NIEUWSBRIEF ART OLIVE | juni exposite in galerie ArtOlive +MIME-Version: 1.0 +Content-Type: multipart/alternative; + boundary="_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d" +X-Mailer: aspNetEmail ver 3.5.2.10 +X-Sender: 87.93.13.24 +X-RemoteIP: 87.93.13.24 +Originating-IP: 87.93.13.24 +X-MAILINGLIJST-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl> +Message-ID: <10374608.109906.11909.2011523105345.MSGID@mailinglijst.nl> +X-Authenticated-User: guest@mailinglijst.eu +X-STA-Metric: 0 (engine=030) +X-STA-NotSpam: geinformeerd vormen spec:usig:3.8.2 twee samen +X-STA-Spam: 2011 &bull • e-mailing subject:juni +X-BTI-AntiSpam: score:0,sta:0/030,dnsbl:passed,sw:passed,bsn:10/passed,spf:off,bsctr:passed/1,dk:off,pbmf:none,ipr:0/3,trusted:no,ts:no,bs:no,ubl:passed +X-Auto-Response-Suppress: DR, RN, NRN, OOF, AutoReply +Resent-Message-Id: <19740414233016.EB6835A132F5FCF4@MTX4.mbn1.net> +Resent-Date: Mon, 23 May 2011 10:54:07 +0200 (CEST) + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d +Content-Type: text/plain; charset="iso-8859-15" +Content-Transfer-Encoding: quoted-printable + +ART-O-NEWS; juni 2011 Westergasfabriekterrein Polonceaukade 17 10= +14 DA Amsterdam tel: 020-6758504 info@artolive.nl www.artolive.nlJuni= + expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers + + +Zondag 5 juni + Elke maand vindt er in de galerie van ArtOlive een expositie plaats. We = +lichten enkele kunstenaars uit (die je misschien al kent van onze website= +), waarbij we een spannende mix van materiaal en techniek presenteren. Ti= +jdens de expositie staan we klaar om elke vraag over ons kunstaanbod te b= +eantwoorden.=20 + +De exposities zijn te bezoeken van maandag t/m vrijdag, tussen 10:00 en 1= +7:00 uur. De opening is altijd op de eerste zondag van de maand. Dit valt= + samen met de Sunday Market die elke maand op het Cultuurpark Westergasfa= +briek georganiseerd wordt. De Sunday Market is gratis te bezoeken en staa= +t vol met kunst, design, mode en heerlijke hapjes, en er hangt altijd een= + vrolijke sfeer. Een ideaal moment dus om in te haken en deze maand twee = +kunstenaars te presenteren: Peter van den Akker en Marinel Vieleers. + +We verwelkomen je graag op zondag 5 juni 2011, van 12:00 t/m 17:00 uur op= + de Polonceaukade 17 van het Cultuurpark Westergasfabriek in Amsterdam!=20= + + + + bekijk meer werk op www.artolive.nl... Peter van den Akker + + +"In mijn beelden en schilderijen staat het mensbeeld centraal; niet als i= +ndividu maar als universele gestalte, waarbij ik op transparante wijze ti= +jdsbeelden en gelaagdheid in het menselijke handelen naar voren breng. Ve= +rhoudingen tussen mensen, verschuivingen in wereldculturen en verandering= +en in techniek, architectuur, natuur en mensbeeld vormen mijn inspiratieb= +ronnen. Het zijn allemaal beelden en sferen die naast en met elkaar besta= +an. Mijn werkwijze omvat vele technieken in verschillende materialen: sch= +ilderijen, gemengde technieken op papier/collages, zeefdrukken, beelden i= +n cortenstaal, keramische objecten." + +Peter van den Akker exposeert regelmatig in binnen- en buitenland bij gal= +erie=EBn en musea en is in verschillende kunstinstellingen en bedrijfscol= +lecties opgenomen. + + + lees meer over Peter... Marinel Vieleers + + +Marinel Vieleers probeert het menselijke in de bouwwerken - en ook vaak i= +ets van het karakter van de bouwer of bewoner - te laten zien. Het zijn m= +aar subtiele details die dat alles weergeven. + +De 'tand des tijds' of invloed van mensen op de gebouwen spelen vaak mee = +in het werk. Koper, cement, lood en andere materialen worden in haar nieu= +we werk gebruikt. Op deze manier kan ze gemakkelijker improviseren en nog= + directer op haar gevoel afgaan. + +Marinel is gefascineerd door de schoonheid van het imperfecte. De gelaagd= +heid van ouderdom, het verval. De imperfectie die ontstaat door toevallig= +e omstandigheden maakt een huis, een muur, een schutting, hout of steen b= +oeiend. Het is doorleefd en het toont een stukje van zijn geschiedenis. + + + lees meer over Marinel... =20 +ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met Marinel Viel= +eers en Peter van den Akker + +Opening op zondag 5 mei, tijdens de Sunday Market op het Cultuurpark West= +ergasfabriek in Amsterdam. Je bent van harte uitgenodigd om tussen 12:00 = +en 17:00 uur langs te komen! + +Daarna is de expositie te zien op werkdagen (ma - vrij) tussen 10:00 en 1= +7:00. De expositie duurt tot 24 juni 2011. + wil je niet langer door artolive ge=EFnformeerd worden? Klik dan hier om= + je af te melden.=20 + kreeg je dit mailtje doorgestuurd en wil je voortaan zelf ook graag de n= +ieuwsbrief ontvangen?=20 + klik dan hier om je aan te melden.=20 + +Deze e-mailing is verzorgd met MailingLijst + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d +Content-Type: text/html; charset="iso-8859-15" +Content-Transfer-Encoding: quoted-printable + + + + + + Artolive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
3D"artolive"
ART-O-NEWS • juni 2011 +
Westergasfabriekterrein   Polonceau= +kade 17 1014 DA Amsterdam   tel: 020-6758504  info@artolive.nl<= +/a>  www.artolive.nl
+ + + + + + + +
Juni= + expositie bij ArtOlive: Peter van den Akker en Marinel Vieleers +

Zondag 5 juni
+ Elke maand vindt er in de galerie van Art= +Olive een expositie plaats. We lichten enkele kunstenaars uit (die je mis= +schien al kent van onze website), waarbij we een spannende mix van materi= +aal en techniek presenteren. Tijdens de expositie staan we klaar om elke = +vraag over ons kunstaanbod te beantwoorden.

+

De exposities zijn te bezoeken van maa= +ndag t/m vrijdag, tussen 10:00 en 17:00 uur. De opening is altijd op de e= +erste zondag van de maand. Dit valt samen met de Sunday Market die elke m= +aand op het Cultuurpark Westergasfabriek georganiseerd wordt. De Sunday M= +arket is gratis te bezoeken en staat vol met kunst, design, mode en heerl= +ijke hapjes, en er hangt altijd een vrolijke sfeer. Een ideaal moment dus= + om in te haken en deze maand twee kunstenaars te presenteren: Peter van = +den Akker en Marinel Vieleers.

+

We verwelkomen je graag op zondag 5 ju= +ni 2011, van 12:00 t/m 17:00 uur op de Polonceaukade 17 van het Cultuurpa= +rk Westergasfabriek in Amsterdam!

+
+

3D""  = +bekijk= + meer werk op www.artolive.nl...   

+
+
+ + + + = + + + + + + +
3D""
+ + + + + + + + + + +
<= +a target=3D"_blank" href=3D"http://www.mailinglijst.eu/redirect.aspx?l=3D= +154043&a=3D10374608&t=3DH"> <= +/td> + Peter van den Akker
+

"In mijn beelden en schild= +erijen staat het mensbeeld centraal; niet als individu maar als universel= +e gestalte, waarbij ik op transparante wijze tijdsbeelden en gelaagdheid = +in het menselijke handelen naar voren breng. Verhoudingen tussen mensen, = +verschuivingen in wereldculturen en veranderingen in techniek, architectu= +ur, natuur en mensbeeld vormen mijn inspiratiebronnen. Het zijn allemaal = +beelden en sferen die naast en met elkaar bestaan. Mijn werkwijze omvat v= +ele technieken in verschillende materialen: schilderijen, gemengde techni= +eken op papier/collages, zeefdrukken, beelden in cortenstaal, keramische = +objecten.”

+

Peter van den Akker expose= +ert regelmatig in binnen- en buitenland bij galerieën en musea en is= + in verschillende kunstinstellingen en bedrijfscollecties opgenomen.

+
+

3D""  = +lees meer over Peter...   

+
= + Marinel Vieleers
+

Marinel Vieleers probeert = +het menselijke in de bouwwerken - en ook vaak iets van het karakter van d= +e bouwer of bewoner - te laten zien. Het zijn maar subtiele details die d= +at alles weergeven.

+

De ‘tand des tijds&r= +squo; of invloed van mensen op de gebouwen spelen vaak mee in het werk. K= +oper, cement, lood en andere materialen worden in haar nieuwe werk gebrui= +kt. Op deze manier kan ze gemakkelijker improviseren en nog directer op h= +aar gevoel afgaan.

+

Marinel is gefascineerd do= +or de schoonheid van het imperfecte. De gelaagdheid van ouderdom, het ver= +val. De imperfectie die ontstaat door toevallige omstandigheden maakt een= + huis, een muur, een schutting, hout of steen boeiend. Het is doorleefd e= +n het toont een stukje van zijn geschiedenis.

+
+

3D""  = +lees meer ov= +er Marinel...   

+
+
+
+ + + + + + + + + +
3D""
+ + + + + + + + + + + +
+
+
+ + + + + + + + + +
3D""
+ + + + + + + + + + + + + + + +

+
ZONDAG 5 MEI - Juni expositie in de galerie van ArtOlive met = +Marinel Vieleers en Peter van den Akker
+

+
Opening op zondag 5 mei, = +tijdens de Sunday Market op het Cultuurpark Westergasfabriek in Amsterdam= +. Je bent van harte uitgenodigd om tussen 12:00 en 17:00 uur langs te kom= +en!
+

+
Daarna is de expositie te zien op werkdagen (ma - vrij= +) tussen 10:00 en 17:00. De expositie duurt tot 24 juni 2011.
+
+
+
3D"Kunst wil je niet langer door artolive geïnformeerd worden? Klik= + dan hier om je af te melden.
+ kreeg je dit mailtje doorgestuurd en wil je voortaan = +zelf ook graag de nieuwsbrief ontvangen?
+ klik dan hier om= + je aan te melden.
+

<= +HR SIZE=3D1 STYLE=3D"COLOR:#d3d3d3" SIZE=3D1>Deze e-mailing is verzorgd m= +et MailingLijst

+ + +--_=aspNetEmail=_5ed4592191214c7a99bd7f6a3a0f077d-- diff --git a/lib/testdir2/Foo/cur/fraiche.eml b/lib/testdir2/Foo/cur/fraiche.eml new file mode 100644 index 0000000..c0bf442 --- /dev/null +++ b/lib/testdir2/Foo/cur/fraiche.eml @@ -0,0 +1,10 @@ +From: Sender +To: Recip +Subject: search accents +Date: 2012-12-08 00:48 +MIME-Version: 1.0 +Content-Type: text/plain; charset=utf-8 +Content-Transfer-Encoding: 8bit + +line 1: Глокая куздра штеко будланула бокра и курдячит бокрёнка +line 2: crème fraîche diff --git a/lib/testdir2/Foo/cur/mail5 b/lib/testdir2/Foo/cur/mail5 new file mode 100644 index 0000000..b72195d --- /dev/null +++ b/lib/testdir2/Foo/cur/mail5 @@ -0,0 +1,625 @@ +From: Sitting Bull +To: George Custer +Subject: pics for you +Mail-Reply-To: djcb@djcbsoftware.nl +User-Agent: Hunkpapa/2.15.9 (Almost Unreal) +Message-Id: CAHSaMxZ9rk5ASjqsbXizjTQuSk583=M6TORHz=bfogtmbGGs5A@mail.gmail.com +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: multipart/mixed; + boundary="Multipart_Sun_Oct_17_10:37:40_2010-1" + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: text/plain; charset=US-ASCII + +Dude! Here are some pics! + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="sittingbull.jpg" +Content-Transfer-Encoding: base64 + +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 +KD//2Q== + + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="custer.jpg" +Content-Transfer-Encoding: base64 + +/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB +AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC +CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox +MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA +//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA +ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g +ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk +LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED +EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B +AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD +REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB +AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR +B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW +V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC +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 +pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k +A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw +213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe +FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9 +PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo +pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth +eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+ +tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr +P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj +3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5 +OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL +7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz +I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw +LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog +ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm +Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v +bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu +Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50 +cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg +eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0 +YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv +YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6 +WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS +ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm +OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4 +bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5 +LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy +OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8 +L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg +ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG +BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF +BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e +Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E +AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl +M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA +AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET +2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z +TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww +RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q +KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL +Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8 +IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd +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 + +Cheerio! + +--Multipart_Sun_Oct_17_10:37:40_2010-1-- diff --git a/lib/testdir2/Foo/new/.noindex b/lib/testdir2/Foo/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir2/Foo/tmp/.noindex b/lib/testdir2/Foo/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir2/bar/cur/181736.eml b/lib/testdir2/bar/cur/181736.eml new file mode 100644 index 0000000..56255c4 --- /dev/null +++ b/lib/testdir2/bar/cur/181736.eml @@ -0,0 +1,42 @@ +Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail +X-newsreader: xrn 9.03-beta-14-64bit +Sender: jimbo@lews (Jimbo Foobarcuux) +From: jimbo@slp53.sl.home (Jimbo Foobarcuux) +Reply-To: slp53@pacbell.net +Subject: Re: Are writes "atomic" to readers of the file? +Newsgroups: comp.unix.programmer +References: <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk> +Organization: UseNetServer - www.usenetserver.com +X-Complaints-To: abuse@usenetserver.com +Message-ID: +Date: 08 Mar 2011 17:04:20 GMT +Lines: 27 +Xref: uutiset.elisa.fi comp.unix.programmer:181736 + +John Denver writes: +>Eric the Red wrote: +> +>>> There _IS_ a requirement that all reads and writes to regular files +>>> be atomic. There is also an ordering guarantee. Any implementation +>>> that doesn't provide both atomicity and ordering guarantees is broken. +>> +>> But where is it specified? +> +>The place where it is stated most explicitly is in XSH7 2.9.7 +>Thread Interactions with Regular File Operations: +> +> All of the following functions shall be atomic with respect to each +> other in the effects specified in POSIX.1-2008 when they operate on +> regular files or symbolic links: +> +> [List of functions that includes read() and write()] +> +> If two threads each call one of these functions, each call shall +> either see all of the specified effects of the other call, or none +> of them. +> + +And, for the purposes of this paragraph, the two threads need not be +part of the same process. + +jimbo diff --git a/lib/testdir2/bar/cur/mail1 b/lib/testdir2/bar/cur/mail1 new file mode 100644 index 0000000..56808c6 --- /dev/null +++ b/lib/testdir2/bar/cur/mail1 @@ -0,0 +1,38 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "John Milton" +Subject: Fere libenter homines id quod volunt credunt +To: "Julius Caesar" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +x-label: Paradise losT +X-Keywords: milton,john +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +OF Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, [ 5 ] +Sing Heav'nly Muse,that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed, +In the Beginning how the Heav'ns and Earth +Rose out of Chaos: Or if Sion Hill [ 10 ] +Delight thee more, and Siloa's Brook that flow'd +Fast by the Oracle of God; I thence +Invoke thy aid to my adventrous Song, +That with no middle flight intends to soar +Above th' Aonian Mount, while it pursues [ 15 ] +Things unattempted yet in Prose or Rhime. +And chiefly Thou O Spirit, that dost prefer +Before all Temples th' upright heart and pure, +Instruct me, for Thou know'st; Thou from the first +Wast present, and with mighty wings outspread [ 20 ] +Dove-like satst brooding on the vast Abyss +And mad'st it pregnant: What in me is dark +Illumin, what is low raise and support; +That to the highth of this great Argument +I may assert Eternal Providence, [ 25 ] +And justifie the wayes of God to men. diff --git a/lib/testdir2/bar/cur/mail2 b/lib/testdir2/bar/cur/mail2 new file mode 100644 index 0000000..3799f30 --- /dev/null +++ b/lib/testdir2/bar/cur/mail2 @@ -0,0 +1,14 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Socrates" +Subject: cool stuff +To: "Alcibiades" +Message-id: <3BE9E6535E0D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +The hour of departure has arrived, and we go our ways—I to die, and you to +live. Which is better God only knows. + +http-emacs diff --git a/lib/testdir2/bar/cur/mail3 b/lib/testdir2/bar/cur/mail3 new file mode 100644 index 0000000..646365e --- /dev/null +++ b/lib/testdir2/bar/cur/mail3 @@ -0,0 +1,34 @@ +From: Napoleon Bonaparte +To: Edmond =?UTF-8?B?RGFudMOocw==?= +Subject: rock on dude +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le trois-mâts +le Pharaon, venant de Smyrne, Trieste et Naples. + +Comme d'habitude, un pilote côtier partit aussitôt du port, rasa le château +d'If, et alla aborder le navire entre le cap de Morgion et l'île de Rion. + +Aussitôt, comme d'habitude encore, la plate-forme du fort Saint-Jean s'était +couverte de curieux; car c'est toujours une grande affaire à Marseille que +l'arrivée d'un bâtiment, surtout quand ce bâtiment, comme le Pharaon, a été +construit, gréé, arrimé sur les chantiers de la vieille Phocée, et appartient +à un armateur de la ville. + +Cependant ce bâtiment s'avançait; il avait heureusement franchi le détroit que +quelque secousse volcanique a creusé entre l'île de Calasareigne et l'île de +Jaros; il avait doublé Pomègue, et il s'avançait sous ses trois huniers, son +grand foc et sa brigantine, mais si lentement et d'une allure si triste, que +les curieux, avec cet instinct qui pressent un malheur, se demandaient quel +accident pouvait être arrivé à bord. Néanmoins les experts en navigation +reconnaissaient que si un accident était arrivé, ce ne pouvait être au +bâtiment lui-même; car il s'avançait dans toutes les conditions d'un navire +parfaitement gouverné: son ancre était en mouillage, ses haubans de beaupré +décrochés; et près du pilote, qui s'apprêtait à diriger le Pharaon par +l'étroite entrée du port de Marseille, était un jeune homme au geste rapide et +à l'œil actif, qui surveillait chaque mouvement du navire et répétait chaque +ordre du pilote. diff --git a/lib/testdir2/bar/cur/mail4 b/lib/testdir2/bar/cur/mail4 new file mode 100644 index 0000000..4d21a48 --- /dev/null +++ b/lib/testdir2/bar/cur/mail4 @@ -0,0 +1,29 @@ +Return-Path: +Delivered-To: foo@example.com +Received: from [128.88.204.56] by freemailng0304.web.de with HTTP; + Mon, 07 May 2005 00:27:52 +0200 +Date: Mon, 07 May 2005 00:27:52 +0200 +Message-Id: <293847329847@web.de> +MIME-Version: 1.0 +From: =?iso-8859-1?Q? "=F6tzi" ?= +To: foo@example.com +Subject: =?iso-8859-1?Q?Re:=20der=20b=E4r=20und=20das=20m=E4dchen?= +Precedence: fm-user +Organization: http://freemail.web.de/ +Content-Type: text/plain; charset="iso-8859-1" +Content-Transfer-Encoding: 8bit +X-MIME-Autoconverted: from quoted-printable to 8bit by mailhost6.ladot.com id j48MScQ30791 +X-Label: \backslash +X-UIDL: 93h!!\i + +123 diff --git a/lib/testdir2/bar/cur/mail6 b/lib/testdir2/bar/cur/mail6 new file mode 100644 index 0000000..c9b799b --- /dev/null +++ b/lib/testdir2/bar/cur/mail6 @@ -0,0 +1,18 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Geoff Tate" +Subject: eyes of a stranger +To: "Enrico Fermi" +Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id> +MIME-version: 1.0 +X-label: @NextActions operation:mindcrime Queensrÿche +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +And I raise my head and stare +Into the eyes of a stranger +I've always known that the mirror never lies +People always turn away +From the eyes of a stranger +Afraid to know what +Lies behind the stare diff --git a/lib/testdir2/bar/new/.noindex b/lib/testdir2/bar/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir2/bar/tmp/.noindex b/lib/testdir2/bar/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir2/wom_bat/cur/atomic b/lib/testdir2/wom_bat/cur/atomic new file mode 100644 index 0000000..c3c6792 --- /dev/null +++ b/lib/testdir2/wom_bat/cur/atomic @@ -0,0 +1,20 @@ +Date: Sat, 12 Nov 2011 12:06:23 -0400 +From: "Richard P. Feynman" +Subject: atoms +To: "Democritus" +Message-id: <3BE9E6535E302944823E7A1A20D852173@msg.id> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +If, in some cataclysm, all scientific knowledge were to be destroyed, +and only one sentence passed on to the next generation of creatures, +what statement would contain the most information in the fewest words? +I believe it is the atomic hypothesis (or atomic fact, or whatever you +wish to call it) that all things are made of atoms — little particles +that move around in perpetual motion, attracting each other when they +are a little distance apart, but repelling upon being squeezed into +one another. In that one sentence you will see an enormous amount of +information about the world, if just a little imagination and thinking +are applied. diff --git a/lib/testdir2/wom_bat/cur/rfc822.1 b/lib/testdir2/wom_bat/cur/rfc822.1 new file mode 100644 index 0000000..71c3107 --- /dev/null +++ b/lib/testdir2/wom_bat/cur/rfc822.1 @@ -0,0 +1,44 @@ +Return-Path: +Subject: Fwd: rfc822 +From: foobar +To: martin +Content-Type: multipart/mixed; boundary="=-XHhVx/BCC6tJB87HLPqF" +Message-Id: <1077300332.871.27.camel@example.com> +Mime-Version: 1.0 +X-Mailer: Ximian Evolution 1.4.5 +Date: Fri, 20 Feb 2004 19:05:33 +0100 + +--=-XHhVx/BCC6tJB87HLPqF +Content-Type: text/plain +Content-Transfer-Encoding: 7bit + +Hello world, forwarding some RFC822 message + +--=-XHhVx/BCC6tJB87HLPqF +Content-Disposition: inline +Content-Type: message/rfc822 + +Return-Path: +Message-ID: <9A01B19D0D605D478E8B72E1367C66340141B9C5@example.com> +From: frob@example.com +To: foo@example.com +Subject: hopjesvla +Date: Sat, 13 Dec 2003 19:35:56 +0100 +MIME-Version: 1.0 +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: 7bit + +The ship drew on and had safely passed the strait, which some volcanic shock +has made between the Calasareigne and Jaros islands; had doubled Pomegue, and +approached the harbor under topsails, jib, and spanker, but so slowly and +sedately that the idlers, with that instinct which is the forerunner of evil, +asked one another what misfortune could have happened on board. However, those +experienced in navigation saw plainly that if any accident had occurred, it was +not to the vessel herself, for she bore down with all the evidence of being +skilfully handled, the anchor a-cockbill, the jib-boom guys already eased off, +and standing by the side of the pilot, who was steering the Pharaon towards the +narrow entrance of the inner port, was a young man, who, with activity and +vigilant eye, watched every motion of the ship, and repeated each direction of +the pilot. + +--=-XHhVx/BCC6tJB87HLPqF-- diff --git a/lib/testdir2/wom_bat/cur/rfc822.2 b/lib/testdir2/wom_bat/cur/rfc822.2 new file mode 100644 index 0000000..316fa3f --- /dev/null +++ b/lib/testdir2/wom_bat/cur/rfc822.2 @@ -0,0 +1,44 @@ +From: dwarf@siblings.net +To: root@eruditorum.org +Subject: Fwd: test abc +References: <8639ddr9wu.fsf@cthulhu.djcbsoftware> +User-agent: mu 0.98pre; emacs 24.0.91.9 +Date: Thu, 24 Nov 2011 14:24:00 +0200 +Message-ID: <861usxr9nj.fsf@cthulhu.djcbsoftware> +Content-Type: multipart/mixed; boundary="=-=-=" +MIME-Version: 1.0 + +--=-=-= +Content-Type: text/plain + +Saw the website. Am willing to stipulate that you are not RIST 9E03. Suspect +that you are the Dentist, who yearns for honest exchange of views. Anonymous, +digitally signed e-mail is the only safe vehicle for same. + +If you want me to believe you are not the Dentist, provide plausible +explanation for your question regarding why we are building the Crypt. + +Yours truly, + +--=-=-= +Content-Type: message/rfc822 +Content-Disposition: inline; filename= + "1322137188_3.11919.foo:2,S" +Content-Description: rfc822 + +From: dwarf@siblings.net +To: root@eruditorum.org +Subject: test abc +User-agent: mu 0.98pre; emacs 24.0.91.9 +Date: Thu, 24 Nov 2011 14:18:25 +0200 +Message-ID: <8639ddr9wu.fsf@cthulhu.djcbsoftware> +Content-Type: text/plain +MIME-Version: 1.0 + +As I stepped on this unknown middle-aged Filipina's feet during an ill-advised +ballroom dancing foray, she leaned close to me and uttered some latitude and +longitude figures with a conspicuously large number of significant digits of +precision, implying a maximum positional error on the order of the size of a +dinner plate. Gosh, was I ever curious! + +--=-=-=-- diff --git a/lib/testdir3/cycle/cur/cycle0 b/lib/testdir3/cycle/cur/cycle0 new file mode 100644 index 0000000..67c08ee --- /dev/null +++ b/lib/testdir3/cycle/cur/cycle0 @@ -0,0 +1,7 @@ +From: foo@example.com +To: bar@example.com +Subject: cycle0 +Message-Id: +Date: Tue, 21 Jun 2011 11:00 +0000 + +def diff --git a/lib/testdir3/cycle/cur/cycle0.0 b/lib/testdir3/cycle/cur/cycle0.0 new file mode 100644 index 0000000..3222a2c --- /dev/null +++ b/lib/testdir3/cycle/cur/cycle0.0 @@ -0,0 +1,8 @@ +From: foo@example.com +To: bar@example.com +Subject: cycle0.0 +Message-Id: +References: +Date: Tue, 21 Jun 2011 12:00 +0000 + +def diff --git a/lib/testdir3/cycle/cur/cycle0.0.0 b/lib/testdir3/cycle/cur/cycle0.0.0 new file mode 100644 index 0000000..907f342 --- /dev/null +++ b/lib/testdir3/cycle/cur/cycle0.0.0 @@ -0,0 +1,8 @@ +From: foo@example.com +To: bar@example.com +Subject: cycle0.0.0 +Message-Id: +References: +Date: Tue, 21 Jun 2011 13:00 +0000 + +def diff --git a/lib/testdir3/cycle/cur/rogue0 b/lib/testdir3/cycle/cur/rogue0 new file mode 100644 index 0000000..2691070 --- /dev/null +++ b/lib/testdir3/cycle/cur/rogue0 @@ -0,0 +1,8 @@ +From: foo@example.com +To: bar@example.com +Subject: rogue0 +Message-Id: +References: +Date: Tue, 21 Jun 2011 15:00 +0000 + +def diff --git a/lib/testdir3/cycle/new/.noindex b/lib/testdir3/cycle/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/cycle/tmp/.noindex b/lib/testdir3/cycle/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/A b/lib/testdir3/sort/1st-child-promotes-thread/cur/A new file mode 100644 index 0000000..85130e8 --- /dev/null +++ b/lib/testdir3/sort/1st-child-promotes-thread/cur/A @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: A +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +A diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/B b/lib/testdir3/sort/1st-child-promotes-thread/cur/B new file mode 100644 index 0000000..254aeb7 --- /dev/null +++ b/lib/testdir3/sort/1st-child-promotes-thread/cur/B @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: B +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +B diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/C b/lib/testdir3/sort/1st-child-promotes-thread/cur/C new file mode 100644 index 0000000..60b7db5 --- /dev/null +++ b/lib/testdir3/sort/1st-child-promotes-thread/cur/C @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: C +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +C diff --git a/lib/testdir3/sort/1st-child-promotes-thread/cur/D b/lib/testdir3/sort/1st-child-promotes-thread/cur/D new file mode 100644 index 0000000..1e4861d --- /dev/null +++ b/lib/testdir3/sort/1st-child-promotes-thread/cur/D @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: D +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +D diff --git a/lib/testdir3/sort/1st-child-promotes-thread/new/.noindex b/lib/testdir3/sort/1st-child-promotes-thread/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/1st-child-promotes-thread/tmp/.noindex b/lib/testdir3/sort/1st-child-promotes-thread/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/A b/lib/testdir3/sort/2nd-child-promotes-thread/cur/A new file mode 100644 index 0000000..85130e8 --- /dev/null +++ b/lib/testdir3/sort/2nd-child-promotes-thread/cur/A @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: A +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +A diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/B b/lib/testdir3/sort/2nd-child-promotes-thread/cur/B new file mode 100644 index 0000000..254aeb7 --- /dev/null +++ b/lib/testdir3/sort/2nd-child-promotes-thread/cur/B @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: B +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +B diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/C b/lib/testdir3/sort/2nd-child-promotes-thread/cur/C new file mode 100644 index 0000000..6d1e19a --- /dev/null +++ b/lib/testdir3/sort/2nd-child-promotes-thread/cur/C @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: C +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +C diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/D b/lib/testdir3/sort/2nd-child-promotes-thread/cur/D new file mode 100644 index 0000000..de61bc1 --- /dev/null +++ b/lib/testdir3/sort/2nd-child-promotes-thread/cur/D @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: D +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +D diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/cur/E b/lib/testdir3/sort/2nd-child-promotes-thread/cur/E new file mode 100644 index 0000000..bb7f5f3 --- /dev/null +++ b/lib/testdir3/sort/2nd-child-promotes-thread/cur/E @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: E +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +E diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/new/.noindex b/lib/testdir3/sort/2nd-child-promotes-thread/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/2nd-child-promotes-thread/tmp/.noindex b/lib/testdir3/sort/2nd-child-promotes-thread/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/A b/lib/testdir3/sort/child-does-not-promote-thread/cur/A new file mode 100644 index 0000000..c59c42f --- /dev/null +++ b/lib/testdir3/sort/child-does-not-promote-thread/cur/A @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: A +Message-Id: +References: +In-reply-to: +Aate: Sat, 17 May 2014 10:00:00 +0000 + +A diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/X b/lib/testdir3/sort/child-does-not-promote-thread/cur/X new file mode 100644 index 0000000..c8ac3aa --- /dev/null +++ b/lib/testdir3/sort/child-does-not-promote-thread/cur/X @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: X +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +X diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/Y b/lib/testdir3/sort/child-does-not-promote-thread/cur/Y new file mode 100644 index 0000000..ceadc20 --- /dev/null +++ b/lib/testdir3/sort/child-does-not-promote-thread/cur/Y @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Y +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +Y diff --git a/lib/testdir3/sort/child-does-not-promote-thread/cur/Z b/lib/testdir3/sort/child-does-not-promote-thread/cur/Z new file mode 100644 index 0000000..365775b --- /dev/null +++ b/lib/testdir3/sort/child-does-not-promote-thread/cur/Z @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Z +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +Z diff --git a/lib/testdir3/sort/child-does-not-promote-thread/new/.noindex b/lib/testdir3/sort/child-does-not-promote-thread/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/child-does-not-promote-thread/tmp/.noindex b/lib/testdir3/sort/child-does-not-promote-thread/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A new file mode 100644 index 0000000..85130e8 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/A @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: A +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +A diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B new file mode 100644 index 0000000..254aeb7 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/B @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: B +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +B diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C new file mode 100644 index 0000000..6d1e19a --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/C @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: C +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +C diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D new file mode 100644 index 0000000..1e4861d --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/D @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: D +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +D diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E new file mode 100644 index 0000000..bb7f5f3 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/E @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: E +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +E diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F new file mode 100644 index 0000000..7c4275d --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/F @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: F +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +F diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G new file mode 100644 index 0000000..4849455 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-only-subthread/cur/G @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: G +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +G diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/new/.noindex b/lib/testdir3/sort/grandchild-promotes-only-subthread/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/grandchild-promotes-only-subthread/tmp/.noindex b/lib/testdir3/sort/grandchild-promotes-only-subthread/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/A b/lib/testdir3/sort/grandchild-promotes-thread/cur/A new file mode 100644 index 0000000..85130e8 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-thread/cur/A @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: A +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +A diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/B b/lib/testdir3/sort/grandchild-promotes-thread/cur/B new file mode 100644 index 0000000..254aeb7 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-thread/cur/B @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: B +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +B diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/C b/lib/testdir3/sort/grandchild-promotes-thread/cur/C new file mode 100644 index 0000000..6d1e19a --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-thread/cur/C @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: C +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +C diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/D b/lib/testdir3/sort/grandchild-promotes-thread/cur/D new file mode 100644 index 0000000..de61bc1 --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-thread/cur/D @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: D +Message-Id: +Date: Sat, 17 May 2014 10:00:00 +0000 + +D diff --git a/lib/testdir3/sort/grandchild-promotes-thread/cur/E b/lib/testdir3/sort/grandchild-promotes-thread/cur/E new file mode 100644 index 0000000..d605b4b --- /dev/null +++ b/lib/testdir3/sort/grandchild-promotes-thread/cur/E @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: E +Message-Id: +References: +In-reply-to: +Date: Sat, 17 May 2014 10:00:00 +0000 + +E diff --git a/lib/testdir3/sort/grandchild-promotes-thread/new/.noindex b/lib/testdir3/sort/grandchild-promotes-thread/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/sort/grandchild-promotes-thread/tmp/.noindex b/lib/testdir3/sort/grandchild-promotes-thread/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/tree/cur/child0.0 b/lib/testdir3/tree/cur/child0.0 new file mode 100644 index 0000000..58da7d6 --- /dev/null +++ b/lib/testdir3/tree/cur/child0.0 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 0.0 +Message-Id: +References: +In-reply-to: +Date: Tue, 21 Jun 2011 11:10 +0000 + +abc diff --git a/lib/testdir3/tree/cur/child0.1 b/lib/testdir3/tree/cur/child0.1 new file mode 100644 index 0000000..163fadb --- /dev/null +++ b/lib/testdir3/tree/cur/child0.1 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 0.1 +Message-Id: +References: +In-reply-to: +Date: Tue, 21 Jun 2011 11:20 +0000 + +abc diff --git a/lib/testdir3/tree/cur/child0.1.0 b/lib/testdir3/tree/cur/child0.1.0 new file mode 100644 index 0000000..40a9eb2 --- /dev/null +++ b/lib/testdir3/tree/cur/child0.1.0 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 0.1.0 +Message-Id: +References: +In-Reply-To: +Date: Tue, 21 Jun 2011 11:22 +0000 + +abc diff --git a/lib/testdir3/tree/cur/child2.0.0 b/lib/testdir3/tree/cur/child2.0.0 new file mode 100644 index 0000000..4e4446e --- /dev/null +++ b/lib/testdir3/tree/cur/child2.0.0 @@ -0,0 +1,12 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 2.0.0 +Message-Id: +References: +In-Reply-To: +Date: Tue, 21 Jun 2011 15:02 +0000 + +abc + +note, there's no message for 'nonexistant@msg.id', so this msg should +be promoted to level 2.0 diff --git a/lib/testdir3/tree/cur/child3.0.0.0.0 b/lib/testdir3/tree/cur/child3.0.0.0.0 new file mode 100644 index 0000000..1e2f4ab --- /dev/null +++ b/lib/testdir3/tree/cur/child3.0.0.0.0 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 3.0.0.0 +Message-Id: +References: +1n-Reply-To: +Date: Wed, 22 Jun 2011 16:33 +0000 + +abc diff --git a/lib/testdir3/tree/cur/child4.0 b/lib/testdir3/tree/cur/child4.0 new file mode 100644 index 0000000..7d0ed79 --- /dev/null +++ b/lib/testdir3/tree/cur/child4.0 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 4.0 +Message-Id: +References: +In-reply-to: +Date: Tue, 24 Jun 2011 11:10 +0000 + +abc diff --git a/lib/testdir3/tree/cur/child4.1 b/lib/testdir3/tree/cur/child4.1 new file mode 100644 index 0000000..295d1b1 --- /dev/null +++ b/lib/testdir3/tree/cur/child4.1 @@ -0,0 +1,9 @@ +From: testfrom@example.com +To: testto@example.com +Subject: Re: child 4.1 +Message-Id: +References: +In-reply-to: +Date: Tue, 24 Jun 2011 11:20 +0000 + +abc diff --git a/lib/testdir3/tree/cur/root0 b/lib/testdir3/tree/cur/root0 new file mode 100644 index 0000000..deb64bb --- /dev/null +++ b/lib/testdir3/tree/cur/root0 @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: root0 +Message-Id: +Date: Tue, 21 Jun 2011 11:00 +0000 + +abc diff --git a/lib/testdir3/tree/cur/root1 b/lib/testdir3/tree/cur/root1 new file mode 100644 index 0000000..fc3efd8 --- /dev/null +++ b/lib/testdir3/tree/cur/root1 @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: root1 +Message-Id: +Date: Tue, 21 Jun 2011 12:00 +0000 + +abc diff --git a/lib/testdir3/tree/cur/root2 b/lib/testdir3/tree/cur/root2 new file mode 100644 index 0000000..6ba2451 --- /dev/null +++ b/lib/testdir3/tree/cur/root2 @@ -0,0 +1,7 @@ +From: testfrom@example.com +To: testto@example.com +Subject: root2 +Message-Id: +Date: Tue, 21 Jun 2011 13:00 +0000 + +abc diff --git a/lib/testdir3/tree/new/.noindex b/lib/testdir3/tree/new/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir3/tree/tmp/.noindex b/lib/testdir3/tree/tmp/.noindex new file mode 100644 index 0000000..e69de29 diff --git a/lib/testdir4/1220863042.12663_1.mindcrime!2,S b/lib/testdir4/1220863042.12663_1.mindcrime!2,S new file mode 100644 index 0000000..ab1500f --- /dev/null +++ b/lib/testdir4/1220863042.12663_1.mindcrime!2,S @@ -0,0 +1,146 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-4.9 required=3.0 tests=BAYES_00,DATE_IN_PAST_96_XX, + RCVD_IN_DNSWL_MED autolearn=ham version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id 5123469CB3 + for ; Thu, 7 Aug 2008 08:10:19 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Thu, 07 Aug 2008 08:10:19 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs39272wfh; Wed, 6 Aug 2008 + 20:15:17 -0700 (PDT) +Received: by 10.65.133.8 with SMTP id k8mr2071878qbn.7.1218078916289; Wed, 06 + Aug 2008 20:15:16 -0700 (PDT) +Received: from sourceware.org (sourceware.org [209.132.176.174]) by + mx.google.com with SMTP id 28si7904461qbw.0.2008.08.06.20.15.15; Wed, 06 Aug + 2008 20:15:16 -0700 (PDT) +Received-SPF: neutral (google.com: 209.132.176.174 is neither permitted nor + denied by domain of gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + client-ip=209.132.176.174; +Authentication-Results: mx.google.com; spf=neutral (google.com: + 209.132.176.174 is neither permitted nor denied by domain of + gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org) + smtp.mail=gcc-help-return-33661-xxxx.klub=gmail.com@gcc.gnu.org +Received: (qmail 13493 invoked by alias); 7 Aug 2008 03:15:13 -0000 +Received: (qmail 13485 invoked by uid 22791); 7 Aug 2008 03:15:12 -0000 +Received: from mailgw1a.lmco.com (HELO mailgw1a.lmco.com) (192.31.106.7) + by sourceware.org (qpsmtpd/0.31) with ESMTP; Thu, 07 Aug 2008 03:14:27 +0000 +Received: from emss07g01.ems.lmco.com (relay5.ems.lmco.com [166.29.2.16])by + mailgw1a.lmco.com (LM-6) with ESMTP id m773EPZH014730for + ; Wed, 6 Aug 2008 21:14:25 -0600 (MDT) +Received: from CONVERSION2-DAEMON.lmco.com by lmco.com (PMDF V6.3-x14 #31428) + id <0K5700601NO18J@lmco.com> for gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 + 21:14:25 -0600 (MDT) +Received: from EMSS04I00.us.lmco.com ([166.17.13.135]) by lmco.com (PMDF + V6.3-x14 #31428) with ESMTP id <0K5700H5MNNWGX@lmco.com> for + gcc-help@gcc.gnu.org; Wed, 06 Aug 2008 21:14:20 -0600 (MDT) +Received: from EMSS35M06.us.lmco.com ([158.187.107.143]) by + EMSS04I00.us.lmco.com with Microsoft SMTPSVC(5.0.2195.6713); Wed, 06 Aug + 2008 23:14:20 -0400 +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "Mickey Mouse" +Subject: gcc include search order +To: "Donald Duck" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Content-class: urn:content-classes:message +Mailing-List: contact gcc-help-help@gcc.gnu.org; run by ezmlm +Precedence: klub +List-Id: +List-Unsubscribe: +List-Archive: +List-Post: +List-Help: +Sender: gcc-help-owner@gcc.gnu.org +Delivered-To: mailing list gcc-help@gcc.gnu.org +Content-Length: 3024 + + +Hi. +In my unit testing I need to change some header files (target is +vxWorks, which supports some things that the sun does not). +So, what I do is fetch the development tree, and then in a new unit test +directory I attempt to compile the unit under test. Since this is NOT +vxworks, I use sed to change some of the .h files and put them in a +./changed directory. + +When I try to compile the file, it is still using the .h file from the +original location, even though I have listed the include path for +./changed before the include path for the development tree. + +Here is a partial output from gcc using the -v option + +GNU CPP version 3.1 (cpplib) (sparc ELF) +GNU C++ version 3.1 (sparc-sun-solaris2.8) + compiled by GNU C version 3.1. +ignoring nonexistent directory "NONE/include" +#include "..." search starts here: +#include <...> search starts here: + . + changed + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/mp/interface + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common + /export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/interface + /usr/local/include/g++-v3 + /usr/local/include/g++-v3/sparc-sun-solaris2.8 + /usr/local/include/g++-v3/backward + /usr/local/include + /usr/local/lib/gcc-lib/sparc-sun-solaris2.8/3.1/include + /usr/local/sparc-sun-solaris2.8/include + /usr/include +End of search list. + +I know the changed file is correct and that the include is not working +as expected, because when I copy the file from ./changed, back into the +development tree, the compilation works as expected. + +One more bit of information. The source that I cam compiling is in +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/ap/app +And it is including files from +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +These include files should be including the files from ./changed (when +they exist) but they are ignoring the .h files in the ./changed +directory and are instead using other, unchanged files in the +/export/home4/xxx/yyyy/builds/int_rel5_latest/src/shared/common +directory. + +The gcc command line is something like + + TEST_DIR="." + + CHANGED_DIR_NAME=changed + CHANGED_FILES_DIR=${TEST_DIR}/${CHANGED_DIR_NAME} + + CICU_HEADER_FILES="-I ${AP_INTERFACE_FILES} -I ${AP_APP_FILES} -I +${SHARED_COMMON_FILES} -I ${SHARED_INTERFACE_FILES}" + + HEADERS="-I ./ -I ${CHANGED_FILES_DIR} ${CICU_HEADER_FILES}" + DEFINES="-DSUNRUN -DA10_DEBUG -DJOETEST" + + CFLAGS="-v -c -g -O1 -pipe -Wformat -Wunused -Wuninitialized -Wshadow +-Wmissing-prototypes -Wmissing-declarations" + + printf "Compiling the UUT File\n" + gcc -fprofile-arcs -ftest-coverage ${CFLAGS} ${HEADERS} ${DEFINES} +${AP_APP_FILES}/unitUnderTest.cpp + + +I hope this explanation is clear. If anyone knows how to fix the command +line so that it gets the .h files in the "changed" directory are used +instead of files in the other include directories. + +Thanks +Joe + +---------------------------------------------------- +Time Flies like an Arrow. Fruit Flies like a Banana + + diff --git a/lib/testdir4/1220863087.12663_19.mindcrime!2,S b/lib/testdir4/1220863087.12663_19.mindcrime!2,S new file mode 100644 index 0000000..78efa2a --- /dev/null +++ b/lib/testdir4/1220863087.12663_19.mindcrime!2,S @@ -0,0 +1,77 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.2.5 (2008-06-10) on mindcrime +X-Spam-Level: +X-Spam-Status: No, score=-2.6 required=3.0 tests=BAYES_00 autolearn=ham + version=3.2.5 +X-Original-To: xxxx@localhost +Delivered-To: xxxx@localhost +Received: from mindcrime (localhost [127.0.0.1]) + by mail.xxxxsoftware.nl (Postfix) with ESMTP id C4D6569CB3 + for ; Thu, 7 Aug 2008 08:10:08 +0300 (EEST) +Delivered-To: xxxx.klub@gmail.com +Received: from gmail-imap.l.google.com [66.249.91.109] + by mindcrime with IMAP (fetchmail-6.3.8) + for (single-drop); Thu, 07 Aug 2008 08:10:08 +0300 (EEST) +Received: by 10.142.237.21 with SMTP id k21cs34794wfh; Wed, 6 Aug 2008 + 13:40:29 -0700 (PDT) +Received: by 10.100.33.13 with SMTP id g13mr1093301ang.79.1218055228418; Wed, + 06 Aug 2008 13:40:28 -0700 (PDT) +Received: from lists.gnu.org (lists.gnu.org [199.232.76.165]) by mx.google.com + with ESMTP id d19si15908789and.17.2008.08.06.13.40.27; Wed, 06 Aug 2008 + 13:40:28 -0700 (PDT) +Received-SPF: pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) client-ip=199.232.76.165; +Authentication-Results: mx.google.com; spf=pass (google.com: domain of + help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org designates 199.232.76.165 + as permitted sender) + smtp.mail=help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Received: from localhost ([127.0.0.1]:56316 helo=lists.gnu.org) by + lists.gnu.org with esmtp (Exim 4.43) id 1KQpo3-0007Pc-Qk for + xxxx.klub@gmail.com; Wed, 06 Aug 2008 16:40:27 -0400 +From: anon@example.com +Newsgroups: gnu.emacs.help +Date: Wed, 6 Aug 2008 20:38:35 +0100 +Message-ID: +References: <55dbm5-qcl.ln1@news.ducksburg.com> + +Mime-Version: 1.0 +Content-Type: text/plain; charset=us-ascii +Content-Transfer-Encoding: 7bit +X-Trace: individual.net bABVU1hcJwWAuRwe/097AAoOXnGGeYR8G1In635iFGIyfDLPUv +X-Orig-Path: news.ducksburg.com!news +Cancel-Lock: sha1:wK7dsPRpNiVxpL/SfvmNzlvUR94= + sha1:oepBoM0tJBLN52DotWmBBvW5wbg= +User-Agent: slrn/pre0.9.9-120/mm/ao (Ubuntu Hardy) +Path: news.stanford.edu!headwall.stanford.edu!newshub.sdsu.edu!feeder.erje.net!proxad.net!feeder1-2.proxad.net!feed.ac-versailles.fr!fu-berlin.de!uni-berlin.de!individual.net!not-for-mail +Xref: news.stanford.edu gnu.emacs.help:160868 +To: help-gnu-emacs@gnu.org +Subject: Re: Learning LISP; Scheme vs elisp. +X-BeenThere: help-gnu-emacs@gnu.org +X-Mailman-Version: 2.1.5 +Precedence: list +List-Id: Users list for the GNU Emacs text editor +List-Unsubscribe: , + +List-Archive: +List-Post: +List-Help: +List-Subscribe: , + +Sender: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Errors-To: help-gnu-emacs-bounces+xxxx.klub=gmail.com@gnu.org +Content-Length: 417 +Lines: 11 + +On 2008-08-01, Thien-Thi Nguyen wrote: + +> warriors attack, felling foe after foe, +> few growing old til they realize: to know +> what deceit is worth deflection; +> such receipt reversed rejection! +> then their heavy arms, e'er transformed to shields: +> balanced hooked charms, ploughed deep, rich yields. + +Aha: the exercise for the reader is to place the parens correctly. +Might take me a while to solve this puzzle. + diff --git a/lib/testdir4/1252168370_3.14675.cthulhu!2,S b/lib/testdir4/1252168370_3.14675.cthulhu!2,S new file mode 100644 index 0000000..1e69622 --- /dev/null +++ b/lib/testdir4/1252168370_3.14675.cthulhu!2,S @@ -0,0 +1,22 @@ +Return-Path: +X-Spam-Checker-Version: SpamAssassin 3.1.0 (2005-09-13) on mindcrime +X-Spam-Level: +Delivered-To: dfgh@floppydisk.nl +Message-ID: <43A09C49.9040902@euler.org> +Date: Wed, 14 Dec 2005 23:27:21 +0100 +From: Fred Flintstone +User-Agent: Mozilla Thunderbird 1.0.7 (X11/20051010) +X-Accept-Language: nl-NL, nl, en +MIME-Version: 1.0 +To: dfgh@floppydisk.nl +List-Id: =?utf-8?q?Example_of_List_Id?= +Subject: Re: xyz +References: <439C1136.90504@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439B41ED.2080402@euler.org> <4399DD94.5070309@euler.org> <20051209233303.GA13812@gauss.org> <439A1E03.3090604@euler.org> <20051211184308.GB13513@gauss.org> +In-Reply-To: <20051211184308.GB13513@gauss.org> +X-Enigmail-Version: 0.92.0.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 7bit +X-UIDL: T +To: Bilbo Baggins +Subject: Greetings from =?UTF-8?B?TG90aGzDs3JpZW4=?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +Fcc: .sent +Organization: The Fellowship of the Ring +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Let's write some fünkÿ text +using umlauts. + +Foo. diff --git a/lib/testdir4/1305664394.2171_402.cthulhu!2, b/lib/testdir4/1305664394.2171_402.cthulhu!2, new file mode 100644 index 0000000..863f714 --- /dev/null +++ b/lib/testdir4/1305664394.2171_402.cthulhu!2, @@ -0,0 +1,17 @@ +From: =?UTF-8?B?TcO8?= +To: Helmut =?UTF-8?B?S3LDtmdlcg==?= +Subject: =?UTF-8?B?TW90w7ZyaGVhZA==?= +User-Agent: Wanderlust/2.15.9 (Almost Unreal) Emacs/24.0 Mule/6.0 (HANACHIRUSATO) +References: +1n-Reply-To: +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + + +Test for issue #38, where apparently searching for accented words in subject, +to etc. fails. + +What about here? Queensrÿche. Mötley Crüe. + + diff --git a/lib/testdir4/181736.eml b/lib/testdir4/181736.eml new file mode 100644 index 0000000..56255c4 --- /dev/null +++ b/lib/testdir4/181736.eml @@ -0,0 +1,42 @@ +Path: uutiset.elisa.fi!feeder2.news.elisa.fi!feeder.erje.net!newsfeed.kamp.net!newsfeed0.kamp.net!nx02.iad01.newshosting.com!newshosting.com!post01.iad!not-for-mail +X-newsreader: xrn 9.03-beta-14-64bit +Sender: jimbo@lews (Jimbo Foobarcuux) +From: jimbo@slp53.sl.home (Jimbo Foobarcuux) +Reply-To: slp53@pacbell.net +Subject: Re: Are writes "atomic" to readers of the file? +Newsgroups: comp.unix.programmer +References: <87hbblwelr.fsf@sapphire.mobileactivedefense.com> <8762s0jreh.fsf@sapphire.mobileactivedefense.com> <87hbbjc5jt.fsf@sapphire.mobileactivedefense.com> <8ioh48-8mu.ln1@leafnode-msgid.gclare.org.uk> +Organization: UseNetServer - www.usenetserver.com +X-Complaints-To: abuse@usenetserver.com +Message-ID: +Date: 08 Mar 2011 17:04:20 GMT +Lines: 27 +Xref: uutiset.elisa.fi comp.unix.programmer:181736 + +John Denver writes: +>Eric the Red wrote: +> +>>> There _IS_ a requirement that all reads and writes to regular files +>>> be atomic. There is also an ordering guarantee. Any implementation +>>> that doesn't provide both atomicity and ordering guarantees is broken. +>> +>> But where is it specified? +> +>The place where it is stated most explicitly is in XSH7 2.9.7 +>Thread Interactions with Regular File Operations: +> +> All of the following functions shall be atomic with respect to each +> other in the effects specified in POSIX.1-2008 when they operate on +> regular files or symbolic links: +> +> [List of functions that includes read() and write()] +> +> If two threads each call one of these functions, each call shall +> either see all of the specified effects of the other call, or none +> of them. +> + +And, for the purposes of this paragraph, the two threads need not be +part of the same process. + +jimbo diff --git a/lib/testdir4/encrypted!2,S b/lib/testdir4/encrypted!2,S new file mode 100644 index 0000000..b6470e7 --- /dev/null +++ b/lib/testdir4/encrypted!2,S @@ -0,0 +1,57 @@ +Return-path: <> +Envelope-to: peter@example.com +Delivery-date: Fri, 11 May 2012 16:22:03 +0300 +Received: from localhost.example.com ([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-Ux + for djcb@localhost; Fri, 11 May 2012 16:21:58 +0300 +Delivered-To: peter@example.com +From: Brian +To: Peter +Subject: encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:21:42 +0300 +Message-ID: <877gwi97kp.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA//YXkUB4Dy09ngCRyHWbXmV3XBjuKTr8xrak5ML1kwurav +gyagOHKLMU+5CKvObChiKtXhtgU0od7IC8o+ALlHevQ0XXcqNYA2KUfX8R7akq7d +Xx9mA6D8P7Y/P8juUCLBpfrCi2GC42DtvPZSUu3bL/ctUJ3InPHIfHibKF2HMm7/ +gUHAKY8VPJF39dLP8GLcfki6qFdeWbxgtzmuyzHfCBCLnDL0J9vpEQBpGDFMcc4v +cCbmMJaiPOmRb6U4WOuRVnuXuTztLiIn0jMslzOSFDcLTVBAsrC01r71O+XZKfN4 +mIfcpcWJYKM2NQW8Jwf+8Hr84uznBqs8uTTlrmppjkAHZGqGMjiQDxLhDVaCQzMy +O8PSV4xT6HPlKXOwV1OLc+vm0A0RAdSBctgZg40oFn4XdB1ur8edwAkLvc0hJKaz +gyTQiPaXm2Uh2cDeEx4xNgXmwCKasqc9jAlnDC2QwA33+pw3OqgZT5h1obn0fAeR +mgB+iW1503DIi/96p8HLZcr2EswLEH9ViHIEaFj/vlR5BaOncsLB0SsNV/MHRvym +Xg5GUjzPIiyBZ3KaR9OIBiZ5eXw+bSrPAo/CAs0Zwxag7W3CH//oK39Qo1GnkYpc +4IQxhx4IwkzqtCnripltV/kfpGu0yA/OdK8lOjkUqCwvL97o73utXIxm21Zd3mEP +/iLNrduZjMCq+goz1pDAQa9Dez6VjwRuRPTqeAac8Fx/nzrVzIoIEAt36hpuaH1l +KpbmHpKgsUWcrE5iYT0RRlRRtRF4PfJg8PUmP1hvw8TaEmNfT+0HgzcJB/gRsVdy +gTzkzUDzGZLhRcpmM5eW4BkuUmIO7625pM6Jd3HOGyfCGSXyEZGYYeVKzv8xbzYf +QM6YYKooRN9Ya2jdcWguW0sCSJO/RZ9eaORpTeOba2+Fp6w5L7lga+XM9GLfgref +Cf39XX1RsmRBsrJTw0z5COf4bT8G3/IfQP0QyKWIFITiFjGmpZhLsKQ3KT4vSe/d +gTY1xViVhkjvMFn3cgSOSrvktQpAhsXx0IRazN0T7pTU33a5K0SrZajY9ynFDIw9 +we7XYyVwZzYEXjGih5mTH1PhWYK5fZZEKKqaz5TyYv9SeWJ+8FrHeXUKD38SQEHM +qkpl9Iv17RF4Qy9uASWwRoobhKO+GykTaBSTyw8R8ctG/hfAlnaZxQ3TwNyHWyvU +9SVJsp27ulv/W9MLZtGpEMK0ckAR164Vyou1KOn200BqxbC2tJpegNeD2TP5ZtdY +HIcxkgKr0haYcDnVEf1ulSxv23pZWIexbgvVCG7dRL0eB+6O28f9CWehle10MDyM +0AYyw8Da2cu7PONMovqt4nayScyGTacFBp7c2KXR9DGZ0mcBwOjL/mGRKcVWN3MG +2auCrwn2KVWmKZI3Jp0T8KhfGBnFs9lUElpDTOiED1/2bKz6Yoc385QtWx99DFMZ +IWiH5wMxkWFpzjE+GHiJ09vSbTTL4JY9eu2n5nxQmtjYMBVxQm7S7qwH +=0Paa +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/testdir4/mail1 b/lib/testdir4/mail1 new file mode 100644 index 0000000..a4e19c1 --- /dev/null +++ b/lib/testdir4/mail1 @@ -0,0 +1,38 @@ +Date: Thu, 31 Jul 2008 14:57:25 -0400 +From: "John Milton" +Subject: Fere libenter homines id quod volunt credunt +To: "Julius Caesar" +Message-id: <3BE9E6535E3029448670913581E7A1A20D852173@emss35m06.us.lmco.com> +MIME-version: 1.0 +x-label: Paradise losT +X-keywords: john, milton +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT +Precedence: high + +OF Mans First Disobedience, and the Fruit +Of that Forbidden Tree, whose mortal tast +Brought Death into the World, and all our woe, +With loss of Eden, till one greater Man +Restore us, and regain the blissful Seat, [ 5 ] +Sing Heav'nly Muse,that on the secret top +Of Oreb, or of Sinai, didst inspire +That Shepherd, who first taught the chosen Seed, +In the Beginning how the Heav'ns and Earth +Rose out of Chaos: Or if Sion Hill [ 10 ] +Delight thee more, and Siloa's Brook that flow'd +Fast by the Oracle of God; I thence +Invoke thy aid to my adventrous Song, +That with no middle flight intends to soar +Above th' Aonian Mount, while it pursues [ 15 ] +Things unattempted yet in Prose or Rhime. +And chiefly Thou O Spirit, that dost prefer +Before all Temples th' upright heart and pure, +Instruct me, for Thou know'st; Thou from the first +Wast present, and with mighty wings outspread [ 20 ] +Dove-like satst brooding on the vast Abyss +And mad'st it pregnant: What in me is dark +Illumin, what is low raise and support; +That to the highth of this great Argument +I may assert Eternal Providence, [ 25 ] +And justifie the wayes of God to men. diff --git a/lib/testdir4/mail5 b/lib/testdir4/mail5 new file mode 100644 index 0000000..b12387a --- /dev/null +++ b/lib/testdir4/mail5 @@ -0,0 +1,624 @@ +From: Sitting Bull +To: George Custer +Subject: pics for you +Mail-Reply-To: djcb@djcbsoftware.nl +User-Agent: Hunkpapa/2.15.9 (Almost Unreal) +Fcc: .sent +MIME-Version: 1.0 (generated by SEMI 1.14.6 - "Maruoka") +Content-Type: multipart/mixed; + boundary="Multipart_Sun_Oct_17_10:37:40_2010-1" + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: text/plain; charset=US-ASCII + +Dude! Here are some pics! + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="sittingbull.jpg" +Content-Transfer-Encoding: base64 + +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 +KD//2Q== + + + +--Multipart_Sun_Oct_17_10:37:40_2010-1 +Content-Type: image/jpeg +Content-Disposition: inline; filename="custer.jpg" +Content-Transfer-Encoding: base64 + +/9j/4AAQSkZJRgABAQAAAQABAAD/4Q1kRXhpZgAASUkqAAgAAAAIABIBCQABAAAAAQAAABoBCQAB +AAAAyAAAABsBBQABAAAAbgAAACgBAwABAAAAAgAAADEBAgAOAAAAdgAAADIBAgAUAAAAhAAAABMC +CQABAAAAAQAAAGmHBAABAAAAmAAAAOYAAADIAAAAAQAAAGd0aHVtYiAyLjExLjMAMjAwNTowMTox +MCAwMDo1NzowMwAGAACQBwAEAAAAMDIyMQGRBwAEAAAAAQIDAACgBwAEAAAAMDEwMAGgAwABAAAA +//8AAAKgCQABAAAAyAAAAAOgCQABAAAA9gAAAAAAAAAGAAMBAwABAAAABgAAABoBCQABAAAASAAA +ABsBCQABAAAASAAAACgBCQABAAAAAgAAAAECBAABAAAANAEAAAICBAABAAAAJwwAAAAAAAD/2P/g +ABBKRklGAAEBAAABAAEAAP/bAEMACAYGBwYFCAcHBwkJCAoMFA0MCwsMGRITDxQdGh8eHRocHCAk +LicgIiwjHBwoNyksMDE0NDQfJzk9ODI8LjM0Mv/bAEMBCQkJDAsMGA0NGDIhHCEyMjIyMjIyMjIy +MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMv/AABEIAIAAaAMBIgACEQED +EQH/xAAfAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgv/xAC1EAACAQMDAgQDBQUEBAAAAX0B +AgMABBEFEiExQQYTUWEHInEUMoGRoQgjQrHBFVLR8CQzYnKCCQoWFxgZGiUmJygpKjQ1Njc4OTpD +REVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmq +srO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4eLj5OXm5+jp6vHy8/T19vf4+fr/xAAfAQADAQEB +AQEBAQEBAAAAAAAAAQIDBAUGBwgJCgv/xAC1EQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFR +B2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVW +V1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrC +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 +pAznLa7juUCSEJMvr3qZwydRx6isi4ty3zJw1TWksjrs3kOOoJpiL0twwXlCwHoKxri6WFj8pG/k +A1fe6vIODtKmu1+Gc9vcazNHeRxyZh+RXjVuc8nJ5oA4zwxp91rmpSW1nGGnZMZbgAZ5JP5V6Cvw +213Jb7TYgbcKPMbr/wB816pGsESARpGi+iqAKT7VCPlMi5+tAHisPwc8SLJk39hH/tpI+R/47XWe +FvBeqeGtHnhlENxO8hlYRSH5vQDIHP1xXoSzow4ZT9DSvIO1AHiepeMNUWaWCBPsgDFWDrl8jsc9 +PpXMajqdxe3BuLt90zAAsABkDp0rq/ixpTWeuwanbDC3kZDr/trwT+IIrziS6uUcJcqGWgDpPDOo +pb+ILctJxITGQffp+uKKyNFtjc67YxqdyNMrfgDk/wAqKljR64vB5rK8Q6DaapCtxI0qXC7I0Kth +eXAGR3+8fzrTDYNQavKRol465DpEZFI6gryP1FJMbPMEcqxRhytNkwrLInDDriqST/ay3z4lB3A+ +tSrIWQq3DjqDVEl9h9oiDIC2e1V4H1KxvEuIGe2KHPmZxtqkbuazYtC5FVJ767vG/eyEr6dKAPQr +P4k6tZRHcy3KKeGlXJI98VuQ+OdM1EB3nubN3IASWM4Y+gIzXksN4YcB49yjsank1L7Q4ZvlVfuj +3oC57To2vQX8jJBdJujO0hnCnP0ODXWxC6dBuLL6ZFeP2/izwp4hsIodejmtNREflvdRICjnszY5 +OepGOtcvc6vqeh3Ij0jXbnyifl8mZth+maLDPU/i9Cf+EOt53YB4rtcHvypBx+leILdgDbICwrqL +7TvHPiVo7fUTdTxp8y+dIAg9+uM/rXS6D8PbDTQs+pst5cjkJ/yzU/T+L8fyp3AyfAmiXHn/ANrz +I0duqkQhurk8Z+mM0V6BI+FCqAFHAA6CiobA/9kA/+EMRWh0dHA6Ly9ucy5hZG9iZS5jb20veGFw +LzEuMC8APD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQi +Pz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUg +NC40LjAtRXhpdjIiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5 +LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgog +ICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6dGlm +Zj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICB4bWxuczpleGlmPSJodHRwOi8v +bnMuYWRvYmUuY29tL2V4aWYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUu +Y29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50 +cy8xLjEvIgogICB4bXA6Q3JlYXRlRGF0ZT0iMjAwNS0wMS0xMFQwMDowNzoyNyswMTowMCIKICAg +eG1wOk1vZGlmeURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpNZXRhZGF0 +YURhdGU9IjIwMDUtMDEtMTBUMDA6NTc6MDMrMDE6MDAiCiAgIHhtcDpDcmVhdG9yVG9vbD0iQWRv +YmUgUGhvdG9zaG9wIENTIFdpbmRvd3MiCiAgIHRpZmY6T3JpZW50YXRpb249IjEiCiAgIHRpZmY6 +WFJlc29sdXRpb249IjIwMC8xIgogICB0aWZmOllSZXNvbHV0aW9uPSIyMDAvMSIKICAgdGlmZjpS +ZXNvbHV0aW9uVW5pdD0iMiIKICAgZXhpZjpDb2xvclNwYWNlPSI0Mjk0OTY3Mjk1IgogICBleGlm +OlBpeGVsWERpbWVuc2lvbj0iNzU1IgogICBleGlmOlBpeGVsWURpbWVuc2lvbj0iOTMwIgogICB4 +bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6Zjg2ZTcwZTQtNjI5OC0xMWQ5 +LTllM2YtZDQyZjM0NjM5ZGJiIgogICB4bXBNTTpJbnN0YW5jZUlEPSJ1dWlkOmY4NmU3MGU1LTYy +OTgtMTFkOS05ZTNmLWQ0MmYzNDYzOWRiYiIKICAgZGM6Zm9ybWF0PSJpbWFnZS9qcGVnIi8+CiA8 +L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +IAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAog +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg +ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAg +ICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz7/2wBDAAUDBAQEAwUEBAQFBQUG +BwwIBwcHBw8LCwkMEQ8SEhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/2wBDAQUF +BQcGBw4ICA4eFBEUHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4e +Hh4eHh7/wAARCAD2AMgDASIAAhEBAxEB/8QAHQAAAAcBAQEAAAAAAAAAAAAAAAIDBAUGBwEICf/E +AEIQAAIBAwIEBAQDBgUBCAMBAAECAwAEEQUhBhIxQQcTUWEicYGRFDKhI0JSscHRCBVicoLwFiQl +M5KissJDU9Lh/8QAGQEBAQEBAQEAAAAAAAAAAAAAAAECAwQF/8QAGhEBAQEBAQEBAAAAAAAAAAAA +AAERAjEhEv/aAAwDAQACEQMRAD8Am8Hl60dEGN+tAbnalEBIr5+PUIoGcmllXPSgkW9LxxntVBET +2pVY/Y0tFEe+KcpFRDYRDHSjrD/pp4sdHWIUwMHhwOlJcpXr0qVeEEd6aXCxxRtLK6xou7MxwB8z +TDSSpjelAtQFxxvwjaSmGXW7dnH/AOtWkH3UEUeLjjhGXATV0z7wyD/61cNTTDbG1AKcdKjBxXww +RznWbUA9Mkj+lSem3+n6lE0mnXtvdopwzQyBgp9DjpTDXGQ0mY6fFD6Vzy8DpTDTMR7UOT3p0U9q +KF3oaasm1F5PSnTr3pMAb1FJrGwGRRuX1o64xvRtqYG5X2oyp7UpgV1agCKAQSKFKZHLQozUEoNL +Rj3oBd+lLxJsKrTsaEmnccW+d65Ehz0pzGp9KuI5HHv0pdU7Yrsa+1LqmSBitSAsabdKrXiLxfb8 +IabG6xR3V/O4EVsXweXuxx2/maX4w410PhmJknlWe7A2gRhkf7j2/nWCaxxK9/qs+ohea6mbJuJd +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 + +Cheerio! + +--Multipart_Sun_Oct_17_10:37:40_2010-1-- diff --git a/lib/testdir4/multimime!2,FS b/lib/testdir4/multimime!2,FS new file mode 100644 index 0000000..84f85aa --- /dev/null +++ b/lib/testdir4/multimime!2,FS @@ -0,0 +1,27 @@ +Return-path: <> +Envelope-to: djcb@localhost +Delivery-date: Sun, 20 May 2012 09:59:51 +0300 +From: Steve Jobs +To: Bill Gates +Subject: multimime +User-agent: mu4e 0.9.8.4; emacs 23.3.1 +Date: Sat, 19 May 2012 20:57:56 +0100 +Message-ID: +MIME-Version: 1.0 +Content-Type: multipart/mixed; boundary="=-=-=" + +--=-=-= +Content-Type: text/plain + +abc +--=-=-= +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="test1.C" +Content-Transfer-Encoding: base64 + +aGVyZSBpcyBhIHNpbXBsZSB0ZXN0IGZpbGUuCg== +--=-=-= +Content-Type: text/plain + +def +--=-=-=-- diff --git a/lib/testdir4/signed!2,S b/lib/testdir4/signed!2,S new file mode 100644 index 0000000..7e1319a --- /dev/null +++ b/lib/testdir4/signed!2,S @@ -0,0 +1,36 @@ +User-agent: mu4e 1.1.0; emacs 27.0.50 +From: Skipio +To: Hannibal +Subject: test 123 +Date: Sun, 24 Mar 2019 11:50:42 +0200 +Message-ID: <87zhpky51p.fsf@djcbsoftware.nl> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; + micalg=pgp-sha256; protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! + +--=-=-= +Content-Type: application/pgp-signature; name="signature.asc" + +-----BEGIN PGP SIGNATURE----- + +iQIzBAEBCAAdFiEEaYec7RdFk3UPFNqYEd3+qdzEoDYFAlyXUwAACgkQEd3+qdzE +oDbjdw//dAosaEyqSfyUMXjS++iJEeDIwKwO6AjEI0xCbJjHmxq93PA61ApE/BS3 +d/sKa1dsfN+plRS+Fh3NNGSA7evar9dXtMBUr6hwL0VTmm5NDwedaPeuW6mgyVcB +VNUn5x1e/QdnSClapnGd156sryfcM1pg/667fTHT6WC01Xe0sezpkV9l0j4pslYt +y6ud/Hejszax+NcwQY7vkCcVWfB9K4zbiapdoCjHi78S4YAcsbd//KmePOqn04Sa +Tg1XsmMzIh7L/3njkJdIOd9XctTwYEcN5geY1QKrHQ/3+gBeaEYvwsvrnqnVKqMY +WCg/aYibuXl+xNkPMcKHIj1dXA3m5MkL77RrxODiAYz0YkiQx1/DLZs8PV3IVoB4 +f0GGDqyiOwSmSDa4iuCottwO4yG1WM1i7r6pir22qAekIt43wSdwakOrT1IkS8q2 +o0VGiQtEPy27D+ufiw06t02Ryf20Q7i2YcueZxYeRBq41m11M41DJ4wH7LQcJsww +qG5iBOdwQFCTWpi1UrbbFjlxXXWvKMuIU+4k7nsamrEL4SDXmq1v13vtlcgJ6vnn +v7c9+MF7laqdfI+BYnlD1v/9LosPbFTm0hPdvK4yIOORp8Iwj/1PGzTOz6SCUxzA +kDu+Y+NN9/SM1ppStg1OikYPcfEXF8igWhuORwqcmpgHxVkIQ9I= +=wnkU +-----END PGP SIGNATURE----- +--=-=-=-- diff --git a/lib/testdir4/signed-bad!2,S b/lib/testdir4/signed-bad!2,S new file mode 100644 index 0000000..7a37ba9 --- /dev/null +++ b/lib/testdir4/signed-bad!2,S @@ -0,0 +1,35 @@ +Return-path: <> +Envelope-to: skipio@localhost +Delivery-date: Fri, 11 May 2012 16:21:57 +0300 +Received: from localhost.roma.net([127.0.0.1] helo=borealis) + by borealis with esmtp (Exim 4.77) + id 1SSpnB-00038a-55 + for djcb@localhost; Fri, 11 May 2012 16:21:57 +0300 +From: Skipio +To: Hannibal +Subject: signed +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:20:45 +0300 +Message-ID: <878vgy97ma.fsf@roma.net> +MIME-Version: 1.0 +Content-Type: multipart/signed; boundary="=-=-="; micalg=pgp-sha1; + protocol="application/pgp-signature" + +--=-=-= +Content-Type: text/plain + + +I am signed! But it's not good because I added this later + +--=-=-= +Content-Type: application/pgp-signature + +-----BEGIN PGP SIGNATURE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +iEYEARECAAYFAk+tEi0ACgkQ6WrHoQF92jxTzACeKd/XxY+P7bpymWL3JBRHaW9p +DpwAoKw7PDW4z/lNTkWjndVTjoO9jGhs +=blXz +-----END PGP SIGNATURE----- +--=-=-=-- + diff --git a/lib/testdir4/signed-encrypted!2,S b/lib/testdir4/signed-encrypted!2,S new file mode 100644 index 0000000..a3910e6 --- /dev/null +++ b/lib/testdir4/signed-encrypted!2,S @@ -0,0 +1,54 @@ +Return-path: <> +Envelope-to: karjala@localhost +Delivery-date: Fri, 11 May 2012 16:37:57 +0300 +From: karjala@example.com +To: lapinkulta@example.com +Subject: signed + encrypted +User-agent: mu4e 0.9.8.5-dev1; emacs 24.1.50.8 +Date: Fri, 11 May 2012 16:36:08 +0300 +Message-ID: <874nrm96wn.fsf@example.com> +MIME-Version: 1.0 +Content-Type: multipart/encrypted; boundary="=-=-="; + protocol="application/pgp-encrypted" + +--=-=-= +Content-Type: application/pgp-encrypted + +Version: 1 + +--=-=-= +Content-Type: application/octet-stream + +-----BEGIN PGP MESSAGE----- +Version: GnuPG v1.4.12 (GNU/Linux) + +hQQOA1T38TPQrHD6EA/+K4kSpMa7zk+qihUkQnHSq28xYxisNQx6X5DVNjA/Qx16 +uZj/40ae+PoSMTVfklP+B2S/IomuTW6dwVqS7aQ3u4MTzi+YOi11k1lEMD7hR0Wb +L0i48o3/iCPuCTpnOsaLZvRL06g+oTi0BF2pgz/YdsgsBTGrTb3pkDGSlLIhvh/J +P8eE3OuzkXS6d8ymJKx2S2wQJrc1AFf1BgJfgc5T0iAvcV+zIMG+PIYcVd04zVpj +cORFEfvGgfxWkeX+Ks3tu/l5PA1EesnoqFdNFZm+RKBg3RFsOm8tBlJ46xJjfeHg +zLgifeSLy3tOX7CvWYs9torrx7s7UOI2gV8kzBqz+a7diyCMezceeQ9l0nIRybwW +C9Egp8Bpfb02iXTOGdE/vRiNItQH14GKmXf4nCSwdtQUm3yzaqY9yL3xBxAlW53e +YOFfPMESt+E7IlPn0c7llWGrcdrhJbUEoGOIPezES7kdeNPzi8G1lLtvT04/SSZJ +QxPH5FNzSFaYFAQSdI7TR69P7L7vtLL8ndkjY49HfLFXochQQzsqrzVxzRCruHxA +zbZSRptNf9SuXEaX9buO1vlFHheGvrCKzEWa6O7JD/DiyrE/zqy4jdlh9abMCouQ +GWGSbn8jk6SMTQQ2Yv/VOyFqifHZp0UJD59tyIdenpxoYu5M0lwHLNVDlRjLEwUQ +AIDz1tbLoM7lxs2FOKGr8QqbKIeMfL+NUmbvVIDc4mJrOlRnHh+cZYm4Z49iTl1v +bYNMYgR5nY7W6rqh0ae7ZOW0h2NzpkAwTzuf1YrSjNavd9KBwOCFtAoZhRwfwFVx +ju+ByHFNnf7g/R6DekHS0pSiatM0cPDJT05atEZb+13CRHHznonmLHi+VahXjrpg +cIUA8Lhjdfm6Fsabo7gNZnTTRxNBqUXKK2vJF/XLbNrH5K2BH2dCCmUNtm3yFWiM +DOzaw3665Y3S6MvZdyKpatbNrVoJdBpRgPxJ1YCSEituFUqHJBStay+aRb5fVkQR +w3+9hWw+Ob0+2EumKbgfQ7iMwTZBCZP4VOxkoqdHvs9aWm4N7wHtXsyCew3icbJx +lyUWsDx/FI+HlQRfOqeAMxmp8kKybmHNw8oGiw+uPPUHSD1NFYVm2DtwhYll3Fvs +YY7r5s3yP1ZnwxMqWI3OsExVUXs8MS4UTAgO+cggO7YidPcANbBDihBFP8mTXtni +Oo5n5v+/eRoLfHmnsGcaK8EkKsfFHpbqn4gxXGcBuHaTTJ/ZhbW6bi1WWZA9ExaJ +IeTDtp5Bks1pJvTjCDacvgwl3rEBM6yaeIvB7575Y/GPMTOZhawhfOxV1smMmTKI +JOWYb3+PuN2cvWetkjFgH8re4sRXq22DKBZHJEWYU8sH0sACAePnIr+pkrOtGeJB +t1zBqZUnrupH6ptk9n/AjbQ+XSMTEKu55gSjYLAYx1EHApx52QLkdh+ej5xCIVeY +6wS1Iipkoc6/r6F7CKctupXurNY2AlD4uQIOfD6kQgkqK4PY3hsRHQA+Zqj6oRfr +kxysFJZvhgt26IeBVapFs10WuYt9iHfpbPUBQUIZCLyPAh08UdVW64Uc2DvUPy+I +C+3RrmTHQPP/YNKgDQaZ3ySVEDkqjaDPmXr5K0Ibaib2dtPCLcA= +=pv03 +-----END PGP MESSAGE----- +--=-=-=-- + diff --git a/lib/testdir4/special!2,Sabc b/lib/testdir4/special!2,Sabc new file mode 100644 index 0000000..7f1de8e --- /dev/null +++ b/lib/testdir4/special!2,Sabc @@ -0,0 +1,10 @@ +Date: Thu, 1 Jun 2012 14:57:25 -0200 +From: "Rocky Balboa" +To: "Ivan Drago" +Subject: currying and tail optimization +Message-id: <3BE9E653ef345@emss35m06.us.lmco.com> +MIME-version: 1.0 +Content-type: text/plain; charset=us-ascii +Content-transfer-encoding: 7BIT + +Test 123. I'm a special message with special flags. diff --git a/lib/utils/Makefile.am b/lib/utils/Makefile.am new file mode 100644 index 0000000..5a638ac --- /dev/null +++ b/lib/utils/Makefile.am @@ -0,0 +1,109 @@ +## Copyright (C) 2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CFLAGS= \ + $(WARN_CFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \ + -Wno-format-nonliteral \ + -Wno-switch-enum \ + -Wno-deprecated-declarations \ + -Wno-inline \ + -I${top_srcdir}/lib + +AM_CPPFLAGS= \ + $(CODE_COVERAGE_CPPFLAGS) + +AM_CXXFLAGS= \ + $(WARN_CXXFLAGS) \ + $(GLIB_CFLAGS) \ + $(ASAN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -I${top_srcdir}/lib + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) + +noinst_LTLIBRARIES= \ + libmu-utils.la + +libmu_utils_la_SOURCES= \ + mu-date.c \ + mu-date.h \ + mu-error.hh \ + mu-log.c \ + mu-log.h \ + mu-command-parser.cc \ + mu-command-parser.hh \ + mu-sexp-parser.cc \ + mu-sexp-parser.hh \ + mu-str.c \ + mu-str.h \ + mu-util.c \ + mu-util.h \ + mu-utils.cc \ + mu-utils.hh + +libmu_utils_la_LIBADD= \ + $(GLIB_LIBS) \ + $(CODE_COVERAGE_LIBS) + +noinst_PROGRAMS= \ + $(TEST_PROGS) + +TEST_PROGS+= \ + test-mu-util +test_mu_util_SOURCES= \ + test-mu-util.c +test_mu_util_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-mu-utils +test_mu_utils_SOURCES= \ + test-utils.cc +test_mu_utils_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-mu-str +test_mu_str_SOURCES= \ + test-mu-str.c +test_mu_str_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-sexp-parser +test_sexp_parser_SOURCES= \ + test-sexp-parser.cc +test_sexp_parser_LDADD= \ + libmu-utils.la + +TEST_PROGS+= \ + test-command-parser +test_command_parser_SOURCES= \ + test-command-parser.cc +test_command_parser_LDADD= \ + libmu-utils.la + +TESTS=$(TEST_PROGS) + +include $(top_srcdir)/aminclude_static.am diff --git a/lib/utils/mu-command-parser.cc b/lib/utils/mu-command-parser.cc new file mode 100644 index 0000000..7c4aa4a --- /dev/null +++ b/lib/utils/mu-command-parser.cc @@ -0,0 +1,196 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "mu-command-parser.hh" +#include "mu-utils.hh" + +#include +#include + +using namespace Mu; +using namespace Command; +using namespace Sexp; + +static Mu::Error +command_error(const std::string& msg) +{ + return Mu::Error(Error::Code::Command, msg); +} + + +void +Command::invoke(const Command::CommandMap& cmap, const Node& call) +{ + if (call.type != Type::List || call.children.empty() || + call.children[0].type != Type::Symbol) + throw command_error("call must be a list starting with a symbol"); + + const auto& params{call.children}; + const auto cmd_it = cmap.find(params[0].value); + if (cmd_it == cmap.end()) + throw command_error("unknown command '" + params[0].value + "'"); + + const auto& cinfo{cmd_it->second}; + + // all required parameters must be present + for (auto&& arg: cinfo.args) { + const auto& argname{arg.first}; + const auto& arginfo{arg.second}; + + // calls used keyword-parameters, e.g. + // (my-function :bar 1 :cuux "fnorb") + // so, we're looking for the odd-numbered parameters. + const auto param_it = [&]() { + for (size_t i = 1; i < params.size(); i += 2) + if (params[i].type == Type::Symbol && + params[i].value == ':' + argname) + return params.begin() + i + 1; + + return params.end(); + + }(); + + // it's an error when a required parameter is missing. + if (param_it == params.end()) { + if (arginfo.required) + throw command_error("missing required parameter '" + argname + "'"); + continue; // not required + } + + // the types must match, but the 'nil' symbol is acceptable as + // "no value" + if (param_it->type != arginfo.type && + !(param_it->type == Type::Symbol && param_it->value == "nil")) + throw command_error("parameter '" + argname + "' expects type " + + to_string(arginfo.type) + + " but got " + to_string(param_it->type)); + } + + // all passed parameters must be known + for (size_t i = 1; i < params.size(); i += 2) { + if (std::none_of(cinfo.args.begin(), cinfo.args.end(), + [&](auto&& arg) {return params[i].value == ":" + arg.first;})) + throw command_error("unknown parameter '" + params[i].value + "'"); + } + + if (cinfo.handler) + cinfo.handler(params); +} + +static Parameters::const_iterator +find_param_node (const Parameters& params, const std::string& argname) +{ + for (size_t i = 1; i < params.size(); i += 2) { + if (i + 1 != params.size() && + params[i].type == Type::Symbol && + params[i].value == ':' + argname) + return params.begin() + i + 1; + } + + return params.end(); +} + +constexpr auto Nil = "nil"; + +static bool +is_nil(const Node& node) +{ + return node.type == Type::Symbol && node.value == Nil; +} + +const std::string& +Command::get_string_or (const Parameters& params, const std::string& argname, + const std::string& alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::String) + throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", + to_string(it->type).c_str(), + it->value.c_str()); + + return it->value; +} + +const std::string& +Command::get_symbol_or (const Parameters& params, const std::string& argname, + const std::string& alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::Symbol) + throw Error(Error::Code::InvalidArgument, "expected but got %s (value: '%s')", + to_string(it->type).c_str(), + it->value.c_str()); + + return it->value; +} + + +int +Command::get_int_or (const Parameters& params, const std::string& argname, + int alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return alt; + else if (it->type != Type::Integer) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + else + return ::atoi(it->value.c_str()); +} + +bool +Command::get_bool_or (const Parameters& params, const std::string& argname, + bool alt) +{ + const auto it = find_param_node (params, argname); + if (it == params.end()) + return alt; + else if (it->type != Type::Symbol) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + else + return it->value != Nil; +} + +std::vector +Command::get_string_vec (const Parameters& params, const std::string& argname) +{ + const auto it = find_param_node (params, argname); + if (it == params.end() || is_nil(*it)) + return {}; + else if (it->type != Type::List) + throw Error(Error::Code::InvalidArgument, "expected but got %s", + to_string(it->type).c_str()); + + std::vector vec; + for (const auto& n: it->children) { + if (n.type != Type::String) + throw Error(Error::Code::InvalidArgument, + "expected string element but got %s", + to_string(n.type).c_str()); + vec.emplace_back (n.value); + } + + return vec; +} diff --git a/lib/utils/mu-command-parser.hh b/lib/utils/mu-command-parser.hh new file mode 100644 index 0000000..58c2f11 --- /dev/null +++ b/lib/utils/mu-command-parser.hh @@ -0,0 +1,157 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ +#ifndef MU_COMMAND_PARSER_HH__ +#define MU_COMMAND_PARSER_HH__ + +#include +#include +#include +#include +#include +#include +#include + +#include "utils/mu-error.hh" +#include "utils/mu-sexp-parser.hh" + + +namespace Mu { +namespace Command { + +/// +/// Commands are s-expressions with the follow properties: + +/// 1) a command is a list with a command-name as its first argument +/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some +/// type (ie. 'keyword arguments') +/// 3) each command is described by its CommandInfo structure, which defines the type +/// 4) calls to the command must include all required parameters +/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed +/// for specify a non-required parameter to be absent; this is for convenience on the +/// call side. + + +/// Information about a function argument +struct ArgInfo { + ArgInfo (Sexp::Type typearg, bool requiredarg, std::string&& docarg): + type{typearg}, required{requiredarg},docstring{std::move(docarg)} + {} + const Sexp::Type type; /**< Sexp::Type of the argument */ + const bool required; /**< Is this argument required? */ + const std::string docstring; /**< Documentation */ +}; + +/// The arguments for a function, which maps their names to the information. +using ArgMap = std::unordered_map; +// The parameters to a Handler. +using Parameters = std::vector; + +int get_int_or (const Parameters& parms, const std::string& argname, int alt=0); +bool get_bool_or (const Parameters& parms, const std::string& argname, bool alt=false); +const std::string& get_string_or (const Parameters& parms, const std::string& argname, const std::string& alt=""); +const std::string& get_symbol_or (const Parameters& parms, const std::string& argname, const std::string& alt="nil"); + + +std::vector get_string_vec (const Parameters& params, const std::string& argname); + + +// A handler function +using Handler = std::function; + +/// Information about some command +struct CommandInfo { + CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg): + args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{std::move(handlerarg)} + {} + const ArgMap args; + const std::string docstring; + const Handler handler; + + /** + * Get a sorted list of argument names, for display. Required args come + * first, then alphabetical. + * + * @return vec with the sorted names. + */ + std::vector sorted_argnames() const { // sort args -- by required, then alphabetical. + std::vector names; + for (auto&& arg: args) + names.emplace_back(arg.first); + std::sort(names.begin(), names.end(), [&](const auto& name1, const auto& name2) { + const auto& arg1{args.find(name1)->second}; + const auto& arg2{args.find(name2)->second}; + if (arg1.required != arg2.required) + return arg1.required; + else + return name1 < name2; + }); + return names; + } +}; +/// All commands, mapping their name to information about them. +using CommandMap = std::unordered_map; + +/** + * Validate that the call (a Sexp::Node) specifies a valid call, then invoke it. + * + * A call uses keyword arguments, e.g. something like: + * (foo :bar 1 :cuux "fnorb") + * + * On error, throw Error. + * + * @param cmap map of commands + * @param call node describing a call. + */ +void invoke(const Command::CommandMap& cmap, const Sexp::Node& call); + + +static inline std::ostream& +operator<<(std::ostream& os, const Command::ArgInfo& info) +{ + os << info.type + << " (" << ( info.required ? "required" : "optional" ) << ")"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Command::CommandInfo& info) +{ + for (auto&& arg: info.args) + os << " " << arg.first << ": " << arg.second << '\n' + << " " << arg.second.docstring << "\n"; + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Command::CommandMap& map) +{ + for (auto&& c: map) + os << c.first << '\n' << c.second; + + return os; +} + + +} // namespace Command +} // namespace Mu + + +#endif /* MU_COMMAND_PARSER_HH__ */ diff --git a/lib/utils/mu-date.c b/lib/utils/mu-date.c new file mode 100644 index 0000000..4d46397 --- /dev/null +++ b/lib/utils/mu-date.c @@ -0,0 +1,87 @@ +/* +** Copyright (C) 2012 +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include +#include +#include + +#include "mu-util.h" +#include "mu-date.h" +#include "mu-str.h" + +const char* +mu_date_str_s (const char* frm, time_t t) +{ + struct tm *tmbuf; + static char buf[128]; + static int is_utf8 = -1; + size_t len; + + if (G_UNLIKELY(is_utf8 == -1)) + is_utf8 = mu_util_locale_is_utf8 () ? 1 : 0; + + g_return_val_if_fail (frm, NULL); + + tmbuf = localtime(&t); + len = strftime (buf, sizeof(buf) - 1, frm, tmbuf); + if (len == 0) + return ""; /* not necessarily an error... */ + + if (!is_utf8) { + /* charset is _not_ utf8, so we need to convert it, so + * the date could contain locale-specific characters*/ + gchar *conv; + GError *err; + err = NULL; + conv = g_locale_to_utf8 (buf, -1, NULL, NULL, &err); + if (err) { + g_warning ("conversion failed: %s", err->message); + g_error_free (err); + strcpy (buf, ""); + } else { + strncpy (buf, conv, sizeof(buf)-1); + buf[sizeof(buf)-1] = '\0'; + } + + g_free (conv); + } + + return buf; +} + +char* +mu_date_str (const char *frm, time_t t) +{ + return g_strdup (mu_date_str_s(frm, t)); +} + + +const char* +mu_date_display_s (time_t t) +{ + time_t now; + static const time_t SECS_IN_DAY = 24 * 60 * 60; + + now = time (NULL); + + if (ABS(now - t) > SECS_IN_DAY) + return mu_date_str_s ("%x", t); + else + return mu_date_str_s ("%X", t); +} diff --git a/lib/utils/mu-date.h b/lib/utils/mu-date.h new file mode 100644 index 0000000..c7cf6c7 --- /dev/null +++ b/lib/utils/mu-date.h @@ -0,0 +1,67 @@ +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include + +#ifndef __MU_DATE_H__ +#define __MU_DATE_H__ + +G_BEGIN_DECLS + + +/** + * @addtogroup MuDate + * Date-related functions + * @{ + */ + +/** + * get a string for a given time_t + * + * mu_date_str_s returns a ptr to a static buffer, + * while mu_date_str returns dynamically allocated + * memory that must be freed after use. + * + * @param frm the format of the string (in strftime(3) format) + * @param t the time as time_t + * + * @return a string representation of the time; see above for what to + * do with it. Length is max. 128 bytes, inc. the ending \0. if the + * format is too long, the value will be truncated. in practice this + * should not happen. + */ +const char* mu_date_str_s (const char* frm, time_t t) G_GNUC_CONST; +char* mu_date_str (const char* frm, time_t t) G_GNUC_WARN_UNUSED_RESULT; + +/** + * get a display string for a given time_t; if the given is less than + * 24h from the current time, we display the time, otherwise the date, + * using the preferred date/time for the current locale + * + * mu_str_display_date_s returns a ptr to a static buffer, + * + * @param t the time as time_t + * + * @return a string representation of the time/date + */ +const char* mu_date_display_s (time_t t); + +G_END_DECLS + +#endif /*__MU_DATE_H__*/ diff --git a/lib/utils/mu-error.hh b/lib/utils/mu-error.hh new file mode 100644 index 0000000..b2d6ed2 --- /dev/null +++ b/lib/utils/mu-error.hh @@ -0,0 +1,137 @@ +/* +** Copyright (C) 2019 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#ifndef MU_ERROR_HH__ +#define MU_ERROR_HH__ + +#include +#include "mu-utils.hh" +#include + +namespace Mu { + +struct Error final: public std::exception { + + enum struct Code { + AccessDenied, + Command, + File, + Index, + Internal, + InvalidArgument, + Message, + NotFound, + Parsing, + Query, + SchemaMismatch, + Store, + }; + + /** + * Construct an error + * + * @param codearg error-code + * #param msgarg the error diecription + */ + Error(Code codearg, const std::string& msgarg): + code_{codearg}, what_{msgarg} + {} + + /** + * Build an error from an error-code and a format string + * + * @param code error-code + * @param frm format string + * @param ... format parameters + * + * @return an Error object + */ + __attribute__((format(printf, 3, 0))) + Error(Code codearg, const char *frm, ...): code_{codearg} { + va_list args; + va_start(args, frm); + what_ = format(frm, args); + va_end(args); + } + + Error(Error&& rhs) = default; + Error(const Error& rhs) = delete; + + /** + * Build an error from a GError an error-code and a format string + * + * @param code error-code + * @param gerr a GError or {}, which is consumed + * @param frm format string + * @param ... format parameters + * + * @return an Error object + */ + __attribute__((format(printf, 4, 0))) + Error(Code codearg, GError **err, const char *frm, ...): code_{codearg} { + + va_list args; + va_start(args, frm); + what_ = format(frm, args); + va_end(args); + + if (err && *err) + what_ += format (": %s", (*err)->message); + else + what_ += ": something went wrong"; + + g_clear_error(err); + } + + /** + * DTOR + * + */ + virtual ~Error() = default; + + /** + * Get the descriptiove message. + * + * @return + */ + virtual const char* what() const noexcept override { + return what_.c_str(); + } + + /** + * Get the error-code for this error + * + * @return the error-code + */ + Code code() const { return code_; } + + + +private: + const Code code_; + std::string what_; + +}; + + +} // namespace Mu + + +#endif /* MU_ERROR_HH__ */ diff --git a/lib/utils/mu-log.c b/lib/utils/mu-log.c new file mode 100644 index 0000000..92ea188 --- /dev/null +++ b/lib/utils/mu-log.c @@ -0,0 +1,319 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2016 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include "mu-log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mu-util.h" + +#define MU_MAX_LOG_FILE_SIZE 1000 * 1000 /* 1 MB (SI units) */ +#define MU_LOG_FILE "mu.log" + +struct _MuLog { + int _fd; /* log file descriptor */ + + MuLogOptions _opts; + + gboolean _color_stdout; /* whether to use color */ + gboolean _color_stderr; + + GLogFunc _old_log_func; +}; +typedef struct _MuLog MuLog; + +/* we use globals, because logging is a global operation as it + * globally modifies the behaviour of g_warning and friends + */ +static MuLog* MU_LOG = NULL; +static void log_write (const char* domain, GLogLevelFlags level, + const gchar *msg); + +static void +try_close (int fd) +{ + if (fd < 0) + return; + + if (close (fd) < 0) + g_printerr ("%s: close() of fd %d failed: %s\n", + __func__, fd, strerror(errno)); +} + +static void +silence (void) +{ + return; +} + +gboolean +mu_log_init_silence (void) +{ + g_return_val_if_fail (!MU_LOG, FALSE); + + MU_LOG = g_new0 (MuLog, 1); + MU_LOG->_fd = -1; + + mu_log_options_set (MU_LOG_OPTIONS_NONE); + + MU_LOG->_old_log_func = + g_log_set_default_handler ((GLogFunc)silence, NULL); + + return TRUE; +} + +static void +log_handler (const gchar* log_domain, GLogLevelFlags log_level, + const gchar* msg) +{ + if ((log_level & G_LOG_LEVEL_DEBUG) && + !(MU_LOG->_opts & MU_LOG_OPTIONS_DEBUG)) + return; + + log_write (log_domain ? log_domain : "mu", log_level, msg); +} + + +void +mu_log_options_set (MuLogOptions opts) +{ + g_return_if_fail (MU_LOG); + + MU_LOG->_opts = opts; + + /* when color is, only enable it when output is to a tty */ + if (MU_LOG->_opts & MU_LOG_OPTIONS_COLOR) { + MU_LOG->_color_stdout = isatty(fileno(stdout)); + MU_LOG->_color_stderr = isatty(fileno(stderr)); + + } +} + + +MuLogOptions +mu_log_options_get (void) +{ + g_return_val_if_fail (MU_LOG, MU_LOG_OPTIONS_NONE); + + return MU_LOG->_opts; +} + + +static gboolean +move_log_file (const char *logfile) +{ + gchar *logfile_old; + int rv; + + logfile_old = g_strdup_printf ("%s.old", logfile); + rv = rename (logfile, logfile_old); + g_free (logfile_old); + + if (rv != 0) { + g_warning ("failed to move %s to %s.old: %s", + logfile, logfile, strerror(rv)); + return FALSE; + } else + return TRUE; + +} + + +static gboolean +log_file_backup_maybe (const char *logfile) +{ + struct stat statbuf; + + if (stat (logfile, &statbuf) != 0) { + if (errno == ENOENT) + return TRUE; /* it did not exist yet, no problem */ + else { + g_warning ("failed to stat(2) %s: %s", + logfile, strerror(errno)); + return FALSE; + } + } + + /* log file is still below the max size? */ + if (statbuf.st_size <= MU_MAX_LOG_FILE_SIZE) + return TRUE; + + /* log file is too big!; we move it to .old, overwriting */ + return move_log_file (logfile); +} + + +gboolean +mu_log_init (const char* logfile, MuLogOptions opts) +{ + int fd; + + /* only init once... */ + g_return_val_if_fail (!MU_LOG, FALSE); + g_return_val_if_fail (logfile, FALSE); + + if (opts & MU_LOG_OPTIONS_BACKUP) + if (!log_file_backup_maybe(logfile)) { + g_warning ("failed to backup log file"); + return FALSE; + } + + fd = open (logfile, O_WRONLY|O_CREAT|O_APPEND, 00600); + if (fd < 0) { + g_warning ("%s: open() of '%s' failed: %s", __func__, + logfile, strerror(errno)); + return FALSE; + } + + MU_LOG = g_new0 (MuLog, 1); + MU_LOG->_fd = fd; + + mu_log_options_set (opts); + + MU_LOG->_old_log_func = + g_log_set_default_handler ((GLogFunc)log_handler, NULL); + + MU_WRITE_LOG ("logging started"); + + return TRUE; +} + +void +mu_log_uninit (void) +{ + if (!MU_LOG) + return; + + MU_WRITE_LOG ("logging stopped"); + + try_close (MU_LOG->_fd); + g_free (MU_LOG); + + MU_LOG = NULL; +} + + +static const char* +levelstr (GLogLevelFlags level) +{ + switch (level) { + case G_LOG_LEVEL_WARNING: return " [WARN] "; + case G_LOG_LEVEL_ERROR : return " [ERR ] "; + case G_LOG_LEVEL_DEBUG: return " [DBG ] "; + case G_LOG_LEVEL_CRITICAL: return " [CRIT] "; + case G_LOG_LEVEL_MESSAGE: return " [MSG ] "; + case G_LOG_LEVEL_INFO : return " [INFO] "; + default: return " [LOG ] "; + } +} + + + +#define color_stdout_maybe(C) \ + do{if (MU_LOG->_color_stdout) fputs ((C),stdout);} while (0) +#define color_stderr_maybe(C) \ + do{if (MU_LOG->_color_stderr) fputs ((C),stderr);} while (0) + + + +static void +log_write_fd (GLogLevelFlags level, const gchar *msg) +{ + time_t now; + char timebuf [22]; + const char *mylevel; + + /* get the time/date string */ + now = time(NULL); + strftime (timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", + localtime(&now)); + + if (write (MU_LOG->_fd, timebuf, strlen (timebuf)) < 0) + goto err; + + mylevel = levelstr (level); + if (write (MU_LOG->_fd, mylevel, strlen (mylevel)) < 0) + goto err; + + if (write (MU_LOG->_fd, msg, strlen (msg)) < 0) + goto err; + + if (write (MU_LOG->_fd, "\n", strlen ("\n")) < 0) + goto err; + + return; /* all went well */ + +err: + fprintf (stderr, "%s: failed to write to log: %s\n", + __func__, strerror(errno)); +} + + +static void +log_write_stdout_stderr (GLogLevelFlags level, const gchar *msg) +{ + const char *mu; + + mu = MU_LOG->_opts & MU_LOG_OPTIONS_NEWLINE ? + "\nmu: " : "mu: "; + + if (!(MU_LOG->_opts & MU_LOG_OPTIONS_QUIET) && + (level & G_LOG_LEVEL_MESSAGE)) { + color_stdout_maybe (MU_COLOR_GREEN); + fputs (mu, stdout); + fputs (msg, stdout); + fputs ("\n", stdout); + color_stdout_maybe (MU_COLOR_DEFAULT); + } + + /* for errors, log them to stderr as well */ + if (level & G_LOG_LEVEL_ERROR || + level & G_LOG_LEVEL_CRITICAL || + level & G_LOG_LEVEL_WARNING) { + color_stderr_maybe (MU_COLOR_RED); + fputs (mu, stderr); + fputs (msg, stderr); + fputs ("\n", stderr); + color_stderr_maybe (MU_COLOR_DEFAULT); + } +} + + +static void +log_write (const char* domain, GLogLevelFlags level, const gchar *msg) +{ + g_return_if_fail (MU_LOG); + + log_write_fd (level, msg); + log_write_stdout_stderr (level, msg); +} diff --git a/lib/utils/mu-log.h b/lib/utils/mu-log.h new file mode 100644 index 0000000..fc05d0b --- /dev/null +++ b/lib/utils/mu-log.h @@ -0,0 +1,99 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: + 8 -*-*/ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_LOG_H__ +#define __MU_LOG_H__ + +#include + +/* mu log is the global logging system */ + +G_BEGIN_DECLS + +enum _MuLogOptions { + MU_LOG_OPTIONS_NONE = 0, + + /* when size of log file > MU_MAX_LOG_FILE_SIZE, move the log + * file to .old and start a new one. The .old file will + * overwrite existing files of that name */ + MU_LOG_OPTIONS_BACKUP = 1 << 1, + + /* quiet: don't log non-errors to stdout/stderr */ + MU_LOG_OPTIONS_QUIET = 1 << 2, + + /* should lines be preceded by \n? useful when errors come + * during indexing */ + MU_LOG_OPTIONS_NEWLINE = 1 << 3, + + /* color in output (iff output is to a tty) */ + MU_LOG_OPTIONS_COLOR = 1 << 4, + + /* log everything to stderr */ + MU_LOG_OPTIONS_STDERR = 1 << 5, + + /* debug: debug include debug-level information */ + MU_LOG_OPTIONS_DEBUG = 1 << 6 +}; +typedef enum _MuLogOptions MuLogOptions; + + +/** + * write logging information to a log file + * + * @param full path to the log file (does not have to exist yet, but + * it's directory must) + * @param opts logging options + * + * @return TRUE if initialization succeeds, FALSE otherwise + */ +gboolean mu_log_init (const char *logfile, MuLogOptions opts) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * be silent except for runtime errors, which will be written to + * stderr. + * + * @return TRUE if initialization succeeds, FALSE otherwise + */ +gboolean mu_log_init_silence (void) G_GNUC_WARN_UNUSED_RESULT; + +/** + * uninitialize the logging system, and free all resources + */ +void mu_log_uninit (void); + +/** + * set logging options, a logical-OR'd value of MuLogOptions + * + * @param opts the options (logically OR'd) + */ +void mu_log_options_set (MuLogOptions opts); + +/** + * get logging options, a logical-OR'd value of MuLogOptions + * + * @param opts the options (logically OR'd) + */ +MuLogOptions mu_log_options_get (void); + +G_END_DECLS + +#endif /*__MU_LOG_H__*/ diff --git a/lib/utils/mu-sexp-parser.cc b/lib/utils/mu-sexp-parser.cc new file mode 100644 index 0000000..52f418a --- /dev/null +++ b/lib/utils/mu-sexp-parser.cc @@ -0,0 +1,175 @@ +/* +** Copyright (C) 2020 djcb +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + + +#include "mu-sexp-parser.hh" +#include "mu-utils.hh" + +using namespace Mu; +using namespace Sexp; + +__attribute__((format(printf, 2, 0))) static Mu::Error +parsing_error(size_t pos, const char* frm, ...) +{ + va_list args; + va_start(args, frm); + auto msg = format(frm, args); + va_end(args); + + if (pos == 0) + return Mu::Error(Error::Code::Parsing, "%s", msg.c_str()); + else + return Mu::Error(Error::Code::Parsing, "%zu: %s", pos, msg.c_str()); +} +static size_t +skip_whitespace (const std::string& s, size_t pos) +{ + while (pos != s.size()) { + if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n') + ++pos; + else + break; + } + + return pos; +} + +static Node parse (const std::string& expr, size_t& pos); + +static Node +parse_list (const std::string& expr, size_t& pos) +{ + if (expr[pos] != '(') // sanity check. + throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]); + + std::vector children; + + ++pos; + while (expr[pos] != ')' && pos != expr.size()) + children.emplace_back(parse(expr, pos)); + + if (expr[pos] != ')') + throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]); + ++pos; + return Node{std::move(children)}; +} + +// parse string +static Node +parse_string (const std::string& expr, size_t& pos) +{ + if (expr[pos] != '"') // sanity check. + throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]); + + bool escape{}; + std::string str; + for (++pos; pos != expr.size(); ++pos) { + + auto kar = expr[pos]; + if (escape && (kar == '"' || kar == '\\')) { + str += kar; + escape = false; + continue; + } + + if (kar == '"') + break; + else if (kar == '\\') + escape = true; + else + str += kar; + } + + if (escape || expr[pos] != '"') + throw parsing_error(pos, "unterminated string '%s'", str.c_str()); + + ++pos; + return Node{Type::String, std::move(str)}; +} + +static Node +parse_integer (const std::string& expr, size_t& pos) +{ + if (!isdigit(expr[pos]) && expr[pos] != '-') // sanity check. + throw parsing_error(pos, "expected: but got '%c", expr[pos]); + + std::string num; // negative number? + if (expr[pos] == '-') { + num = "-"; + ++pos; + } + + for (; isdigit(expr[pos]); ++pos) + num += expr[pos]; + + return Node {Type::Integer, std::move(num)}; +} + +static Node +parse_symbol (const std::string& expr, size_t& pos) +{ + if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check. + throw parsing_error(pos, "expected: |: but got '%c", expr[pos]); + + std::string symbol(1, expr[pos]); + for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos) + symbol += expr[pos]; + + return Node { Type::Symbol, std::move(symbol)}; +} + + +static Node +parse (const std::string& expr, size_t& pos) +{ + pos = skip_whitespace(expr, pos); + + if (pos == expr.size()) + throw parsing_error(pos, "expected: character '%c", expr[pos]); + + const auto kar = expr[pos]; + const auto node =[&]() -> Node { + if (kar == '(') + return parse_list (expr, pos); + else if (kar == '"') + return parse_string(expr, pos); + else if (isdigit(kar) || kar == '-') + return parse_integer(expr, pos); + else if (isalpha(kar) || kar == ':') + return parse_symbol(expr, pos); + else + throw parsing_error(pos, "unexpected character '%c", kar); + }(); + + pos = skip_whitespace(expr, pos); + + return node; +} + +Node +Sexp::parse (const std::string& expr) +{ + size_t pos{}; + auto node{::parse (expr, pos)}; + + if (pos != expr.size()) + throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]); + + return node; +} diff --git a/lib/utils/mu-sexp-parser.hh b/lib/utils/mu-sexp-parser.hh new file mode 100644 index 0000000..ecb97f2 --- /dev/null +++ b/lib/utils/mu-sexp-parser.hh @@ -0,0 +1,115 @@ +/* +** Copyright (C) 2020 djcb +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef MU_SEXP_PARSER_HH__ +#define MU_SEXP_PARSER_HH__ + +#include +#include + +#include "utils/mu-error.hh" + +namespace Mu { +namespace Sexp { + +/// Simple s-expression parser that parses lists () and atoms (strings +/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or +/// ':', then alphanum and '-') +/// +/// (:foo (1234 "bar" nil) :quux (a b c)) + +/// Node type +enum struct Type { List, String, Integer, Symbol }; + +/// Parse node +struct Node { + /** + * Construct a new non-list node + * + * @param typearg the type of node + * @param valuearg the value + */ + Node(Type typearg, std::string&& valuearg): + type{typearg}, value{std::move(valuearg)} { + if (typearg == Type::List) + throw Error(Error::Code::Parsing, + "atomic type cannot be a "); + } + + /** + * Construct a list node + + * @param childrenarg the list children + * + * @return + */ + explicit Node(std::vector&& childrenarg): + type{Type::List}, children{std::move(childrenarg)} + {} + + const Type type; /**< Type of node */ + const std::string value; /**< String value of node (only for non-Type::List)*/ + const std::vector children; /**< Chiidren of node (only for Type::List) */ +}; + +/** + * Parse the string as an s-expressi9on. + * + * @param expr an s-expression string + * + * @return the parsed s-expression, or throw Error. + */ +Node parse(const std::string& expr); + +static inline std::ostream& +operator<<(std::ostream& os, Sexp::Type id) +{ + switch (id) { + case Sexp::Type::List: os << ""; break; + case Sexp::Type::String: os << ""; break; + case Sexp::Type::Integer: os << ""; break; + case Sexp::Type::Symbol: os << ""; break; + default: throw std::runtime_error ("unknown node type"); + } + + return os; +} + +static inline std::ostream& +operator<<(std::ostream& os, const Sexp::Node& node) +{ + os << node.type; + if (node.type == Sexp::Type::List) { + os << '('; + for (auto&& elm: node.children) + os << elm; + os << ')'; + } else + os << '{' << node.value << '}'; + + return os; +} + + +} // Sexp + + +} // Mu + +#endif /* MU_SEXP_PARSER_HH__ */ diff --git a/lib/utils/mu-str.c b/lib/utils/mu-str.c new file mode 100644 index 0000000..607d322 --- /dev/null +++ b/lib/utils/mu-str.c @@ -0,0 +1,453 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + + +#include +#include +#include +#include +#include + +#include "mu-util.h" /* PATH_MAX */ +#include "mu-str.h" + +const char* +mu_str_size_s (size_t s) +{ + static char buf[32]; + char *tmp; + + tmp = g_format_size_for_display ((goffset)s); + strncpy (buf, tmp, sizeof(buf)); + buf[sizeof(buf) -1] = '\0'; /* just in case */ + g_free (tmp); + + return buf; +} + +char* +mu_str_size (size_t s) +{ + return g_strdup (mu_str_size_s(s)); +} + + +char* +mu_str_summarize (const char* str, size_t max_lines) +{ + char *summary; + size_t nl_seen; + unsigned i,j; + gboolean last_was_blank; + + g_return_val_if_fail (str, NULL); + g_return_val_if_fail (max_lines > 0, NULL); + + /* len for summary <= original len */ + summary = g_new (gchar, strlen(str) + 1); + + /* copy the string up to max_lines lines, replace CR/LF/tab with + * single space */ + for (i = j = 0, nl_seen = 0, last_was_blank = TRUE; + nl_seen < max_lines && str[i] != '\0'; ++i) { + + if (str[i] == '\n' || str[i] == '\r' || + str[i] == '\t' || str[i] == ' ' ) { + + if (str[i] == '\n') + ++nl_seen; + + /* no double-blanks or blank at end of str */ + if (!last_was_blank && str[i+1] != '\0') + summary[j++] = ' '; + + last_was_blank = TRUE; + } else { + + summary[j++] = str[i]; + last_was_blank = FALSE; + } + } + + summary[j] = '\0'; + return summary; +} + + + + +char* +mu_str_replace (const char *str, const char *substr, const char *repl) +{ + GString *gstr; + const char *cur; + + g_return_val_if_fail (str, NULL); + g_return_val_if_fail (substr, NULL); + g_return_val_if_fail (repl, NULL); + + gstr = g_string_sized_new (2 * strlen (str)); + + for (cur = str; *cur; ++cur) { + if (g_str_has_prefix (cur, substr)) { + g_string_append (gstr, repl); + cur += strlen (substr) - 1; + } else + g_string_append_c (gstr, *cur); + } + + return g_string_free (gstr, FALSE); +} + + + +char* +mu_str_from_list (const GSList *lst, char sepa) +{ + const GSList *cur; + char *str; + + g_return_val_if_fail (sepa, NULL); + + for (cur = lst, str = NULL; cur; cur = g_slist_next(cur)) { + + char *tmp; + /* two extra dummy '\0' so -Wstack-protector won't complain */ + char sep[4] = { '\0', '\0', '\0', '\0' }; + sep[0] = cur->next ? sepa : '\0'; + + tmp = g_strdup_printf ("%s%s%s", + str ? str : "", + (gchar*)cur->data, + sep); + g_free (str); + str = tmp; + } + + return str; +} + +GSList* +mu_str_to_list (const char *str, char sepa, gboolean strip) +{ + GSList *lst; + gchar **strs, **cur; + /* two extra dummy '\0' so -Wstack-protector won't complain */ + char sep[4] = { '\0', '\0', '\0', '\0' }; + + g_return_val_if_fail (sepa, NULL); + + if (!str) + return NULL; + + sep[0] = sepa; + strs = g_strsplit (str, sep, -1); + + for (cur = strs, lst = NULL; cur && *cur; ++cur) { + char *elm; + elm = g_strdup(*cur); + if (strip) + elm = g_strstrip (elm); + + lst = g_slist_prepend (lst, elm); + } + + lst = g_slist_reverse (lst); + g_strfreev (strs); + + return lst; +} + +GSList* +mu_str_esc_to_list (const char *strings) +{ + GSList *lst; + GString *part; + unsigned u; + gboolean quoted, escaped; + + g_return_val_if_fail (strings, NULL); + + part = g_string_new (NULL); + + for (u = 0, lst = NULL, quoted = FALSE, escaped = FALSE; + u != strlen (strings); ++u) { + + char kar; + kar = strings[u]; + + if (kar == '\\') { + if (escaped) + g_string_append_c (part, '\\'); + escaped = !escaped; + continue; + } + + if (quoted && kar != '"') { + g_string_append_c (part, kar); + continue; + } + + switch (kar) { + case '"': + if (!escaped) + quoted = !quoted; + else + g_string_append_c (part, kar); + continue; + case ' ': + if (part->len > 0) { + lst = g_slist_prepend + (lst, g_string_free (part, FALSE)); + part = g_string_new (NULL); + } + continue; + default: + g_string_append_c (part, kar); + } + } + + if (part->len) + lst = g_slist_prepend (lst, g_string_free (part, FALSE)); + + return g_slist_reverse (lst); +} + + +void +mu_str_free_list (GSList *lst) +{ + g_slist_foreach (lst, (GFunc)g_free, NULL); + g_slist_free (lst); +} + + +/* this function is critical for sorting performance; therefore, no + * regexps, but just some good old c pointer magic */ +const gchar* +mu_str_subject_normalize (const gchar* str) +{ + const char* cur; + + g_return_val_if_fail (str, NULL); + + cur = str; + while (isspace(*cur)) ++cur; /* skip space */ + + /* starts with Re:? */ + if (tolower(cur[0]) == 'r' && tolower(cur[1]) == 'e') + cur += 2; + /* starts with Fwd:? */ + else if (tolower(cur[0]) == 'f' && tolower(cur[1]) == 'w' && + tolower(cur[2]) == 'd') + cur += 3; + else /* nope, different string */ + return str; + + /* we're now past either 'Re' or 'Fwd'. Maybe there's a [] now? + * ie., the Re[3]: foo case */ + if (cur[0] == '[') { /* handle the Re[3]: case */ + if (isdigit(cur[1])) { + do { ++cur; } while (isdigit(*cur)); + if ( cur[0] != ']') { + return str; /* nope: no ending ']' */ + } else /* skip ']' and space */ + do { ++cur; } while (isspace(*cur)); + } else /* nope: no number after '[' */ + return str; + } + + /* now, cur points past either 're' or 'fwd', possibly with + * []; check if it's really a prefix -- after re or fwd + * there should either a ':' and possibly some space */ + if (cur[0] == ':') { + do { ++cur; } while (isspace(*cur)); + /* note: there may still be another prefix, such as + * Re[2]: Fwd: foo */ + return mu_str_subject_normalize (cur); + } else + return str; /* nope, it was not a prefix */ +} + + +/* note: this function is *not* re-entrant, it returns a static buffer */ +const char* +mu_str_fullpath_s (const char* path, const char* name) +{ + static char buf[PATH_MAX + 1]; + + g_return_val_if_fail (path, NULL); + + g_snprintf (buf, sizeof(buf), "%s%c%s", path, G_DIR_SEPARATOR, + name ? name : ""); + + return buf; +} + + +char* +mu_str_escape_c_literal (const gchar* str, gboolean in_quotes) +{ + const char* cur; + GString *tmp; + + g_return_val_if_fail (str, NULL); + + tmp = g_string_sized_new (2 * strlen(str)); + + if (in_quotes) + g_string_append_c (tmp, '"'); + + for (cur = str; *cur; ++cur) + switch (*cur) { + case '\\': tmp = g_string_append (tmp, "\\\\"); break; + case '"': tmp = g_string_append (tmp, "\\\""); break; + default: tmp = g_string_append_c (tmp, *cur); + } + + if (in_quotes) + g_string_append_c (tmp, '"'); + + return g_string_free (tmp, FALSE); +} + + + +/* turn \0-terminated buf into ascii (which is a utf8 subset); convert + * any non-ascii into '.' + */ +char* +mu_str_asciify_in_place (char *buf) +{ + char *c; + + g_return_val_if_fail (buf, NULL); + + for (c = buf; c && *c; ++c) { + if ((!isprint(*c) && !isspace (*c)) || !isascii(*c)) + *c = '.'; + } + + return buf; +} + +char* +mu_str_utf8ify (const char *buf) +{ + char *utf8; + + g_return_val_if_fail (buf, NULL); + + utf8 = g_strdup (buf); + + if (!g_utf8_validate (buf, -1, NULL)) + mu_str_asciify_in_place (utf8); + + return utf8; +} + + + +gchar* +mu_str_convert_to_utf8 (const char* buffer, const char *charset) +{ + GError *err; + gchar * utf8; + + g_return_val_if_fail (buffer, NULL); + g_return_val_if_fail (charset, NULL ); + + err = NULL; + utf8 = g_convert_with_fallback (buffer, -1, "UTF-8", + charset, NULL, + NULL, NULL, &err); + if (!utf8) /* maybe the charset lied; try 8859-15 */ + utf8 = g_convert_with_fallback (buffer, -1, "UTF-8", + "ISO8859-15", NULL, + NULL, NULL, &err); + /* final attempt, maybe it was utf-8 already */ + if (!utf8 && g_utf8_validate (buffer, -1, NULL)) + utf8 = g_strdup (buffer); + + if (!utf8) { + g_warning ("%s: conversion failed from %s: %s", + __func__, charset, err ? err->message : ""); + } + + g_clear_error (&err); + + return utf8; +} + + +gchar* +mu_str_quoted_from_strv (const gchar **params) +{ + GString *str; + int i; + + g_return_val_if_fail (params, NULL); + + if (!params[0]) + return g_strdup (""); + + str = g_string_sized_new (64); /* just a guess */ + + for (i = 0; params[i]; ++i) { + + if (i > 0) + g_string_append_c (str, ' '); + + g_string_append_c (str, '"'); + g_string_append (str, params[i]); + g_string_append_c (str, '"'); + } + + return g_string_free (str, FALSE); +} + + +char* +mu_str_remove_ctrl_in_place (char *str) +{ + char *orig, *cur; + + g_return_val_if_fail (str, NULL); + + orig = str; + + for (cur = orig; *cur; ++cur) { + if (isspace(*cur)) { + /* squash special white space into a simple space */ + *orig++ = ' '; + } else if (iscntrl(*cur)) { + /* eat it */ + } else + *orig++ = *cur; + } + + *orig = '\0'; /* ensure the updated string has a NULL */ + + return str; +} diff --git a/lib/utils/mu-str.h b/lib/utils/mu-str.h new file mode 100644 index 0000000..f009a11 --- /dev/null +++ b/lib/utils/mu-str.h @@ -0,0 +1,221 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2017 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_STR_H__ +#define __MU_STR_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +/** + * @addtogroup MuStr + * Various string utilities + * @{ + */ + +/** + * get a display size for a given size_t; uses M for sizes > + * 1000*1000, k for smaller sizes. Note: this function use the + * 10-based SI units, _not_ the powers-of-2 based ones. + * + * mu_str_size_s returns a ptr to a static buffer, + * while mu_str_size returns dynamically allocated + * memory that must be freed after use. + * + * @param t the size as an size_t + * + * @return a string representation of the size; see above + * for what to do with it + */ +const char* mu_str_size_s (size_t s); +char* mu_str_size (size_t s) G_GNUC_WARN_UNUSED_RESULT; + + +/** + * Replace all occurrences of substr in str with repl + * + * @param str a string + * @param substr some string to replace + * @param repl a replacement string + * + * @return a newly allocated string with the substr replaced by repl; free with g_free + */ +char *mu_str_replace (const char *str, const char *substr, const char *repl); + + +/** + * get a 'summary' of the string, ie. the first /n/ lines of the + * strings, with all newlines removed, replaced by single spaces + * + * @param str the source string + * @param max_lines the maximum number of lines to include in the summary + * + * @return a newly allocated string with the summary. use g_free to free it. + */ +char* mu_str_summarize (const char* str, size_t max_lines) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * create a full path from a path + a filename. function is _not_ + * reentrant. + * + * @param path a path (!= NULL) + * @param name a name (may be NULL) + * + * @return the path as a statically allocated buffer. don't free. + */ +const char* mu_str_fullpath_s (const char* path, const char* name); + +/** + * escape a string like a string literal in C; ie. replace \ with \\, + * and " with \" + * + * @param str a non-NULL str + * @param in_quotes whether the result should be enclosed in "" + * + * @return the escaped string, newly allocated (free with g_free) + */ +char* mu_str_escape_c_literal (const gchar* str, gboolean in_quotes) + G_GNUC_WARN_UNUSED_RESULT; + +/** + * turn a string into plain ascii by replacing each non-ascii + * character with a dot ('.'). Replacement is done in-place. + * + * @param buf a buffer to asciify + * + * @return the buf ptr (as to allow for function composition) + */ +char* mu_str_asciify_in_place (char *buf); + +/** + * turn string in buf into valid utf8. If this string is not valid + * utf8 already, the function massages the offending characters. + * + * @param buf a buffer to utf8ify + * + * @return a newly allocated utf8 string + */ +char* mu_str_utf8ify (const char *buf); + +/** + * convert a string in a certain charset into utf8 + * + * @param buffer a buffer to convert + * @param charset source character set. + * + * @return a UTF8 string (which you need to g_free when done with it), + * or NULL in case of error + */ +gchar* mu_str_convert_to_utf8 (const char* buffer, const char *charset); + + +/** + * macro to check whether the string is empty, ie. if it's NULL or + * it's length is 0 + * + * @param S a string + * + * @return TRUE if the string is empty, FALSE otherwise + */ +#define mu_str_is_empty(S) ((!(S)||!(*S))?TRUE:FALSE) + +/** + * convert a GSList of strings to a #sepa-separated list + * + * @param lst a GSList + * @param the separator character + * + * @return a newly allocated string + */ +char* mu_str_from_list (const GSList *lst, char sepa); + +/** + * convert a #sepa-separated list of strings in to a GSList + * + * @param str a #sepa-separated list of strings + * @param the separator character + * @param remove leading/trailing whitespace from the string + * + * @return a newly allocated GSList (free with mu_str_free_list) + */ +GSList* mu_str_to_list (const char *str, char sepa, gboolean strip); + +/** + * convert a string (with possible escaping) to a list. list items are + * separated by one or more spaces. list items can be quoted (using + * '"'). + * + * @param str a string + * + * @return a list of elements or NULL in case of error, free with + * mu_str_free_list + */ +GSList* mu_str_esc_to_list (const char *str); + +/** + * free a GSList consisting of allocated strings + * + * @param lst a GSList + */ +void mu_str_free_list (GSList *lst); + +/** + * strip the subject of Re:, Fwd: etc. + * + * @param str a subject string + * + * @return a new string -- this is pointing somewhere inside the @str; + * no copy is made, don't free + */ +const gchar* mu_str_subject_normalize (const gchar* str); + + +/** + * take a list of strings, and return the concatenation of their + * quoted forms + * + * @param params NULL-terminated array of strings + * + * @return the quoted concatenation of the strings + */ +gchar* mu_str_quoted_from_strv (const gchar **params); + + + +/** + * Remove control characters from a string + * + * @param str a string + * + * @return the str with control characters removed + */ +char* mu_str_remove_ctrl_in_place (char *str); + + +/** @} */ + +G_END_DECLS + +#endif /*__MU_STR_H__*/ diff --git a/lib/utils/mu-util.c b/lib/utils/mu-util.c new file mode 100644 index 0000000..9f76c3a --- /dev/null +++ b/lib/utils/mu-util.c @@ -0,0 +1,495 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ +/* +** +** Copyright (C) 2008-2016 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include +#endif /*HAVE_CONFIG_H*/ + +#include "mu-util.h" +#define _XOPEN_SOURCE 500 + +#ifdef HAVE_WORDEXP_H +#include /* for shell-style globbing */ +#endif /*HAVE_WORDEXP_H*/ + +#include + +#include +#include /* for setlocale() */ + +#include +#include +#include +#include + +#include +#include +#include + +#include + + +static char* +do_wordexp (const char *path) +{ +#ifdef HAVE_WORDEXP_H + wordexp_t wexp; + char *dir; + + if (!path) { + /* g_debug ("%s: path is empty", __func__); */ + return NULL; + } + + if (wordexp (path, &wexp, 0) != 0) { + /* g_debug ("%s: expansion failed for %s", __func__, path); */ + return NULL; + } + + /* we just pick the first one */ + dir = g_strdup (wexp.we_wordv[0]); + + /* strangely, below seems to lead to a crash on MacOS (BSD); + so we have to allow for a tiny leak here on that + platform... maybe instead of __APPLE__ it should be + __BSD__? + + Hmmm., cannot reproduce that crash anymore, so commenting + it out for now... + */ +/* #ifndef __APPLE__ */ + wordfree (&wexp); +/* #endif /\*__APPLE__*\/ */ + return dir; + +# else /*!HAVE_WORDEXP_H*/ +/* E.g. OpenBSD does not have wordexp.h, so we ignore it */ + return path ? g_strdup (path) : NULL; +#endif /*HAVE_WORDEXP_H*/ +} + + +/* note, the g_debugs are commented out because this function may be + * called before the log handler is installed. */ +char* +mu_util_dir_expand (const char *path) +{ + char *dir; + char resolved[PATH_MAX + 1]; + + g_return_val_if_fail (path, NULL); + + dir = do_wordexp (path); + if (!dir) + return NULL; /* error */ + + /* don't try realpath if the dir does not exist */ + if (access (dir, F_OK) != 0) + return dir; + + /* now resolve any symlinks, .. etc. */ + if (realpath (dir, resolved) == NULL) { + /* g_debug ("%s: could not get realpath for '%s': %s", */ + /* __func__, dir, strerror(errno)); */ + g_free (dir); + return NULL; + } else + g_free (dir); + + return g_strdup (resolved); +} + +GQuark +mu_util_error_quark (void) +{ + static GQuark error_domain = 0; + + if (G_UNLIKELY(error_domain == 0)) + error_domain = g_quark_from_static_string + ("mu-error-quark"); + + return error_domain; +} + + +const char* +mu_util_cache_dir (void) +{ + static char cachedir [PATH_MAX]; + + g_snprintf (cachedir, sizeof(cachedir), "%s%cmu-%u", + g_get_tmp_dir(), G_DIR_SEPARATOR, + getuid()); + + return cachedir; +} + + +gboolean +mu_util_check_dir (const gchar* path, gboolean readable, gboolean writeable) +{ + int mode; + struct stat statbuf; + + if (!path) + return FALSE; + + mode = F_OK | (readable ? R_OK : 0) | (writeable ? W_OK : 0); + + if (access (path, mode) != 0) { + /* g_debug ("Cannot access %s: %s", path, strerror (errno)); */ + return FALSE; + } + + if (stat (path, &statbuf) != 0) { + /* g_debug ("Cannot stat %s: %s", path, strerror (errno)); */ + return FALSE; + } + + return S_ISDIR(statbuf.st_mode) ? TRUE: FALSE; +} + + +gchar* +mu_util_guess_maildir (void) +{ + const gchar *mdir1, *home; + + /* first, try MAILDIR */ + mdir1 = g_getenv ("MAILDIR"); + + if (mdir1 && mu_util_check_dir (mdir1, TRUE, FALSE)) + return g_strdup (mdir1); + + /* then, try /Maildir */ + home = g_get_home_dir(); + if (home) { + char *mdir2; + mdir2 = g_strdup_printf ("%s%cMaildir", + home, G_DIR_SEPARATOR); + if (mu_util_check_dir (mdir2, TRUE, FALSE)) + return mdir2; + g_free (mdir2); + } + + /* nope; nothing found */ + return NULL; +} + +gboolean +mu_util_create_dir_maybe (const gchar *path, mode_t mode, gboolean nowarn) +{ + struct stat statbuf; + + g_return_val_if_fail (path, FALSE); + + /* if it exists, it must be a readable dir */ + if (stat (path, &statbuf) == 0) { + if ((!S_ISDIR(statbuf.st_mode)) || + (access (path, W_OK|R_OK) != 0)) { + if (!nowarn) + g_warning ("not a read-writable" + "directory: %s", path); + return FALSE; + } + } + + if (g_mkdir_with_parents (path, mode) != 0) { + if (!nowarn) + g_warning ("failed to create %s: %s", + path, strerror(errno)); + return FALSE; + } + + return TRUE; +} + + +int +mu_util_create_writeable_fd (const char* path, mode_t mode, + gboolean overwrite) +{ + errno = 0; /* clear! */ + g_return_val_if_fail (path, -1); + + if (overwrite) + return open (path, O_WRONLY|O_CREAT|O_TRUNC, mode); + else + return open (path, O_WRONLY|O_CREAT|O_EXCL, mode); +} + + +gboolean +mu_util_is_local_file (const char* path) +{ + /* if it starts with file:// it's a local file (for the + * purposes of this function -- if it's on a remote FS it's + * still considered local) */ + if (g_ascii_strncasecmp ("file://", path, strlen("file://")) == 0) + return TRUE; + + if (access (path, R_OK) == 0) + return TRUE; + + return FALSE; +} + + +gboolean +mu_util_supports (MuFeature feature) +{ + + /* check for Guile support */ +#ifndef BUILD_GUILE + if (feature & MU_FEATURE_GUILE) + return FALSE; +#endif /*BUILD_GUILE*/ + + /* check for Gnuplot */ + if (feature & MU_FEATURE_GNUPLOT) + if (!mu_util_program_in_path ("gnuplot")) + return FALSE; + + return TRUE; +} + + +gboolean +mu_util_program_in_path (const char *prog) +{ + gchar *path; + + g_return_val_if_fail (prog, FALSE); + + path = g_find_program_in_path (prog); + g_free (path); + + return (path != NULL) ? TRUE : FALSE; +} + + +/* + * Set the child to a group leader to avoid being killed when the + * parent group is killed. + */ +static void +maybe_setsid (G_GNUC_UNUSED gpointer user_data) +{ +#if HAVE_SETSID + setsid(); +#endif /*HAVE_SETSID*/ +} + +gboolean +mu_util_play (const char *path, gboolean allow_local, gboolean allow_remote, + GError **err) +{ + gboolean rv; + const gchar *argv[3]; + const char *prog; + + g_return_val_if_fail (path, FALSE); + g_return_val_if_fail (mu_util_is_local_file (path) || allow_remote, + FALSE); + g_return_val_if_fail (!mu_util_is_local_file (path) || allow_local, + FALSE); + + prog = g_getenv ("MU_PLAY_PROGRAM"); + if (!prog) { +#ifdef __APPLE__ + prog = "open"; +#else + prog = "xdg-open"; +#endif /*!__APPLE__*/ + } + + if (!mu_util_program_in_path (prog)) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_EXECUTE, + "cannot find '%s' in path", prog); + return FALSE; + } + + argv[0] = prog; + argv[1] = path; + argv[2] = NULL; + + err = NULL; + rv = g_spawn_async (NULL, (gchar**)&argv, NULL, + G_SPAWN_SEARCH_PATH, maybe_setsid, + NULL, NULL, err); + return rv; +} + + +unsigned char +mu_util_get_dtype_with_lstat (const char *path) +{ + struct stat statbuf; + + g_return_val_if_fail (path, DT_UNKNOWN); + + if (lstat (path, &statbuf) != 0) { + g_warning ("stat failed on %s: %s", path, strerror(errno)); + return DT_UNKNOWN; + } + + /* we only care about dirs, regular files and links */ + if (S_ISREG (statbuf.st_mode)) + return DT_REG; + else if (S_ISDIR (statbuf.st_mode)) + return DT_DIR; + else if (S_ISLNK (statbuf.st_mode)) + return DT_LNK; + + return DT_UNKNOWN; +} + + +gboolean +mu_util_locale_is_utf8 (void) +{ + const gchar *dummy; + static int is_utf8 = -1; + + if (G_UNLIKELY(is_utf8 == -1)) + is_utf8 = g_get_charset(&dummy) ? 1 : 0; + + return is_utf8 ? TRUE : FALSE; +} + +gboolean +mu_util_fputs_encoded (const char *str, FILE *stream) +{ + int rv; + char *conv; + + g_return_val_if_fail (stream, FALSE); + + /* g_get_charset return TRUE when the locale is UTF8 */ + if (mu_util_locale_is_utf8()) + return fputs (str, stream) == EOF ? FALSE : TRUE; + + /* charset is _not_ utf8, so we need to convert it */ + conv = NULL; + if (g_utf8_validate (str, -1, NULL)) + conv = g_locale_from_utf8 (str, -1, NULL, NULL, NULL); + + /* conversion failed; this happens because is some cases GMime may gives + * us non-UTF-8 strings from e.g. wrongly encoded message-subjects; if + * so, we escape the string */ + conv = conv ? conv : g_strescape (str, "\n\t"); + rv = conv ? fputs (conv, stream) : EOF; + g_free (conv); + + return (rv == EOF) ? FALSE : TRUE; +} + + + +gboolean +mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...) +{ + va_list ap; + char *msg; + + /* don't bother with NULL errors, or errors already set */ + if (!err || *err) + return FALSE; + + msg = NULL; + va_start (ap, frm); + g_vasprintf (&msg, frm, ap); + va_end (ap); + + g_set_error (err, MU_ERROR_DOMAIN, errcode, "%s", msg); + + g_free (msg); + + return FALSE; +} + + + +__attribute__((format(printf, 2, 0))) static gboolean +print_args (FILE *stream, const char *frm, va_list args) +{ + gchar *str; + gboolean rv; + + str = g_strdup_vprintf (frm, args); + + rv = mu_util_fputs_encoded (str, stream); + + g_free (str); + + return rv; +} + + +gboolean +mu_util_print_encoded (const char *frm, ...) +{ + va_list args; + gboolean rv; + + g_return_val_if_fail (frm, FALSE); + + va_start (args, frm); + rv = print_args (stdout, frm, args); + va_end (args); + + return rv; +} + +gboolean +mu_util_printerr_encoded (const char *frm, ...) +{ + va_list args; + gboolean rv; + + g_return_val_if_fail (frm, FALSE); + + va_start (args, frm); + rv = print_args (stderr, frm, args); + va_end (args); + + return rv; +} + + +char* +mu_util_read_password (const char *prompt) +{ + char *pass; + + g_return_val_if_fail (prompt, NULL); + + /* note: getpass is obsolete; replace with something better */ + + pass = getpass (prompt); /* returns static mem, don't free */ + if (!pass) { + if (errno) + g_warning ("error: %s", strerror(errno)); + return NULL; + } + + return g_strdup (pass); +} diff --git a/lib/utils/mu-util.h b/lib/utils/mu-util.h new file mode 100644 index 0000000..72be341 --- /dev/null +++ b/lib/utils/mu-util.h @@ -0,0 +1,444 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 3 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_UTIL_H__ +#define __MU_UTIL_H__ + +#include +#include +#include +#include /* for mode_t */ + +/* hopefully, this should get us a sane PATH_MAX */ +#include +/* not all systems provide PATH_MAX in limits.h */ +#ifndef PATH_MAX +#include +#ifndef PATH_MAX +#define PATH_MAX MAXPATHLEN +#endif /*!PATH_MAX*/ +#endif /*PATH_MAX*/ + +G_BEGIN_DECLS + +/** + * get the expanded path; ie. perform shell expansion on the path. the + * path does not have to exist + * + * @param path path to expand + * + * @return the expanded path as a newly allocated string, or NULL in + * case of error + */ +char* mu_util_dir_expand (const char* path) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * guess the maildir; first try $MAILDIR; if it is unset or + * non-existent, try ~/Maildir if both fail, return NULL + * + * @return full path of the guessed Maildir, or NULL; must be freed (gfree) + */ +char* mu_util_guess_maildir (void) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * if path exists, check that's a read/writeable dir; otherwise try to + * create it (with perms 0700) + * + * @param path path to the dir + * @param mode to set for the dir (as per chmod(1)) + * @param nowarn, if TRUE, don't write warnings (if any) to stderr + * + * @return TRUE if a read/writeable directory `path' exists after + * leaving this function, FALSE otherwise + */ +gboolean mu_util_create_dir_maybe (const gchar *path, mode_t mode, + gboolean nowarn) G_GNUC_WARN_UNUSED_RESULT; + +/** + * check whether path is a directory, and optionally, if it's readable + * and/or writeable + * + * @param path dir path + * @param readable check for readability + * @param writeable check for writability + * + * @return TRUE if dir exist and has the specified properties + */ +gboolean mu_util_check_dir (const gchar* path, gboolean readable, + gboolean writeable) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * get our the cache directory, typically, /tmp/mu-/ + * + * @return the cache directory; don't free + */ +const char* mu_util_cache_dir (void) G_GNUC_CONST; + +/** + * create a writeable file and return its file descriptor (which + * you'll need to close(2) when done with it.) + * + * @param path the full path of the file to create + * @param the mode to open (ie. 0644 or 0600 etc., see chmod(3) + * @param overwrite should we allow for overwriting existing files? + * + * @return a file descriptor, or -1 in case of error. If it's a file + * system error, 'errno' may contain more info. use 'close()' when done + * with the file descriptor + */ +int mu_util_create_writeable_fd (const char* path, mode_t mode, + gboolean overwrite) + G_GNUC_WARN_UNUSED_RESULT; + + +/** + * check if file is local, ie. on the local file system. this means + * that it's either having a file URI, *or* that it's an existing file + * + * @param path a path + * + * @return TRUE if the file is local, FALSE otherwise + */ +gboolean mu_util_is_local_file (const char* path); + + +/** + * is the current locale utf-8 compatible? + * + * @return TRUE if it's utf8 compatible, FALSE otherwise + */ +gboolean mu_util_locale_is_utf8 (void) G_GNUC_CONST; + +/** + * write a string (assumed to be in utf8-format) to a stream, + * converted to the current locale + * + * @param str a string + * @param stream a stream + * + * @return TRUE if printing worked, FALSE otherwise + */ +gboolean mu_util_fputs_encoded (const char *str, FILE *stream); + +/** + * print a formatted string (assumed to be in utf8-format) to stdout, + * converted to the current locale + * + * @param a standard printf() format string, followed by a parameter list + * + * @return TRUE if printing worked, FALSE otherwise + */ +gboolean mu_util_print_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2); + +/** + * print a formatted string (assumed to be in utf8-format) to stderr, + * converted to the current locale + * + * @param a standard printf() format string, followed by a parameter list + * + * @return TRUE if printing worked, FALSE otherwise + */ +gboolean mu_util_printerr_encoded (const char *frm, ...) G_GNUC_PRINTF(1,2); + + +/** + * read a password from stdin (without echoing), and return it. + * + * @param prompt the prompt text before the password + * + * @return the password (free with g_free), or NULL + */ +char* mu_util_read_password (const char *prompt) + G_GNUC_MALLOC G_GNUC_WARN_UNUSED_RESULT; + +/** + * Try to 'play' (ie., open with it's associated program) a file. On + * MacOS, the the program 'open' is used for this; on other platforms + * 'xdg-open' to do the actual opening. In addition you can set it to another program + * by setting the MU_PLAY_PROGRAM environment variable + * + * @param path full path of the file to open + * @param allow_local allow local files (ie. with file:// prefix or fs paths) + * @param allow_remote allow URIs (ie., http, mailto) + * @param err receives error information, if any + * + * @return TRUE if it succeeded, FALSE otherwise + */ +gboolean mu_util_play (const char *path, gboolean allow_local, + gboolean allow_remote, GError **err); + +/** + * Check whether program prog exists in PATH + * + * @param prog a program (executable) + * + * @return TRUE if it exists and is executable, FALSE otherwise + */ +gboolean mu_util_program_in_path (const char *prog); + + + +enum _MuFeature { + MU_FEATURE_GUILE = 1 << 0, /* do we support Guile 2.0? */ + MU_FEATURE_GNUPLOT = 1 << 1, /* do we have gnuplot installed? */ + MU_FEATURE_CRYPTO = 1 << 2 /* do we support crypto (Gmime >= 2.6) */ +}; +typedef enum _MuFeature MuFeature; + + +/** + * Check whether mu supports some particular feature + * + * @param feature a feature (multiple features can be logical-or'd together) + * + * @return TRUE if the feature is supported, FALSE otherwise + */ +gboolean mu_util_supports (MuFeature feature); + + + +/** + * Get an error-query for mu, to be used in `g_set_error'. Recent + * version of Glib warn when using 0 for the error-domain in + * g_set_error. + * + * + * @return an error quark for mu + */ +GQuark mu_util_error_quark (void) G_GNUC_CONST; +#define MU_ERROR_DOMAIN (mu_util_error_quark()) + + +/* + * for OSs with out support for direntry->d_type, like Solaris + */ +#ifndef DT_UNKNOWN +enum { + DT_UNKNOWN = 0, +#define DT_UNKNOWN DT_UNKNOWN + DT_FIFO = 1, +#define DT_FIFO DT_FIFO + DT_CHR = 2, +#define DT_CHR DT_CHR + DT_DIR = 4, +#define DT_DIR DT_DIR + DT_BLK = 6, +#define DT_BLK DT_BLK + DT_REG = 8, +#define DT_REG DT_REG + DT_LNK = 10, +#define DT_LNK DT_LNK + DT_SOCK = 12, +#define DT_SOCK DT_SOCK + DT_WHT = 14 +#define DT_WHT DT_WHT +}; +#endif /*DT_UNKNOWN*/ + + +/** + * get the d_type (as in direntry->d_type) for the file at path, using +* lstat(3) + * + * @param path full path + * + * @return DT_REG, DT_DIR, DT_LNK, or DT_UNKNOWN (other values are not + * supported currently ) + */ +unsigned char mu_util_get_dtype_with_lstat (const char *path); + + +/** + * we need this when using Xapian::Document* from C + * + */ +typedef gpointer XapianDocument; + +/** + * we need this when using Xapian::Enquire* from C + * + */ +typedef gpointer XapianEnquire; + + +/* print a warning for a GError, and free it */ +#define MU_HANDLE_G_ERROR(GE) \ + do { \ + if (!(GE)) \ + g_warning ("%s:%u: an error occurred in %s", \ + __FILE__, __LINE__, __func__); \ + else { \ + g_warning ("error %u: %s", (GE)->code, (GE)->message); \ + g_error_free ((GE)); \ + } \ + } while (0) + + + +/** + * log something in the log file; note, we use G_LOG_LEVEL_INFO + * for such messages + */ +#define MU_WRITE_LOG(...) \ + G_STMT_START { \ + g_log (G_LOG_DOMAIN, \ + G_LOG_LEVEL_INFO, \ + __VA_ARGS__); \ + } G_STMT_END + + + +#define MU_G_ERROR_CODE(GE) ((GE)&&(*(GE))?(*(GE))->code:MU_ERROR) + + +enum _MuError { + /* no error at all! */ + MU_OK = 0, + + /* generic error */ + MU_ERROR = 1, + MU_ERROR_IN_PARAMETERS = 2, + MU_ERROR_INTERNAL = 3, + MU_ERROR_NO_MATCHES = 4, + + /* not really an error; for callbacks */ + MU_IGNORE = 5, + + MU_ERROR_SCRIPT_NOT_FOUND = 8, + + /* general xapian related error */ + MU_ERROR_XAPIAN = 11, + + /* (parsing) error in the query */ + MU_ERROR_XAPIAN_QUERY = 13, + + /* missing data for a document */ + MU_ERROR_XAPIAN_MISSING_DATA = 17, + /* can't get write lock */ + MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK = 19, + /* could not write */ + MU_ERROR_XAPIAN_STORE_FAILED = 21, + /* could not remove */ + MU_ERROR_XAPIAN_REMOVE_FAILED = 22, + /* database was modified; reload */ + MU_ERROR_XAPIAN_MODIFIED = 23, + /* database was modified; reload */ + MU_ERROR_XAPIAN_NEEDS_REINDEX = 24, + /* database schema version doesn't match */ + MU_ERROR_XAPIAN_SCHEMA_MISMATCH = 25, + /* failed to open the database */ + MU_ERROR_XAPIAN_CANNOT_OPEN = 26, + + /* GMime related errors */ + + /* gmime parsing related error */ + MU_ERROR_GMIME = 30, + + /* contacts related errors */ + MU_ERROR_CONTACTS = 50, + MU_ERROR_CONTACTS_CANNOT_RETRIEVE = 51, + + /* crypto related errors */ + MU_ERROR_CRYPTO = 60, + + + /* File errors */ + /* generic file-related error */ + MU_ERROR_FILE = 70, + MU_ERROR_FILE_INVALID_NAME = 71, + MU_ERROR_FILE_CANNOT_LINK = 72, + MU_ERROR_FILE_CANNOT_OPEN = 73, + MU_ERROR_FILE_CANNOT_READ = 74, + MU_ERROR_FILE_CANNOT_EXECUTE = 75, + MU_ERROR_FILE_CANNOT_CREATE = 76, + MU_ERROR_FILE_CANNOT_MKDIR = 77, + MU_ERROR_FILE_STAT_FAILED = 78, + MU_ERROR_FILE_READDIR_FAILED = 79, + MU_ERROR_FILE_INVALID_SOURCE = 80, + MU_ERROR_FILE_TARGET_EQUALS_SOURCE = 81, + MU_ERROR_FILE_CANNOT_WRITE = 82, + MU_ERROR_FILE_CANNOT_UNLINK = 83, + + /* not really an error, used in callbacks */ + MU_STOP = 99 +}; +typedef enum _MuError MuError; + + +/** + * set an error if it's not already set, and return FALSE + * + * @param err errptr, or NULL + * @param errcode error code + * @param frm printf-style format, followed by parameters + * + * @return FALSE + */ +gboolean mu_util_g_set_error (GError **err, MuError errcode, const char *frm, ...) + G_GNUC_PRINTF(3,4); + +/** + * calculate a 64-bit hash for the given string, based on a combination of the + * DJB and BKDR hash functions. + * + * @param a string + * + * @return the hash + */ +static inline guint64 +mu_util_get_hash (const char* str) +{ + guint32 djbhash; + guint32 bkdrhash; + guint32 bkdrseed; + guint64 hash; + + djbhash = 5381; + bkdrhash = 0; + bkdrseed = 1313; + + for(unsigned u = 0U; str[u]; ++u) { + djbhash = ((djbhash << 5) + djbhash) + str[u]; + bkdrhash = bkdrhash * bkdrseed + str[u]; + } + + hash = djbhash; + return (hash<<32) | bkdrhash; +} + + + + + + +#define MU_COLOR_RED "\x1b[31m" +#define MU_COLOR_GREEN "\x1b[32m" +#define MU_COLOR_YELLOW "\x1b[33m" +#define MU_COLOR_BLUE "\x1b[34m" +#define MU_COLOR_MAGENTA "\x1b[35m" +#define MU_COLOR_CYAN "\x1b[36m" +#define MU_COLOR_DEFAULT "\x1b[0m" + +G_END_DECLS + +#endif /*__MU_UTIL_H__*/ diff --git a/lib/utils/mu-utils.cc b/lib/utils/mu-utils.cc new file mode 100644 index 0000000..2354c0c --- /dev/null +++ b/lib/utils/mu-utils.cc @@ -0,0 +1,471 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + + +#define _XOPEN_SOURCE +#include + +#define GNU_SOURCE +#include +#include + +#include +#include +#include + +#include +#include + +#include "mu-utils.hh" + + +using namespace Mu; + +namespace { + +static gunichar +unichar_tolower (gunichar uc) +{ + if (!g_unichar_isalpha(uc)) + return uc; + + if (g_unichar_get_script (uc) != G_UNICODE_SCRIPT_LATIN) + return g_unichar_tolower (uc); + + switch (uc) + { + case 0x00e6: + case 0x00c6: return 'e'; /* æ */ + case 0x00f8: return 'o'; /* ø */ + case 0x0110: + case 0x0111: return 'd'; /* đ */ + /* todo: many more */ + default: return g_unichar_tolower (uc); + } +} + +/** + * gx_utf8_flatten: + * @str: a UTF-8 string + * @len: the length of @str, or -1 if it is %NULL-terminated + * + * Flatten some UTF-8 string; that is, downcase it and remove any diacritics. + * + * Returns: (transfer full): a flattened string, free with g_free(). + */ +static char* +gx_utf8_flatten (const gchar *str, gssize len) +{ + GString *gstr; + char *norm, *cur; + + g_return_val_if_fail (str, NULL); + + norm = g_utf8_normalize (str, len, G_NORMALIZE_ALL); + if (!norm) + return NULL; + + gstr = g_string_sized_new (strlen (norm)); + + for (cur = norm; cur && *cur; cur = g_utf8_next_char (cur)) + { + gunichar uc; + + uc = g_utf8_get_char (cur); + if (g_unichar_combining_class (uc) != 0) + continue; + + g_string_append_unichar (gstr, unichar_tolower(uc)); + } + + g_free (norm); + + return g_string_free (gstr, FALSE); +} + +} // namespace + +std::string // gx_utf8_flatten +Mu::utf8_flatten (const char *str) +{ + if (!str) + return {}; + + // the pure-ascii case + if (g_str_is_ascii(str)) { + auto l = g_ascii_strdown (str, -1); + std::string s{l}; + g_free (l); + return s; + } + + // seems we need the big guns + char *flat = gx_utf8_flatten (str, -1); + if (!flat) + return {}; + + std::string s{flat}; + g_free (flat); + + return s; +} + +std::string +Mu::utf8_clean (const std::string& dirty) +{ + GString *gstr = g_string_sized_new (dirty.length()); + + for (auto cur = dirty.c_str(); cur && *cur; cur = g_utf8_next_char (cur)) { + + const gunichar uc = g_utf8_get_char (cur); + if (g_unichar_iscntrl (uc)) + g_string_append_c (gstr, ' '); + else + g_string_append_unichar (gstr, uc); + } + + std::string clean(gstr->str, gstr->len); + g_string_free (gstr, TRUE); + + clean.erase (0, clean.find_first_not_of(" ")); + clean.erase (clean.find_last_not_of(" ") + 1); // remove trailing space + + return clean; +} + +std::vector +Mu::split (const std::string& str, const std::string& sepa) +{ + char **parts = g_strsplit(str.c_str(), sepa.c_str(), -1); + std::vector vec; + for (auto part = parts; part && *part; ++part) + vec.push_back (*part); + + g_strfreev(parts); + + return vec; +} + +std::string +Mu::quote (const std::string& str) +{ + char *s = g_strescape (str.c_str(), NULL); + if (!s) + return {}; + + std::string res (s); + g_free (s); + + return "\"" + res + "\""; +} + + std::string + Mu::format (const char *frm, ...) + { + va_list args; + + va_start (args, frm); + auto str = format(frm, args); + va_end (args); + + return str; + } + +std::string +Mu::format (const char *frm, va_list args) +{ + char *s{}; + const auto res = g_vasprintf (&s, frm, args); + if (res == -1) { + std::cerr << "string format failed" << std::endl; + return {}; + } + + std::string str{s}; + g_free (s); + + return str; +} + + +constexpr const auto InternalDateFormat = "%010" G_GINT64_FORMAT; +constexpr const char InternalDateMin[] = "0000000000"; +constexpr const char InternalDateMax[] = "9999999999"; +static_assert(sizeof(InternalDateMin) == 10 + 1, "invalid"); +static_assert(sizeof(InternalDateMax) == 10 + 1, "invalid"); + +static std::string +date_boundary (bool is_first) +{ + return is_first ? InternalDateMin : InternalDateMax; +} + +std::string +Mu::date_to_time_t_string (int64_t t) +{ + char buf[sizeof(InternalDateMax)]; + g_snprintf (buf, sizeof(buf), InternalDateFormat, t); + + return buf; +} + +static std::string +delta_ymwdhMs (const std::string& expr) +{ + char *endptr; + auto num = strtol (expr.c_str(), &endptr, 10); + if (num <= 0 || num > 9999 || !endptr || !*endptr) + return date_boundary (true); + + int years, months, weeks, days, hours, minutes, seconds; + years = months = weeks = days = hours = minutes = seconds = 0; + + switch (endptr[0]) { + case 's': seconds = num; break; + case 'M': minutes = num; break; + case 'h': hours = num; break; + case 'd': days = num; break; + case 'w': weeks = num; break; + case 'm': months = num; break; + case 'y': years = num; break; + default: + return date_boundary (true); + } + + GDateTime *then, *now = g_date_time_new_now_local (); + if (weeks != 0) + then = g_date_time_add_weeks (now, -weeks); + else + then = g_date_time_add_full (now, -years, -months,-days, + -hours, -minutes, -seconds); + + time_t t = MAX (0, (gint64)g_date_time_to_unix (then)); + + g_date_time_unref (then); + g_date_time_unref (now); + + return date_to_time_t_string (t); +} + +static std::string +special_date (const std::string& d, bool is_first) +{ + if (d == "now") + return date_to_time_t_string (time(NULL)); + + else if (d == "today") { + + GDateTime *dt, *midnight; + dt = g_date_time_new_now_local (); + + if (!is_first) { + GDateTime *tmp = dt; + dt = g_date_time_add_days (dt, 1); + g_date_time_unref (tmp); + } + + midnight = g_date_time_add_full (dt, 0, 0, 0, + -g_date_time_get_hour(dt), + -g_date_time_get_minute (dt), + -g_date_time_get_second (dt)); + time_t t = MAX(0, (gint64)g_date_time_to_unix (midnight)); + g_date_time_unref (dt); + g_date_time_unref (midnight); + return date_to_time_t_string ((time_t)t); + + } else + return date_boundary (is_first); +} + +// if a date has a month day greater than the number of days in that month, +// change it to a valid date point to the last second in that month +static void +fixup_month (struct tm *tbuf) +{ + decltype(tbuf->tm_mday) max_days; + const auto month = tbuf->tm_mon + 1; + const auto year = tbuf->tm_year + 1900; + + switch (month) { + case 2: + if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) + max_days = 29; + else + max_days = 28; + break; + case 4: + case 6: + case 9: + case 11: + max_days = 30; + break; + default: + max_days = 31; + break; + } + + if (tbuf->tm_mday > max_days) { + tbuf->tm_mday = max_days; + tbuf->tm_hour = 23; + tbuf->tm_min = 59; + tbuf->tm_sec = 59; + } +} + +std::string +Mu::date_to_time_t_string (const std::string& dstr, bool is_first) +{ + gint64 t; + struct tm tbuf; + GDateTime *dtime; + + /* one-sided dates */ + if (dstr.empty()) + return date_boundary (is_first); + else if (dstr == "today" || dstr == "now") + return special_date (dstr, is_first); + + else if (dstr.find_first_of("ymdwhMs") != std::string::npos) + return delta_ymwdhMs (dstr); + + constexpr char UserDateMin[] = "19700101000000"; + constexpr char UserDateMax[] = "29991231235959"; + + std::string date (is_first ? UserDateMin : UserDateMax); + std::copy_if (dstr.begin(), dstr.end(), date.begin(),[](auto c){return isdigit(c);}); + + memset (&tbuf, 0, sizeof tbuf); + if (!strptime (date.c_str(), "%Y%m%d%H%M%S", &tbuf) && + !strptime (date.c_str(), "%Y%m%d%H%M", &tbuf) && + !strptime (date.c_str(), "%Y%m%d", &tbuf) && + !strptime (date.c_str(), "%Y%m", &tbuf) && + !strptime (date.c_str(), "%Y", &tbuf)) + return date_boundary (is_first); + + fixup_month(&tbuf); + + dtime = g_date_time_new_local (tbuf.tm_year + 1900, + tbuf.tm_mon + 1, + tbuf.tm_mday, + tbuf.tm_hour, + tbuf.tm_min, + tbuf.tm_sec); + if (!dtime) { + g_warning ("invalid %s date '%s'", + is_first ? "lower" : "upper", date.c_str()); + return date_boundary (is_first); + } + + t = g_date_time_to_unix (dtime); + g_date_time_unref (dtime); + + if (t < 0 || t > 9999999999) + return date_boundary (is_first); + else + return date_to_time_t_string (t); +} + +constexpr const auto SizeFormat = "%010" G_GINT64_FORMAT; + +constexpr const char SizeMin[] = "0000000000"; +constexpr const char SizeMax[] = "9999999999"; +static_assert(sizeof(SizeMin) == 10 + 1, "invalid"); +static_assert(sizeof(SizeMax) == 10 + 1, "invalid"); + +static std::string +size_boundary (bool is_first) +{ + return is_first ? SizeMin : SizeMax; +} + +std::string +Mu::size_to_string (int64_t size) +{ + char buf[sizeof(SizeMax)]; + g_snprintf (buf, sizeof(buf), SizeFormat, size); + + return buf; +} + +std::string +Mu::size_to_string (const std::string& val, bool is_first) +{ + std::string str; + GRegex *rx; + GMatchInfo *minfo; + + /* one-sided ranges */ + if (val.empty()) + return size_boundary (is_first); + + rx = g_regex_new ("(\\d+)(b|k|kb|m|mb|g|gb)?", + G_REGEX_CASELESS, (GRegexMatchFlags)0, NULL); + minfo = NULL; + if (g_regex_match (rx, val.c_str(), (GRegexMatchFlags)0, &minfo)) { + gint64 size; + char *s; + + s = g_match_info_fetch (minfo, 1); + size = atoll (s); + g_free (s); + + s = g_match_info_fetch (minfo, 2); + switch (s ? g_ascii_tolower(s[0]) : 0) { + case 'k': size *= 1024; break; + case 'm': size *= (1024 * 1024); break; + case 'g': size *= (1024 * 1024 * 1024); break; + default: break; + } + + g_free (s); + str = size_to_string (size); + } else + str = size_boundary (is_first); + + g_regex_unref (rx); + g_match_info_unref (minfo); + + return str; +} + +void +Mu::assert_equal(const std::string& s1, const std::string& s2) +{ + g_assert_cmpstr (s1.c_str(), ==, s2.c_str()); +} + +void +Mu::assert_equal (const Mu::StringVec& v1, const Mu::StringVec& v2) +{ + g_assert_cmpuint(v1.size(), ==, v2.size()); + + for (auto i = 0U; i != v1.size(); ++i) + assert_equal(v1[i], v2[i]); +} + + +void +Mu::allow_warnings() +{ + g_test_log_set_fatal_handler( + [](const char*, GLogLevelFlags, const char*, gpointer) { + return FALSE; + },{}); + +} diff --git a/lib/utils/mu-utils.hh b/lib/utils/mu-utils.hh new file mode 100644 index 0000000..0c35170 --- /dev/null +++ b/lib/utils/mu-utils.hh @@ -0,0 +1,271 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#ifndef __MU_UTILS_HH__ +#define __MU_UTILS_HH__ + +#include +#include +#include +#include +#include +#include + +namespace Mu { + +using StringVec = std::vector; + +/** + * Flatten a string -- downcase and fold diacritics etc. + * + * @param str a string + * + * @return a flattened string + */ +std::string utf8_flatten (const char *str); +inline std::string utf8_flatten (const std::string& s) { return utf8_flatten(s.c_str()); } + + + +/** + * Replace all control characters with spaces, and remove leading and trailing space. + * + * @param dirty an unclean string + * + * @return a cleaned-up string. + */ +std::string utf8_clean (const std::string& dirty); + + +/** + * Split a string in parts + * + * @param str a string + * @param sepa the separator + * + * @return the parts. + */ +std::vector split (const std::string& str, + const std::string& sepa); + +/** + * Quote & escape a string + * + * @param str a string + * + * @return quoted string + */ +std::string quote (const std::string& str); + +/** + * Format a string, printf style + * + * @param frm format string + * @param ... parameters + * + * @return a formatted string + */ +std::string format (const char *frm, ...) __attribute__((format(printf, 1, 2))); + +/** + * Format a string, printf style + * + * @param frm format string + * @param ... parameters + * + * @return a formatted string + */ +std::string format (const char *frm, va_list args) __attribute__((format(printf, 1, 0))); + + +/** + * Convert an date to the corresponding time expressed as a string with a + * 10-digit time_t + * + * @param date the date expressed a YYYYMMDDHHMMSS or any n... of the first + * characters. + * @param first whether to fill out incomplete dates to the start or the end; + * ie. either 1972 -> 197201010000 or 1972 -> 197212312359 + * + * @return the corresponding time_t expressed as a string + */ +std::string date_to_time_t_string (const std::string& date, bool first); + +/** + * 64-bit incarnation of time_t expressed as a 10-digit string. Uses 64-bit for the time-value, + * regardless of the size of time_t. + * + * @param t some time value + * + * @return + */ +std::string date_to_time_t_string (int64_t t); + + + +/** + * Convert a size string to a size in bytes + * + * @param sizestr the size string + * @param first + * + * @return the size expressed as a string with the decimal number of bytes + */ +std::string size_to_string (const std::string& sizestr, bool first); + +/** + * Convert a size into a size in bytes string + * + * @param size the size + * @param first + * + * @return the size expressed as a string with the decimal number of bytes + */ +std::string size_to_string (int64_t size); + + +/** + * Convert any ostreamable<< value to a string + * + * @param t the value + * + * @return a std::string + */ +template +static inline std::string to_string (const T& val) +{ + std::stringstream sstr; + sstr << val; + + return sstr.str(); +} + + +/** + * + * don't repeat these catch blocks everywhere... + * + */ + +#define MU_XAPIAN_CATCH_BLOCK \ + catch (const Xapian::Error &xerr) { \ + g_critical ("%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + } catch (const std::runtime_error& re) { \ + g_critical ("%s: error: %s", __func__, re.what()); \ + } catch (...) { \ + g_critical ("%s: caught exception", __func__); \ + } + +#define MU_XAPIAN_CATCH_BLOCK_G_ERROR(GE,E) \ + catch (const Xapian::DatabaseLockError &xerr) { \ + mu_util_g_set_error ((GE), \ + MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, \ + "%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + } catch (const Xapian::DatabaseError &xerr) { \ + mu_util_g_set_error ((GE),MU_ERROR_XAPIAN, \ + "%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + } catch (const Xapian::Error &xerr) { \ + mu_util_g_set_error ((GE),(E), \ + "%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + } catch (const std::runtime_error& ex) { \ + mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL), \ + "%s: error: %s", __func__, ex.what()); \ + \ + } catch (...) { \ + mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL), \ + "%s: caught exception", __func__); \ + } + + +#define MU_XAPIAN_CATCH_BLOCK_RETURN(R) \ + catch (const Xapian::Error &xerr) { \ + g_critical ("%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + return (R); \ + } catch (const std::runtime_error& ex) { \ + g_critical("%s: error: %s", __func__, ex.what()); \ + return (R); \ + } catch (...) { \ + g_critical ("%s: caught exception", __func__); \ + return (R); \ + } + +#define MU_XAPIAN_CATCH_BLOCK_G_ERROR_RETURN(GE,E,R) \ + catch (const Xapian::Error &xerr) { \ + mu_util_g_set_error ((GE),(E), \ + "%s: xapian error '%s'", \ + __func__, xerr.get_msg().c_str()); \ + return (R); \ + } catch (const std::runtime_error& ex) { \ + mu_util_g_set_error ((GE),(MU_ERROR_INTERNAL), \ + "%s: error: %s", __func__, ex.what()); \ + return (R); \ + } catch (...) { \ + if ((GE)&&!(*(GE))) \ + mu_util_g_set_error ((GE), \ + (MU_ERROR_INTERNAL), \ + "%s: caught exception", __func__); \ + return (R); \ + } + + + + +/// Allow using enum structs as bitflags +#define MU_TO_NUM(ET,ELM) std::underlying_type_t(ELM) +#define MU_TO_ENUM(ET,NUM) static_cast(NUM) +#define MU_ENABLE_BITOPS(ET) \ + constexpr ET operator& (ET e1, ET e2) { return MU_TO_ENUM(ET,MU_TO_NUM(ET,e1)&MU_TO_NUM(ET,e2)); } \ + constexpr ET operator| (ET e1, ET e2) { return MU_TO_ENUM(ET,MU_TO_NUM(ET,e1)|MU_TO_NUM(ET,e2)); } \ + constexpr ET operator~ (ET e) { return MU_TO_ENUM(ET,~(MU_TO_NUM(ET, e))); } \ + constexpr bool any_of(ET e) { return MU_TO_NUM(ET,e) != 0; } \ + constexpr bool none_of(ET e) { return MU_TO_NUM(ET,e) == 0; } \ + static inline ET& operator&=(ET& e1, ET e2) { return e1 = e1 & e2;} \ + static inline ET& operator|=(ET& e1, ET e2) { return e1 = e1 | e2;} + + +/** + * For unit tests, assert two std::string's are equal. + * + * @param s1 string1 + * @param s2 string2 + */ +void assert_equal(const std::string& s1, const std::string& s2); +/** + * For unit tests, assert that to containers are the same. + * + * @param c1 container1 + * @param c2 container2 + */ +void assert_equal (const StringVec& v1, const StringVec& v2); + +/** + * For unit-tests, allow warnings in the current function. + * + */ +void allow_warnings(); + +} // namespace Mu + + +#endif /* __MU_UTILS_HH__ */ diff --git a/lib/utils/test-command-parser.cc b/lib/utils/test-command-parser.cc new file mode 100644 index 0000000..f18e642 --- /dev/null +++ b/lib/utils/test-command-parser.cc @@ -0,0 +1,147 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include + +#include "mu-command-parser.hh" +#include "mu-utils.hh" + +using namespace Mu; + +static void +test_param_getters() +{ + const auto node { Sexp::parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")}; + + std::cout << node << "\n"; + + g_assert_cmpint(Command::get_int_or(node.children,"bar"), ==, 123); + assert_equal(Command::get_string_or(node.children, "bra", "bla"), "bla"); + assert_equal(Command::get_string_or(node.children, "cuux"), "456"); + + g_assert_true(Command::get_bool_or(node.children,"boo") == false); + g_assert_true(Command::get_bool_or(node.children,"bah") == true); +} + + +static bool +call (const Command::CommandMap& cmap, const std::string& sexp) try +{ + const auto node{Sexp::parse(sexp)}; + g_message ("invoking %s", to_string(node).c_str()); + + invoke (cmap, node); + + return true; + +} catch (const Error& err) { + g_warning ("%s", err.what()); + return false; +} + +static void +test_command() +{ + using namespace Command; + allow_warnings(); + + CommandMap cmap; + + + cmap.emplace("my-command", + CommandInfo{ + ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }}, + {"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}}, + "My command,", + {}}); + + //std::cout << cmap << "\n"; + + g_assert_true(call(cmap, "(my-command :param1 \"hello\")")); + g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)")); + + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)")); +} + +static void +test_command2() +{ + using namespace Command; + allow_warnings(); + + CommandMap cmap; + cmap.emplace("bla", + CommandInfo{ + ArgMap{ + {"foo", ArgInfo{Sexp::Type::Integer, false, "foo"}}, + {"bar", ArgInfo{Sexp::Type::String, false, "bar"}}, + },"yeah", + [&](const auto& params){}}); + + + g_assert_true (call(cmap, "(bla :foo nil)")); + g_assert_false (call(cmap, "(bla :foo nil :bla nil)")); +} + + +static void +test_command_fail() +{ + using namespace Command; + + allow_warnings(); + + CommandMap cmap; + + cmap.emplace("my-command", + CommandInfo{ + ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }}, + {"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}}, + "My command,", + {}}); + + g_assert_false (call(cmap, "(my-command)")); + g_assert_false (call(cmap, "(my-command2)")); + g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)")); + g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")")); + +} + + +int +main (int argc, char *argv[]) try +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/utils/command-parser/param-getters", test_param_getters); + g_test_add_func ("/utils/command-parser/command", test_command); + g_test_add_func ("/utils/command-parser/command2", test_command2); + g_test_add_func ("/utils/command-parser/command-fail", test_command_fail); + + return g_test_run (); + + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/utils/test-mu-str.c b/lib/utils/test-mu-str.c new file mode 100644 index 0000000..75fc71f --- /dev/null +++ b/lib/utils/test-mu-str.c @@ -0,0 +1,275 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include +#include +#include + +#include + +#include "mu-str.h" + + +static void +test_mu_str_size_01 (void) +{ + struct lconv *lc; + char *tmp2; + + lc = localeconv(); + + g_assert_cmpstr (mu_str_size_s (0), ==, "0 bytes"); + + tmp2 = g_strdup_printf ("97%s7 KB", lc->decimal_point); + g_assert_cmpstr (mu_str_size_s (100000), ==, tmp2); + g_free (tmp2); + + tmp2 = g_strdup_printf ("1%s0 MB", lc->decimal_point); + g_assert_cmpstr (mu_str_size_s (1100*1000), ==, tmp2); + g_free (tmp2); +} + + + +static void +test_mu_str_size_02 (void) +{ + struct lconv *lc; + char *tmp1, *tmp2; + + lc = localeconv(); + + tmp2 = g_strdup_printf ("1%s0 MB", lc->decimal_point); + tmp1 = mu_str_size (999999); + g_assert_cmpstr (tmp1, !=, tmp2); + + g_free (tmp1); + g_free (tmp2); +} + +static void +test_mu_str_esc_to_list (void) +{ + int i; + struct { + const char* str; + const char* strs[3]; + } strings [] = { + { "maildir:foo", + {"maildir:foo", NULL, NULL}}, + { "maildir:sent items", + {"maildir:sent", "items", NULL}}, + { "\"maildir:sent items\"", + {"maildir:sent items", NULL, NULL}}, + }; + + for (i = 0; i != G_N_ELEMENTS(strings); ++i) { + GSList *lst, *cur; + unsigned u; + lst = mu_str_esc_to_list (strings[i].str); + for (cur = lst, u = 0; cur; cur = g_slist_next(cur), ++u) + g_assert_cmpstr ((const char*)cur->data,==, + strings[i].strs[u]); + mu_str_free_list (lst); + } +} + + + +static void +assert_cmplst (GSList *lst, const char *items[]) +{ + int i; + + if (!lst) + g_assert (!items); + + for (i = 0; lst; lst = g_slist_next(lst), ++i) + g_assert_cmpstr ((char*)lst->data,==,items[i]); + + g_assert (items[i] == NULL); +} + + +static GSList* +create_list (const char *items[]) +{ + GSList *lst; + + lst = NULL; + while (items && *items) { + lst = g_slist_prepend (lst, g_strdup(*items)); + ++items; + } + + return g_slist_reverse (lst); + +} + +static void +test_mu_str_from_list (void) +{ + { + const char *strs[] = {"aap", "noot", "mies", NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst, ','); + g_assert_cmpstr ("aap,noot,mies", ==, str); + mu_str_free_list (lst); + g_free (str); + } + + { + const char *strs[] = {"aap", "no,ot", "mies", NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst, ','); + g_assert_cmpstr ("aap,no,ot,mies", ==, str); + mu_str_free_list (lst); + g_free (str); + } + + { + const char *strs[] = {NULL}; + GSList *lst = create_list (strs); + gchar *str = mu_str_from_list (lst,'@'); + g_assert_cmpstr (NULL, ==, str); + mu_str_free_list (lst); + g_free (str); + } + + +} + + +static void +test_mu_str_to_list (void) +{ + { + const char *items[]= {"foo", "bar ", "cuux", NULL}; + GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', FALSE); + assert_cmplst (lst, items); + mu_str_free_list (lst); + } + + { + GSList *lst = mu_str_to_list (NULL,'x',FALSE); + g_assert (lst == NULL); + mu_str_free_list (lst); + } +} + +static void +test_mu_str_to_list_strip (void) +{ + const char *items[]= {"foo", "bar", "cuux", NULL}; + GSList *lst = mu_str_to_list ("foo@bar @cuux",'@', TRUE); + assert_cmplst (lst, items); + mu_str_free_list (lst); +} + + +static void +test_mu_str_replace (void) +{ + unsigned u; + struct { + const char* str; + const char* sub; + const char *repl; + const char *exp; + } strings [] = { + { "hello", "ll", "xx", "hexxo" }, + { "hello", "hello", "hi", "hi" }, + { "hello", "foo", "bar", "hello" } + }; + + for (u = 0; u != G_N_ELEMENTS(strings); ++u) { + char *res; + res = mu_str_replace (strings[u].str, + strings[u].sub, + strings[u].repl); + g_assert_cmpstr (res,==,strings[u].exp); + g_free (res); + } +} + + +static void +test_mu_str_remove_ctrl_in_place (void) +{ + unsigned u; + struct { + char *str; + const char *exp; + } strings [] = { + { g_strdup(""), ""}, + { g_strdup("hello, world!"), "hello, world!" }, + { g_strdup("hello,\tworld!"), "hello, world!" }, + { g_strdup("hello,\n\nworld!"), "hello, world!", }, + { g_strdup("hello,\x1f\x1e\x1ew\nor\nld!"), "hello,w or ld!" }, + { g_strdup("\x1ehello, world!\x1f"), "hello, world!" } + }; + + for (u = 0; u != G_N_ELEMENTS(strings); ++u) { + char *res; + res = mu_str_remove_ctrl_in_place (strings[u].str); + g_assert_cmpstr (res,==,strings[u].exp); + g_free (strings[u].str); + } +} + + +int +main (int argc, char *argv[]) +{ + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + + /* mu_str_size */ + g_test_add_func ("/mu-str/mu-str-size-01", + test_mu_str_size_01); + g_test_add_func ("/mu-str/mu-str-size-02", + test_mu_str_size_02); + + g_test_add_func ("/mu-str/mu-str-from-list", + test_mu_str_from_list); + g_test_add_func ("/mu-str/mu-str-to-list", + test_mu_str_to_list); + g_test_add_func ("/mu-str/mu-str-to-list-strip", + test_mu_str_to_list_strip); + + g_test_add_func ("/mu-str/mu-str-replace", + test_mu_str_replace); + + g_test_add_func ("/mu-str/mu-str-esc-to-list", + test_mu_str_esc_to_list); + + g_test_add_func ("/mu-str/mu_str_remove_ctrl_in_place", + test_mu_str_remove_ctrl_in_place); + + + return g_test_run (); +} diff --git a/lib/utils/test-mu-util.c b/lib/utils/test-mu-util.c new file mode 100644 index 0000000..f3b0277 --- /dev/null +++ b/lib/utils/test-mu-util.c @@ -0,0 +1,248 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include + +#include +#include +#include + +#include "mu-util.h" + +static void +test_mu_util_dir_expand_00 (void) +{ +#ifdef HAVE_WORDEXP_H + gchar *got, *expected; + + got = mu_util_dir_expand ("~/IProbablyDoNotExist"); + expected = g_strdup_printf ("%s%cIProbablyDoNotExist", + getenv("HOME"), G_DIR_SEPARATOR); + + g_assert_cmpstr (got,==,expected); + + g_free (got); + g_free (expected); +#endif /*HAVE_WORDEXP_H*/ +} + +static void +test_mu_util_dir_expand_01 (void) +{ + /* XXXX: the testcase does not work when using some dir + * setups; (see issue #585), although the code should still + * work. Turn of the test for now */ + return; + + +#ifdef HAVE_WORDEXP_H + { + gchar *got, *expected; + + got = mu_util_dir_expand ("~/Desktop"); + expected = g_strdup_printf ("%s%cDesktop", + getenv("HOME"), G_DIR_SEPARATOR); + + g_assert_cmpstr (got,==,expected); + + g_free (got); + g_free (expected); + } +#endif /*HAVE_WORDEXP_H*/ +} + + + +static void +test_mu_util_guess_maildir_01 (void) +{ + char *got; + const char *expected; + + /* skip the test if there's no /tmp */ + if (access ("/tmp", F_OK)) + return; + + g_setenv ("MAILDIR", "/tmp", TRUE); + + got = mu_util_guess_maildir (); + expected = "/tmp"; + + g_assert_cmpstr (got,==,expected); + g_free (got); +} + + +static void +test_mu_util_guess_maildir_02 (void) +{ + char *got, *mdir; + + g_unsetenv ("MAILDIR"); + + mdir = g_strdup_printf ("%s%cMaildir", + getenv("HOME"), G_DIR_SEPARATOR); + got = mu_util_guess_maildir (); + + if (access (mdir, F_OK) == 0) + g_assert_cmpstr (got, ==, mdir); + else + g_assert_cmpstr (got, == , NULL); + + g_free (got); + g_free (mdir); +} + + +static void +test_mu_util_check_dir_01 (void) +{ + if (g_access ("/usr/bin", F_OK) == 0) { + g_assert_cmpuint ( + mu_util_check_dir ("/usr/bin", TRUE, FALSE) == TRUE, + ==, + g_access ("/usr/bin", R_OK) == 0); + } +} + + +static void +test_mu_util_check_dir_02 (void) +{ + if (g_access ("/tmp", F_OK) == 0) { + g_assert_cmpuint ( + mu_util_check_dir ("/tmp", FALSE, TRUE) == TRUE, + ==, + g_access ("/tmp", W_OK) == 0); + } +} + + +static void +test_mu_util_check_dir_03 (void) +{ + if (g_access (".", F_OK) == 0) { + g_assert_cmpuint ( + mu_util_check_dir (".", TRUE, TRUE) == TRUE, + ==, + g_access (".", W_OK | R_OK) == 0); + } +} + + +static void +test_mu_util_check_dir_04 (void) +{ + /* not a dir, so it must be false */ + g_assert_cmpuint ( + mu_util_check_dir ("test-util.c", TRUE, TRUE), + ==, + FALSE); +} + +static void +test_mu_util_get_dtype_with_lstat (void) +{ + g_assert_cmpuint ( + mu_util_get_dtype_with_lstat (MU_TESTMAILDIR), ==, DT_DIR); + g_assert_cmpuint ( + mu_util_get_dtype_with_lstat (MU_TESTMAILDIR2), ==, DT_DIR); + g_assert_cmpuint ( + mu_util_get_dtype_with_lstat (MU_TESTMAILDIR2 "/Foo/cur/mail5"), + ==, DT_REG); +} + + +static void +test_mu_util_supports (void) +{ + gboolean has_guile; + gchar *path; + + has_guile = FALSE; +#ifdef BUILD_GUILE + has_guile = TRUE; +#endif /*BUILD_GUILE*/ + + g_assert_cmpuint (mu_util_supports (MU_FEATURE_GUILE), == ,has_guile); + g_assert_cmpuint (mu_util_supports (MU_FEATURE_CRYPTO), == ,TRUE); + + path = g_find_program_in_path ("gnuplot"); + g_free (path); + + g_assert_cmpuint (mu_util_supports (MU_FEATURE_GNUPLOT),==, + path ? TRUE : FALSE); + + g_assert_cmpuint ( + mu_util_supports (MU_FEATURE_GNUPLOT|MU_FEATURE_GUILE| + MU_FEATURE_CRYPTO), + ==, + has_guile && path ? TRUE : FALSE); +} + + +static void +test_mu_util_program_in_path (void) +{ + g_assert_cmpuint (mu_util_program_in_path("ls"),==,TRUE); +} + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + /* mu_util_dir_expand */ + g_test_add_func ("/mu-util/mu-util-dir-expand-00", + test_mu_util_dir_expand_00); + g_test_add_func ("/mu-util/mu-util-dir-expand-01", + test_mu_util_dir_expand_01); + + /* mu_util_guess_maildir */ + g_test_add_func ("/mu-util/mu-util-guess-maildir-01", + test_mu_util_guess_maildir_01); + g_test_add_func ("/mu-util/mu-util-guess-maildir-02", + test_mu_util_guess_maildir_02); + + /* mu_util_check_dir */ + g_test_add_func ("/mu-util/mu-util-check-dir-01", + test_mu_util_check_dir_01); + g_test_add_func ("/mu-util/mu-util-check-dir-02", + test_mu_util_check_dir_02); + g_test_add_func ("/mu-util/mu-util-check-dir-03", + test_mu_util_check_dir_03); + g_test_add_func ("/mu-util/mu-util-check-dir-04", + test_mu_util_check_dir_04); + + g_test_add_func ("/mu-util/mu-util-get-dtype-with-lstat", + test_mu_util_get_dtype_with_lstat); + + g_test_add_func ("/mu-util/mu-util-supports", test_mu_util_supports); + g_test_add_func ("/mu-util/mu-util-program-in-path", + test_mu_util_program_in_path); + + return g_test_run (); +} diff --git a/lib/utils/test-sexp-parser.cc b/lib/utils/test-sexp-parser.cc new file mode 100644 index 0000000..0dccf96 --- /dev/null +++ b/lib/utils/test-sexp-parser.cc @@ -0,0 +1,76 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include + +#include "mu-command-parser.hh" +#include "mu-utils.hh" + +using namespace Mu; + +static bool +check_parse (const std::string& expr, const std::string& expected) +{ + try { + const auto parsed{to_string(Sexp::parse(expr))}; + g_assert_cmpstr(parsed.c_str(), ==, expected.c_str()); + return true; + + } catch (const Error& err) { + g_warning ("caught exception parsing '%s': %s", expr.c_str(), err.what()); + return false; + } +} + +static void +test_parser() +{ + check_parse(R"(:foo-123)", "{:foo-123}"); + check_parse(R"("foo")", "{foo}"); + check_parse(R"(12345)", "{12345}"); + check_parse(R"(-12345)", "{-12345}"); + check_parse(R"((123 bar "cuux"))", "({123}{bar}{cuux})"); + + check_parse(R"("\"")", "{\"}"); + check_parse(R"("\\")", "{\\}"); +} + +int +main (int argc, char *argv[]) try +{ + g_test_init (&argc, &argv, NULL); + + if (argc == 2) { + std::cout << Sexp::parse(argv[1]) << '\n'; + return 0; + } + + g_test_add_func ("/utils/command-parser/parse", test_parser); + + return g_test_run (); + + +} catch (const std::runtime_error& re) { + std::cerr << re.what() << "\n"; + return 1; +} diff --git a/lib/utils/test-utils.cc b/lib/utils/test-utils.cc new file mode 100644 index 0000000..b1f666e --- /dev/null +++ b/lib/utils/test-utils.cc @@ -0,0 +1,206 @@ +/* +** Copyright (C) 2017 Dirk-Jan C. Binnema +** +** This library is free software; you can redistribute it and/or +** modify it under the terms of the GNU Lesser General Public License +** as published by the Free Software Foundation; either version 2.1 +** of the License, or (at your option) any later version. +** +** This library is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +** Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public +** License along with this library; if not, write to the Free +** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA +** 02110-1301, USA. +*/ + +#include +#include + +#include +#include +#include + +#include "mu-utils.hh" + +using namespace Mu; + +struct Case { + const std::string expr; + bool is_first{}; + const std::string expected; +}; +using CaseVec = std::vector; +using ProcFunc = std::function; + + +static void +test_cases(const CaseVec& cases, ProcFunc proc) +{ + for (const auto& casus : cases ) { + + const auto res = proc(casus.expr, casus.is_first); + if (g_test_verbose()) { + std::cout << "\n"; + std::cout << casus.expr << ' ' << casus.is_first << std::endl; + std::cout << "exp: '" << casus.expected << "'" << std::endl; + std::cout << "got: '" << res << "'" << std::endl; + } + + g_assert_true (casus.expected == res); + } +} + +static void +test_date_basic () +{ + g_setenv ("TZ", "Europe/Helsinki", TRUE); + + CaseVec cases = { + { "2015-09-18T09:10:23", true, "1442556623" }, + { "1972-12-14T09:10:23", true, "0093165023" }, + { "1854-11-18T17:10:23", true, "0000000000" }, + + { "2000-02-31T09:10:23", true, "0951861599" }, + { "2000-02-29T23:59:59", true, "0951861599" }, + + { "2016", true, "1451599200" }, + { "2016", false, "1483221599" }, + + { "fnorb", true, "0000000000" }, + { "fnorb", false, "9999999999" }, + { "", false, "9999999999" }, + { "", true, "0000000000" } + }; + + test_cases (cases, [](auto s, auto f){ return date_to_time_t_string(s,f); }); +} + +static void +test_date_ymwdhMs (void) +{ + struct { + std::string expr; + long diff; + int tolerance; + } tests[] = { + { "3h", 3 * 60 * 60, 1 }, + { "21d", 21 * 24 * 60 * 60, 3600 + 1 }, + { "2w", 2 * 7 * 24 * 60 * 60, 3600 + 1 }, + + { "2y", 2 * 365 * 24 * 60 * 60, 24 * 3600 + 1 }, + { "3m", 3 * 30 * 24 * 60 * 60, 3 * 24 * 3600 + 1 } + }; + + for (auto i = 0; i != G_N_ELEMENTS(tests); ++i) { + const auto diff = time(NULL) - + strtol(Mu::date_to_time_t_string(tests[i].expr, true).c_str(), + NULL, 10); + if (g_test_verbose()) + std::cerr << tests[i].expr << ' ' + << diff << ' ' + << tests[i].diff << std::endl; + + g_assert_true (tests[i].diff - diff <= tests[i].tolerance); + } + + g_assert_true (strtol(Mu::date_to_time_t_string("-1y", true).c_str(), + NULL, 10) == 0); +} + +static void +test_size () +{ + CaseVec cases = { + { "456", true, "0000000456" }, + { "", false, "9999999999" }, + { "", true, "0000000000" }, + }; + + test_cases (cases, [](auto s, auto f){ return size_to_string(s,f); }); +} + + +static void +test_flatten () +{ + CaseVec cases = { + { "Менделе́ев", true, "менделеев" }, + { "", false, "" }, + { "Ångström", true, "angstrom" }, + }; + + test_cases (cases, [](auto s, auto f){ return utf8_flatten(s); }); +} + +static void +test_clean () +{ + CaseVec cases = { + { "\t a\t\nb ", true, "a b" }, + { "", false, "" }, + { "Ångström", true, "Ångström" }, + }; + + test_cases (cases, [](auto s, auto f){ return utf8_clean(s); }); +} + + +static void +test_format () +{ + g_assert_true (format ("hello %s, %u", "world", 123) == + "hello world, 123"); +} + + +enum struct Bits { None = 0, Bit1 = 1 << 0, Bit2 = 1 << 1 }; +MU_ENABLE_BITOPS(Bits); + +static void +test_define_bitmap() +{ + g_assert_cmpuint((guint)Bits::None,==,(guint)0); + g_assert_cmpuint((guint)Bits::Bit1,==,(guint)1); + g_assert_cmpuint((guint)Bits::Bit2,==,(guint)2); + + g_assert_cmpuint((guint)(Bits::Bit1|Bits::Bit2),==,(guint)3); + g_assert_cmpuint((guint)(Bits::Bit1&Bits::Bit2),==,(guint)0); + + g_assert_cmpuint((guint)(Bits::Bit1&(~Bits::Bit2)),==,(guint)1); + + { + Bits b{Bits::Bit1}; + b|=Bits::Bit2; + g_assert_cmpuint((guint)b,==,(guint)3); + } + + { + Bits b{Bits::Bit1}; + b&=Bits::Bit1; + g_assert_cmpuint((guint)b,==,(guint)1); + } + + +} + + + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/utils/date-basic", test_date_basic); + g_test_add_func ("/utils/date-ymwdhMs", test_date_ymwdhMs); + g_test_add_func ("/utils/size", test_size); + g_test_add_func ("/utils/flatten", test_flatten); + g_test_add_func ("/utils/clean", test_clean); + g_test_add_func ("/utils/format", test_format); + g_test_add_func ("/utils/define-bitmap", test_define_bitmap); + + return g_test_run (); +} diff --git a/m4/Makefile.am b/m4/Makefile.am new file mode 100644 index 0000000..eeb8a05 --- /dev/null +++ b/m4/Makefile.am @@ -0,0 +1,47 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +EXTRA_DIST= \ + ax_ac_append_to_file.m4 \ + ax_ac_print_to_file.m4 \ + ax_add_am_macro_static.m4 \ + ax_am_macros_static.m4 \ + ax_append_compile_flags.m4 \ + ax_append_flag.m4 \ + ax_append_link_flags.m4 \ + ax_check_compile_flag.m4 \ + ax_check_enable_debug.m4 \ + ax_check_gnu_make.m4 \ + ax_check_link_flag.m4 \ + ax_code_coverage.m4 \ + ax_compiler_flags.m4 \ + ax_compiler_flags_cflags.m4 \ + ax_compiler_flags_cxxflags.m4 \ + ax_compiler_flags_gir.m4 \ + ax_compiler_flags_ldflags.m4 \ + ax_cxx_compile_stdcxx.m4 \ + ax_cxx_compile_stdcxx_14.m4 \ + ax_file_escapes.m4 \ + ax_is_release.m4 \ + ax_lib_readline.m4 \ + ax_require_defined.m4 \ + ax_valgrind_check.m4 \ + guile-2.2.m4 \ + lib-ld.m4 \ + lib-link.m4 \ + lib-prefix.m4 diff --git a/m4/ax_ac_append_to_file.m4 b/m4/ax_ac_append_to_file.m4 new file mode 100644 index 0000000..242b3d5 --- /dev/null +++ b/m4/ax_ac_append_to_file.m4 @@ -0,0 +1,32 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_ac_append_to_file.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AC_APPEND_TO_FILE([FILE],[DATA]) +# +# DESCRIPTION +# +# Appends the specified data to the specified Autoconf is run. If you want +# to append to a file when configure is run use AX_APPEND_TO_FILE instead. +# +# LICENSE +# +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +AC_DEFUN([AX_AC_APPEND_TO_FILE],[ +AC_REQUIRE([AX_FILE_ESCAPES]) +m4_esyscmd( +AX_FILE_ESCAPES +[ +printf "%s" "$2" >> "$1" +]) +]) diff --git a/m4/ax_ac_print_to_file.m4 b/m4/ax_ac_print_to_file.m4 new file mode 100644 index 0000000..642dfc1 --- /dev/null +++ b/m4/ax_ac_print_to_file.m4 @@ -0,0 +1,32 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_ac_print_to_file.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AC_PRINT_TO_FILE([FILE],[DATA]) +# +# DESCRIPTION +# +# Writes the specified data to the specified file when Autoconf is run. If +# you want to print to a file when configure is run use AX_PRINT_TO_FILE +# instead. +# +# LICENSE +# +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +AC_DEFUN([AX_AC_PRINT_TO_FILE],[ +m4_esyscmd( +AC_REQUIRE([AX_FILE_ESCAPES]) +[ +printf "%s" "$2" > "$1" +]) +]) diff --git a/m4/ax_add_am_macro_static.m4 b/m4/ax_add_am_macro_static.m4 new file mode 100644 index 0000000..6442d24 --- /dev/null +++ b/m4/ax_add_am_macro_static.m4 @@ -0,0 +1,28 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_add_am_macro_static.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_ADD_AM_MACRO_STATIC([RULE]) +# +# DESCRIPTION +# +# Adds the specified rule to $AMINCLUDE. +# +# LICENSE +# +# Copyright (c) 2009 Tom Howard +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AC_DEFUN([AX_ADD_AM_MACRO_STATIC],[ + AC_REQUIRE([AX_AM_MACROS_STATIC]) + AX_AC_APPEND_TO_FILE(AMINCLUDE_STATIC,[$1]) +]) diff --git a/m4/ax_am_macros_static.m4 b/m4/ax_am_macros_static.m4 new file mode 100644 index 0000000..f4cee8c --- /dev/null +++ b/m4/ax_am_macros_static.m4 @@ -0,0 +1,38 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_am_macros_static.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_AM_MACROS_STATIC +# +# DESCRIPTION +# +# Adds support for macros that create Automake rules. You must manually +# add the following line +# +# include $(top_srcdir)/aminclude_static.am +# +# to your Makefile.am files. +# +# LICENSE +# +# Copyright (c) 2009 Tom Howard +# Copyright (c) 2009 Allan Caffee +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +AC_DEFUN([AMINCLUDE_STATIC],[aminclude_static.am]) + +AC_DEFUN([AX_AM_MACROS_STATIC], +[ +AX_AC_PRINT_TO_FILE(AMINCLUDE_STATIC,[ +# ]AMINCLUDE_STATIC[ generated automatically by Autoconf +# from AX_AM_MACROS_STATIC on ]m4_esyscmd([LC_ALL=C date])[ +]) +]) diff --git a/m4/ax_append_compile_flags.m4 b/m4/ax_append_compile_flags.m4 new file mode 100644 index 0000000..9c85635 --- /dev/null +++ b/m4/ax_append_compile_flags.m4 @@ -0,0 +1,46 @@ +# ============================================================================ +# https://www.gnu.org/software/autoconf-archive/ax_append_compile_flags.html +# ============================================================================ +# +# SYNOPSIS +# +# AX_APPEND_COMPILE_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# For every FLAG1, FLAG2 it is checked whether the compiler works with the +# flag. If it does, the flag is added FLAGS-VARIABLE +# +# If FLAGS-VARIABLE is not specified, the current language's flags (e.g. +# CFLAGS) is used. During the check the flag is always added to the +# current language's flags. +# +# If EXTRA-FLAGS is defined, it is added to the current language's default +# flags (e.g. CFLAGS) when the check is done. The check is thus made with +# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to +# force the compiler to issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_COMPILE_IFELSE. +# +# NOTE: This macro depends on the AX_APPEND_FLAG and +# AX_CHECK_COMPILE_FLAG. Please keep this macro in sync with +# AX_APPEND_LINK_FLAGS. +# +# LICENSE +# +# Copyright (c) 2011 Maarten Bosmans +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 7 + +AC_DEFUN([AX_APPEND_COMPILE_FLAGS], +[AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) +AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) +for flag in $1; do + AX_CHECK_COMPILE_FLAG([$flag], [AX_APPEND_FLAG([$flag], [$2])], [], [$3], [$4]) +done +])dnl AX_APPEND_COMPILE_FLAGS diff --git a/m4/ax_append_flag.m4 b/m4/ax_append_flag.m4 new file mode 100644 index 0000000..dd6d8b6 --- /dev/null +++ b/m4/ax_append_flag.m4 @@ -0,0 +1,50 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_append_flag.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_APPEND_FLAG(FLAG, [FLAGS-VARIABLE]) +# +# DESCRIPTION +# +# FLAG is appended to the FLAGS-VARIABLE shell variable, with a space +# added in between. +# +# If FLAGS-VARIABLE is not specified, the current language's flags (e.g. +# CFLAGS) is used. FLAGS-VARIABLE is not changed if it already contains +# FLAG. If FLAGS-VARIABLE is unset in the shell, it is set to exactly +# FLAG. +# +# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. +# +# LICENSE +# +# Copyright (c) 2008 Guido U. Draheim +# Copyright (c) 2011 Maarten Bosmans +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AC_DEFUN([AX_APPEND_FLAG], +[dnl +AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_SET_IF +AS_VAR_PUSHDEF([FLAGS], [m4_default($2,_AC_LANG_PREFIX[FLAGS])]) +AS_VAR_SET_IF(FLAGS,[ + AS_CASE([" AS_VAR_GET(FLAGS) "], + [*" $1 "*], [AC_RUN_LOG([: FLAGS already contains $1])], + [ + AS_VAR_APPEND(FLAGS,[" $1"]) + AC_RUN_LOG([: FLAGS="$FLAGS"]) + ]) + ], + [ + AS_VAR_SET(FLAGS,[$1]) + AC_RUN_LOG([: FLAGS="$FLAGS"]) + ]) +AS_VAR_POPDEF([FLAGS])dnl +])dnl AX_APPEND_FLAG diff --git a/m4/ax_append_link_flags.m4 b/m4/ax_append_link_flags.m4 new file mode 100644 index 0000000..99b9fa5 --- /dev/null +++ b/m4/ax_append_link_flags.m4 @@ -0,0 +1,44 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_append_link_flags.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_APPEND_LINK_FLAGS([FLAG1 FLAG2 ...], [FLAGS-VARIABLE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# For every FLAG1, FLAG2 it is checked whether the linker works with the +# flag. If it does, the flag is added FLAGS-VARIABLE +# +# If FLAGS-VARIABLE is not specified, the linker's flags (LDFLAGS) is +# used. During the check the flag is always added to the linker's flags. +# +# If EXTRA-FLAGS is defined, it is added to the linker's default flags +# when the check is done. The check is thus made with the flags: "LDFLAGS +# EXTRA-FLAGS FLAG". This can for example be used to force the linker to +# issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_COMPILE_IFELSE. +# +# NOTE: This macro depends on the AX_APPEND_FLAG and AX_CHECK_LINK_FLAG. +# Please keep this macro in sync with AX_APPEND_COMPILE_FLAGS. +# +# LICENSE +# +# Copyright (c) 2011 Maarten Bosmans +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 7 + +AC_DEFUN([AX_APPEND_LINK_FLAGS], +[AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) +AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) +for flag in $1; do + AX_CHECK_LINK_FLAG([$flag], [AX_APPEND_FLAG([$flag], [m4_default([$2], [LDFLAGS])])], [], [$3], [$4]) +done +])dnl AX_APPEND_LINK_FLAGS diff --git a/m4/ax_check_compile_flag.m4 b/m4/ax_check_compile_flag.m4 new file mode 100644 index 0000000..bd753b3 --- /dev/null +++ b/m4/ax_check_compile_flag.m4 @@ -0,0 +1,53 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_compile_flag.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_COMPILE_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# Check whether the given FLAG works with the current language's compiler +# or gives an error. (Warnings, however, are ignored) +# +# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on +# success/failure. +# +# If EXTRA-FLAGS is defined, it is added to the current language's default +# flags (e.g. CFLAGS) when the check is done. The check is thus made with +# the flags: "CFLAGS EXTRA-FLAGS FLAG". This can for example be used to +# force the compiler to issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_COMPILE_IFELSE. +# +# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this +# macro in sync with AX_CHECK_{PREPROC,LINK}_FLAG. +# +# LICENSE +# +# Copyright (c) 2008 Guido U. Draheim +# Copyright (c) 2011 Maarten Bosmans +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 6 + +AC_DEFUN([AX_CHECK_COMPILE_FLAG], +[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF +AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_[]_AC_LANG_ABBREV[]flags_$4_$1])dnl +AC_CACHE_CHECK([whether _AC_LANG compiler accepts $1], CACHEVAR, [ + ax_check_save_flags=$[]_AC_LANG_PREFIX[]FLAGS + _AC_LANG_PREFIX[]FLAGS="$[]_AC_LANG_PREFIX[]FLAGS $4 $1" + AC_COMPILE_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], + [AS_VAR_SET(CACHEVAR,[yes])], + [AS_VAR_SET(CACHEVAR,[no])]) + _AC_LANG_PREFIX[]FLAGS=$ax_check_save_flags]) +AS_VAR_IF(CACHEVAR,yes, + [m4_default([$2], :)], + [m4_default([$3], :)]) +AS_VAR_POPDEF([CACHEVAR])dnl +])dnl AX_CHECK_COMPILE_FLAGS diff --git a/m4/ax_check_enable_debug.m4 b/m4/ax_check_enable_debug.m4 new file mode 100644 index 0000000..7bc7710 --- /dev/null +++ b/m4/ax_check_enable_debug.m4 @@ -0,0 +1,124 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_enable_debug.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_ENABLE_DEBUG([enable by default=yes/info/profile/no], [ENABLE DEBUG VARIABLES ...], [DISABLE DEBUG VARIABLES NDEBUG ...], [IS-RELEASE]) +# +# DESCRIPTION +# +# Check for the presence of an --enable-debug option to configure, with +# the specified default value used when the option is not present. Return +# the value in the variable $ax_enable_debug. +# +# Specifying 'yes' adds '-g -O0' to the compilation flags for all +# languages. Specifying 'info' adds '-g' to the compilation flags. +# Specifying 'profile' adds '-g -pg' to the compilation flags and '-pg' to +# the linking flags. Otherwise, nothing is added. +# +# Define the variables listed in the second argument if debug is enabled, +# defaulting to no variables. Defines the variables listed in the third +# argument if debug is disabled, defaulting to NDEBUG. All lists of +# variables should be space-separated. +# +# If debug is not enabled, ensure AC_PROG_* will not add debugging flags. +# Should be invoked prior to any AC_PROG_* compiler checks. +# +# IS-RELEASE can be used to change the default to 'no' when making a +# release. Set IS-RELEASE to 'yes' or 'no' as appropriate. By default, it +# uses the value of $ax_is_release, so if you are using the AX_IS_RELEASE +# macro, there is no need to pass this parameter. +# +# AX_IS_RELEASE([git-directory]) +# AX_CHECK_ENABLE_DEBUG() +# +# LICENSE +# +# Copyright (c) 2011 Rhys Ulerich +# Copyright (c) 2014, 2015 Philip Withnall +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. + +#serial 9 + +AC_DEFUN([AX_CHECK_ENABLE_DEBUG],[ + AC_BEFORE([$0],[AC_PROG_CC])dnl + AC_BEFORE([$0],[AC_PROG_CXX])dnl + AC_BEFORE([$0],[AC_PROG_F77])dnl + AC_BEFORE([$0],[AC_PROG_FC])dnl + + AC_MSG_CHECKING(whether to enable debugging) + + ax_enable_debug_default=m4_tolower(m4_normalize(ifelse([$1],,[no],[$1]))) + ax_enable_debug_is_release=m4_tolower(m4_normalize(ifelse([$4],, + [$ax_is_release], + [$4]))) + + # If this is a release, override the default. + AS_IF([test "$ax_enable_debug_is_release" = "yes"], + [ax_enable_debug_default="no"]) + + m4_define(ax_enable_debug_vars,[m4_normalize(ifelse([$2],,,[$2]))]) + m4_define(ax_disable_debug_vars,[m4_normalize(ifelse([$3],,[NDEBUG],[$3]))]) + + AC_ARG_ENABLE(debug, + [AS_HELP_STRING([--enable-debug=]@<:@yes/info/profile/no@:>@,[compile with debugging])], + [],enable_debug=$ax_enable_debug_default) + + # empty mean debug yes + AS_IF([test "x$enable_debug" = "x"], + [enable_debug="yes"]) + + # case of debug + AS_CASE([$enable_debug], + [yes],[ + AC_MSG_RESULT(yes) + CFLAGS="${CFLAGS} -g -O0" + CXXFLAGS="${CXXFLAGS} -g -O0" + FFLAGS="${FFLAGS} -g -O0" + FCFLAGS="${FCFLAGS} -g -O0" + OBJCFLAGS="${OBJCFLAGS} -g -O0" + ], + [info],[ + AC_MSG_RESULT(info) + CFLAGS="${CFLAGS} -g" + CXXFLAGS="${CXXFLAGS} -g" + FFLAGS="${FFLAGS} -g" + FCFLAGS="${FCFLAGS} -g" + OBJCFLAGS="${OBJCFLAGS} -g" + ], + [profile],[ + AC_MSG_RESULT(profile) + CFLAGS="${CFLAGS} -g -pg" + CXXFLAGS="${CXXFLAGS} -g -pg" + FFLAGS="${FFLAGS} -g -pg" + FCFLAGS="${FCFLAGS} -g -pg" + OBJCFLAGS="${OBJCFLAGS} -g -pg" + LDFLAGS="${LDFLAGS} -pg" + ], + [ + AC_MSG_RESULT(no) + dnl Ensure AC_PROG_CC/CXX/F77/FC/OBJC will not enable debug flags + dnl by setting any unset environment flag variables + AS_IF([test "x${CFLAGS+set}" != "xset"], + [CFLAGS=""]) + AS_IF([test "x${CXXFLAGS+set}" != "xset"], + [CXXFLAGS=""]) + AS_IF([test "x${FFLAGS+set}" != "xset"], + [FFLAGS=""]) + AS_IF([test "x${FCFLAGS+set}" != "xset"], + [FCFLAGS=""]) + AS_IF([test "x${OBJCFLAGS+set}" != "xset"], + [OBJCFLAGS=""]) + ]) + + dnl Define various variables if debugging is disabled. + dnl assert.h is a NOP if NDEBUG is defined, so define it by default. + AS_IF([test "x$enable_debug" = "xyes"], + [m4_map_args_w(ax_enable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is enabled])])], + [m4_map_args_w(ax_disable_debug_vars, [AC_DEFINE(], [,[1],[Define if debugging is disabled])])]) + ax_enable_debug=$enable_debug +]) diff --git a/m4/ax_check_gnu_make.m4 b/m4/ax_check_gnu_make.m4 new file mode 100644 index 0000000..6811043 --- /dev/null +++ b/m4/ax_check_gnu_make.m4 @@ -0,0 +1,95 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_gnu_make.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_GNU_MAKE([run-if-true],[run-if-false]) +# +# DESCRIPTION +# +# This macro searches for a GNU version of make. If a match is found: +# +# * The makefile variable `ifGNUmake' is set to the empty string, otherwise +# it is set to "#". This is useful for including a special features in a +# Makefile, which cannot be handled by other versions of make. +# * The makefile variable `ifnGNUmake' is set to #, otherwise +# it is set to the empty string. This is useful for including a special +# features in a Makefile, which can be handled +# by other versions of make or to specify else like clause. +# * The variable `_cv_gnu_make_command` is set to the command to invoke +# GNU make if it exists, the empty string otherwise. +# * The variable `ax_cv_gnu_make_command` is set to the command to invoke +# GNU make by copying `_cv_gnu_make_command`, otherwise it is unset. +# * If GNU Make is found, its version is extracted from the output of +# `make --version` as the last field of a record of space-separated +# columns and saved into the variable `ax_check_gnu_make_version`. +# * Additionally if GNU Make is found, run shell code run-if-true +# else run shell code run-if-false. +# +# Here is an example of its use: +# +# Makefile.in might contain: +# +# # A failsafe way of putting a dependency rule into a makefile +# $(DEPEND): +# $(CC) -MM $(srcdir)/*.c > $(DEPEND) +# +# @ifGNUmake@ ifeq ($(DEPEND),$(wildcard $(DEPEND))) +# @ifGNUmake@ include $(DEPEND) +# @ifGNUmake@ else +# fallback code +# @ifGNUmake@ endif +# +# Then configure.in would normally contain: +# +# AX_CHECK_GNU_MAKE() +# AC_OUTPUT(Makefile) +# +# Then perhaps to cause gnu make to override any other make, we could do +# something like this (note that GNU make always looks for GNUmakefile +# first): +# +# if ! test x$_cv_gnu_make_command = x ; then +# mv Makefile GNUmakefile +# echo .DEFAULT: > Makefile ; +# echo \ $_cv_gnu_make_command \$@ >> Makefile; +# fi +# +# Then, if any (well almost any) other make is called, and GNU make also +# exists, then the other make wraps the GNU make. +# +# LICENSE +# +# Copyright (c) 2008 John Darrington +# Copyright (c) 2015 Enrico M. Crisostomo +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 11 + +AC_DEFUN([AX_CHECK_GNU_MAKE],dnl + [AC_PROG_AWK + AC_CACHE_CHECK([for GNU make],[_cv_gnu_make_command],[dnl + _cv_gnu_make_command="" ; +dnl Search all the common names for GNU make + for a in "$MAKE" make gmake gnumake ; do + if test -z "$a" ; then continue ; fi ; + if "$a" --version 2> /dev/null | grep GNU 2>&1 > /dev/null ; then + _cv_gnu_make_command=$a ; + AX_CHECK_GNU_MAKE_HEADLINE=$("$a" --version 2> /dev/null | grep "GNU Make") + ax_check_gnu_make_version=$(echo ${AX_CHECK_GNU_MAKE_HEADLINE} | ${AWK} -F " " '{ print $(NF); }') + break ; + fi + done ;]) +dnl If there was a GNU version, then set @ifGNUmake@ to the empty string, '#' otherwise + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifGNUmake], ["#"])], [AS_VAR_SET([ifGNUmake], [""])]) + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifnGNUmake], [""])], [AS_VAR_SET([ifGNUmake], ["#"])]) + AS_VAR_IF([_cv_gnu_make_command], [""], [AS_UNSET(ax_cv_gnu_make_command)], [AS_VAR_SET([ax_cv_gnu_make_command], [${_cv_gnu_make_command}])]) + AS_VAR_IF([_cv_gnu_make_command], [""],[$2],[$1]) + AC_SUBST([ifGNUmake]) + AC_SUBST([ifnGNUmake]) +]) diff --git a/m4/ax_check_link_flag.m4 b/m4/ax_check_link_flag.m4 new file mode 100644 index 0000000..03a30ce --- /dev/null +++ b/m4/ax_check_link_flag.m4 @@ -0,0 +1,53 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_check_link_flag.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CHECK_LINK_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT]) +# +# DESCRIPTION +# +# Check whether the given FLAG works with the linker or gives an error. +# (Warnings, however, are ignored) +# +# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on +# success/failure. +# +# If EXTRA-FLAGS is defined, it is added to the linker's default flags +# when the check is done. The check is thus made with the flags: "LDFLAGS +# EXTRA-FLAGS FLAG". This can for example be used to force the linker to +# issue an error when a bad flag is given. +# +# INPUT gives an alternative input source to AC_LINK_IFELSE. +# +# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this +# macro in sync with AX_CHECK_{PREPROC,COMPILE}_FLAG. +# +# LICENSE +# +# Copyright (c) 2008 Guido U. Draheim +# Copyright (c) 2011 Maarten Bosmans +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 6 + +AC_DEFUN([AX_CHECK_LINK_FLAG], +[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF +AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_ldflags_$4_$1])dnl +AC_CACHE_CHECK([whether the linker accepts $1], CACHEVAR, [ + ax_check_save_flags=$LDFLAGS + LDFLAGS="$LDFLAGS $4 $1" + AC_LINK_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])], + [AS_VAR_SET(CACHEVAR,[yes])], + [AS_VAR_SET(CACHEVAR,[no])]) + LDFLAGS=$ax_check_save_flags]) +AS_VAR_IF(CACHEVAR,yes, + [m4_default([$2], :)], + [m4_default([$3], :)]) +AS_VAR_POPDEF([CACHEVAR])dnl +])dnl AX_CHECK_LINK_FLAGS diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4 new file mode 100644 index 0000000..6d08319 --- /dev/null +++ b/m4/ax_code_coverage.m4 @@ -0,0 +1,272 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CODE_COVERAGE() +# +# DESCRIPTION +# +# Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS, +# CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included +# in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every +# build target (program or library) which should be built with code +# coverage support. Also add rules using AX_ADD_AM_MACRO_STATIC; and +# $enable_code_coverage which can be used in subsequent configure output. +# CODE_COVERAGE_ENABLED is defined and substituted, and corresponds to the +# value of the --enable-code-coverage option, which defaults to being +# disabled. +# +# Test also for gcov program and create GCOV variable that could be +# substituted. +# +# Note that all optimization flags in CFLAGS must be disabled when code +# coverage is enabled. +# +# Usage example: +# +# configure.ac: +# +# AX_CODE_COVERAGE +# +# Makefile.am: +# +# include $(top_srcdir)/aminclude_static.am +# +# my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ... +# my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ... +# my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ... +# my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ... +# +# clean-local: code-coverage-clean +# distclean-local: code-coverage-dist-clean +# +# This results in a "check-code-coverage" rule being added to any +# Makefile.am which do "include $(top_srcdir)/aminclude_static.am" +# (assuming the module has been configured with --enable-code-coverage). +# Running `make check-code-coverage` in that directory will run the +# module's test suite (`make check`) and build a code coverage report +# detailing the code which was touched, then print the URI for the report. +# +# This code was derived from Makefile.decl in GLib, originally licensed +# under LGPLv2.1+. +# +# LICENSE +# +# Copyright (c) 2012, 2016 Philip Withnall +# Copyright (c) 2012 Xan Lopez +# Copyright (c) 2012 Christian Persch +# Copyright (c) 2012 Paolo Borelli +# Copyright (c) 2012 Dan Winship +# Copyright (c) 2015,2018 Bastien ROUCARIES +# +# This library is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or (at +# your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser +# General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . + +#serial 33 + +m4_define(_AX_CODE_COVERAGE_RULES,[ +AX_ADD_AM_MACRO_STATIC([ +# Code coverage +# +# Optional: +# - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting. +# Multiple directories may be specified, separated by whitespace. +# (Default: \$(top_builddir)) +# - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated +# by lcov for code coverage. (Default: +# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info) +# - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage +# reports to be created. (Default: +# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage) +# - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage, +# set to 0 to disable it and leave empty to stay with the default. +# (Default: empty) +# - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov +# instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) +# - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov +# instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) +# - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov +# - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the +# collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) +# - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov +# instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) +# - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering +# lcov instance. (Default: empty) +# - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov +# instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) +# - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the +# genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE) +# - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml +# instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +# - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore +# +# The generated report will be titled using the \$(PACKAGE_NAME) and +# \$(PACKAGE_VERSION). In order to add the current git hash to the title, +# use the git-version-gen script, available online. +# Optional variables +# run only on top dir +if CODE_COVERAGE_ENABLED + ifeq (\$(abs_builddir), \$(abs_top_builddir)) +CODE_COVERAGE_DIRECTORY ?= \$(top_builddir) +CODE_COVERAGE_OUTPUT_FILE ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info +CODE_COVERAGE_OUTPUT_DIRECTORY ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage + +CODE_COVERAGE_BRANCH_COVERAGE ?= +CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= \$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\ +--rc lcov_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE)) +CODE_COVERAGE_LCOV_SHOPTS ?= \$(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT) +CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool \"\$(GCOV)\" +CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= \$(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH) +CODE_COVERAGE_LCOV_OPTIONS ?= \$(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT) +CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?= +CODE_COVERAGE_LCOV_RMOPTS ?= \$(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) +CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\ +\$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\ +--rc genhtml_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE)) +CODE_COVERAGE_GENHTML_OPTIONS ?= \$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +CODE_COVERAGE_IGNORE_PATTERN ?= + +GITIGNOREFILES = \$(GITIGNOREFILES) \$(CODE_COVERAGE_OUTPUT_FILE) \$(CODE_COVERAGE_OUTPUT_DIRECTORY) +code_coverage_v_lcov_cap = \$(code_coverage_v_lcov_cap_\$(V)) +code_coverage_v_lcov_cap_ = \$(code_coverage_v_lcov_cap_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_lcov_cap_0 = @echo \" LCOV --capture\" \$(CODE_COVERAGE_OUTPUT_FILE); +code_coverage_v_lcov_ign = \$(code_coverage_v_lcov_ign_\$(V)) +code_coverage_v_lcov_ign_ = \$(code_coverage_v_lcov_ign_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_lcov_ign_0 = @echo \" LCOV --remove /tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN); +code_coverage_v_genhtml = \$(code_coverage_v_genhtml_\$(V)) +code_coverage_v_genhtml_ = \$(code_coverage_v_genhtml_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_v_genhtml_0 = @echo \" GEN \" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\"; +code_coverage_quiet = \$(code_coverage_quiet_\$(V)) +code_coverage_quiet_ = \$(code_coverage_quiet_\$(AM_DEFAULT_VERBOSITY)) +code_coverage_quiet_0 = --quiet + +# sanitizes the test-name: replaces with underscores: dashes and dots +code_coverage_sanitize = \$(subst -,_,\$(subst .,_,\$(1))) + +# Use recursive makes in order to ignore errors during check +check-code-coverage: + -\$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) -k check + \$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) code-coverage-capture + +# Capture code coverage data +code-coverage-capture: code-coverage-capture-hook + \$(code_coverage_v_lcov_cap)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --capture --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" --test-name \"\$(call code_coverage_sanitize,\$(PACKAGE_NAME)-\$(PACKAGE_VERSION))\" --no-checksum --compat-libtool \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_OPTIONS) + \$(code_coverage_v_lcov_ign)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --remove \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"/tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN) --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_RMOPTS) + -@rm -f \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" + \$(code_coverage_v_genhtml)LANG=C \$(GENHTML) \$(code_coverage_quiet) \$(addprefix --prefix ,\$(CODE_COVERAGE_DIRECTORY)) --output-directory \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" --title \"\$(PACKAGE_NAME)-\$(PACKAGE_VERSION) Code Coverage\" --legend --show-details \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_GENHTML_OPTIONS) + @echo \"file://\$(abs_builddir)/\$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html\" + +code-coverage-clean: + -\$(LCOV) --directory \$(top_builddir) -z + -rm -rf \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" + -find . \\( -name \"*.gcda\" -o -name \"*.gcno\" -o -name \"*.gcov\" \\) -delete + +code-coverage-dist-clean: + +A][M_DISTCHECK_CONFIGURE_FLAGS := \$(A][M_DISTCHECK_CONFIGURE_FLAGS) --disable-code-coverage + else # ifneq (\$(abs_builddir), \$(abs_top_builddir)) +check-code-coverage: + +code-coverage-capture: code-coverage-capture-hook + +code-coverage-clean: + +code-coverage-dist-clean: + endif # ifeq (\$(abs_builddir), \$(abs_top_builddir)) +else #! CODE_COVERAGE_ENABLED +# Use recursive makes in order to ignore errors during check +check-code-coverage: + @echo \"Need to reconfigure with --enable-code-coverage\" +# Capture code coverage data +code-coverage-capture: code-coverage-capture-hook + @echo \"Need to reconfigure with --enable-code-coverage\" + +code-coverage-clean: + +code-coverage-dist-clean: + +endif #CODE_COVERAGE_ENABLED +# Hook rule executed before code-coverage-capture, overridable by the user +code-coverage-capture-hook: + +.PHONY: check-code-coverage code-coverage-capture code-coverage-dist-clean code-coverage-clean code-coverage-capture-hook +]) +]) + +AC_DEFUN([_AX_CODE_COVERAGE_ENABLED],[ + AX_CHECK_GNU_MAKE([],[AC_MSG_ERROR([not using GNU make that is needed for coverage])]) + AC_REQUIRE([AX_ADD_AM_MACRO_STATIC]) + # check for gcov + AC_CHECK_TOOL([GCOV], + [$_AX_CODE_COVERAGE_GCOV_PROG_WITH], + [:]) + AS_IF([test "X$GCOV" = "X:"], + [AC_MSG_ERROR([gcov is needed to do coverage])]) + AC_SUBST([GCOV]) + + dnl Check if gcc is being used + AS_IF([ test "$GCC" = "no" ], [ + AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage]) + ]) + + AC_CHECK_PROG([LCOV], [lcov], [lcov]) + AC_CHECK_PROG([GENHTML], [genhtml], [genhtml]) + + AS_IF([ test x"$LCOV" = x ], [ + AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed]) + ]) + + AS_IF([ test x"$GENHTML" = x ], [ + AC_MSG_ERROR([Could not find genhtml from the lcov package]) + ]) + + dnl Build the code coverage flags + dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility + CODE_COVERAGE_CPPFLAGS="-DNDEBUG" + CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" + CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage" + CODE_COVERAGE_LIBS="-lgcov" + + AC_SUBST([CODE_COVERAGE_CPPFLAGS]) + AC_SUBST([CODE_COVERAGE_CFLAGS]) + AC_SUBST([CODE_COVERAGE_CXXFLAGS]) + AC_SUBST([CODE_COVERAGE_LIBS]) +]) + +AC_DEFUN([AX_CODE_COVERAGE],[ + dnl Check for --enable-code-coverage + + # allow to override gcov location + AC_ARG_WITH([gcov], + [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])], + [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov], + [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov]) + + AC_MSG_CHECKING([whether to build with code coverage support]) + AC_ARG_ENABLE([code-coverage], + AS_HELP_STRING([--enable-code-coverage], + [Whether to enable code coverage support]),, + enable_code_coverage=no) + + AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test "x$enable_code_coverage" = xyes]) + AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage]) + AC_MSG_RESULT($enable_code_coverage) + + AS_IF([ test "x$enable_code_coverage" = xyes ], [ + _AX_CODE_COVERAGE_ENABLED + ]) + + _AX_CODE_COVERAGE_RULES +]) diff --git a/m4/ax_compiler_flags.m4 b/m4/ax_compiler_flags.m4 new file mode 100644 index 0000000..ddb0456 --- /dev/null +++ b/m4/ax_compiler_flags.m4 @@ -0,0 +1,158 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS([CFLAGS-VARIABLE], [LDFLAGS-VARIABLE], [IS-RELEASE], [EXTRA-BASE-CFLAGS], [EXTRA-YES-CFLAGS], [UNUSED], [UNUSED], [UNUSED], [EXTRA-BASE-LDFLAGS], [EXTRA-YES-LDFLAGS], [UNUSED], [UNUSED], [UNUSED]) +# +# DESCRIPTION +# +# Check for the presence of an --enable-compile-warnings option to +# configure, defaulting to "error" in normal operation, or "yes" if +# IS-RELEASE is equal to "yes". Return the value in the variable +# $ax_enable_compile_warnings. +# +# Depending on the value of --enable-compile-warnings, different compiler +# warnings are checked to see if they work with the current compiler and, +# if so, are appended to CFLAGS-VARIABLE and LDFLAGS-VARIABLE. This +# allows a consistent set of baseline compiler warnings to be used across +# a code base, irrespective of any warnings enabled locally by individual +# developers. By standardising the warnings used by all developers of a +# project, the project can commit to a zero-warnings policy, using -Werror +# to prevent compilation if new warnings are introduced. This makes +# catching bugs which are flagged by warnings a lot easier. +# +# By providing a consistent --enable-compile-warnings argument across all +# projects using this macro, continuous integration systems can easily be +# configured the same for all projects. Automated systems or build +# systems aimed at beginners may want to pass the --disable-Werror +# argument to unconditionally prevent warnings being fatal. +# +# --enable-compile-warnings can take the values: +# +# * no: Base compiler warnings only; not even -Wall. +# * yes: The above, plus a broad range of useful warnings. +# * error: The above, plus -Werror so that all warnings are fatal. +# Use --disable-Werror to override this and disable fatal +# warnings. +# +# The set of base and enabled flags can be augmented using the +# EXTRA-*-CFLAGS and EXTRA-*-LDFLAGS variables, which are tested and +# appended to the output variable if --enable-compile-warnings is not +# "no". Flags should not be disabled using these arguments, as the entire +# point of AX_COMPILER_FLAGS is to enforce a consistent set of useful +# compiler warnings on code, using warnings which have been chosen for low +# false positive rates. If a compiler emits false positives for a +# warning, a #pragma should be used in the code to disable the warning +# locally. See: +# +# https://gcc.gnu.org/onlinedocs/gcc-4.9.2/gcc/Diagnostic-Pragmas.html#Diagnostic-Pragmas +# +# The EXTRA-* variables should only be used to supply extra warning flags, +# and not general purpose compiler flags, as they are controlled by +# configure options such as --disable-Werror. +# +# IS-RELEASE can be used to disable -Werror when making a release, which +# is useful for those hairy moments when you just want to get the release +# done as quickly as possible. Set it to "yes" to disable -Werror. By +# default, it uses the value of $ax_is_release, so if you are using the +# AX_IS_RELEASE macro, there is no need to pass this parameter. For +# example: +# +# AX_IS_RELEASE([git-directory]) +# AX_COMPILER_FLAGS() +# +# CFLAGS-VARIABLE defaults to WARN_CFLAGS, and LDFLAGS-VARIABLE defaults +# to WARN_LDFLAGS. Both variables are AC_SUBST-ed by this macro, but must +# be manually added to the CFLAGS and LDFLAGS variables for each target in +# the code base. +# +# If C++ language support is enabled with AC_PROG_CXX, which must occur +# before this macro in configure.ac, warning flags for the C++ compiler +# are AC_SUBST-ed as WARN_CXXFLAGS, and must be manually added to the +# CXXFLAGS variables for each target in the code base. EXTRA-*-CFLAGS can +# be used to augment the base and enabled flags. +# +# Warning flags for g-ir-scanner (from GObject Introspection) are +# AC_SUBST-ed as WARN_SCANNERFLAGS. This variable must be manually added +# to the SCANNERFLAGS variable for each GIR target in the code base. If +# extra g-ir-scanner flags need to be enabled, the AX_COMPILER_FLAGS_GIR +# macro must be invoked manually. +# +# AX_COMPILER_FLAGS may add support for other tools in future, in addition +# to the compiler and linker. No extra EXTRA-* variables will be added +# for those tools, and all extra support will still use the single +# --enable-compile-warnings configure option. For finer grained control +# over the flags for individual tools, use AX_COMPILER_FLAGS_CFLAGS, +# AX_COMPILER_FLAGS_LDFLAGS and AX_COMPILER_FLAGS_* for new tools. +# +# The UNUSED variables date from a previous version of this macro, and are +# automatically appended to the preceding non-UNUSED variable. They should +# be left empty in new uses of the macro. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2015 David King +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 14 + +# _AX_COMPILER_FLAGS_LANG([LANGNAME]) +m4_defun([_AX_COMPILER_FLAGS_LANG], +[m4_ifdef([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [], + [m4_define([_AX_COMPILER_FLAGS_LANG_]$1[_enabled], [])dnl + AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_]$1[FLAGS])])dnl +]) + +AC_DEFUN([AX_COMPILER_FLAGS],[ + # C support is enabled by default. + _AX_COMPILER_FLAGS_LANG([C]) + # Only enable C++ support if AC_PROG_CXX is called. The redefinition of + # AC_PROG_CXX is so that a fatal error is emitted if this macro is called + # before AC_PROG_CXX, which would otherwise cause no C++ warnings to be + # checked. + AC_PROVIDE_IFELSE([AC_PROG_CXX], + [_AX_COMPILER_FLAGS_LANG([CXX])], + [m4_define([AC_PROG_CXX], defn([AC_PROG_CXX])[_AX_COMPILER_FLAGS_LANG([CXX])])]) + AX_REQUIRE_DEFINED([AX_COMPILER_FLAGS_LDFLAGS]) + + # Default value for IS-RELEASE is $ax_is_release + ax_compiler_flags_is_release=m4_tolower(m4_normalize(ifelse([$3],, + [$ax_is_release], + [$3]))) + + AC_ARG_ENABLE([compile-warnings], + AS_HELP_STRING([--enable-compile-warnings=@<:@no/yes/error@:>@], + [Enable compiler warnings and errors]),, + [AS_IF([test "$ax_compiler_flags_is_release" = "yes"], + [enable_compile_warnings="yes"], + [enable_compile_warnings="error"])]) + AC_ARG_ENABLE([Werror], + AS_HELP_STRING([--disable-Werror], + [Unconditionally make all compiler warnings non-fatal]),, + [enable_Werror=maybe]) + + # Return the user's chosen warning level + AS_IF([test "$enable_Werror" = "no" -a \ + "$enable_compile_warnings" = "error"],[ + enable_compile_warnings="yes" + ]) + + ax_enable_compile_warnings=$enable_compile_warnings + + AX_COMPILER_FLAGS_CFLAGS([$1],[$ax_compiler_flags_is_release], + [$4],[$5 $6 $7 $8]) + m4_ifdef([_AX_COMPILER_FLAGS_LANG_CXX_enabled], + [AX_COMPILER_FLAGS_CXXFLAGS([WARN_CXXFLAGS], + [$ax_compiler_flags_is_release], + [$4],[$5 $6 $7 $8])]) + AX_COMPILER_FLAGS_LDFLAGS([$2],[$ax_compiler_flags_is_release], + [$9],[$10 $11 $12 $13]) + AX_COMPILER_FLAGS_GIR([WARN_SCANNERFLAGS],[$ax_compiler_flags_is_release]) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_cflags.m4 b/m4/ax_compiler_flags_cflags.m4 new file mode 100644 index 0000000..916f918 --- /dev/null +++ b/m4/ax_compiler_flags_cflags.m4 @@ -0,0 +1,161 @@ +# ============================================================================= +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cflags.html +# ============================================================================= +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_CFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the C compiler to VARIABLE, which defaults to +# WARN_CFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the CFLAGS variable for each target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2017, 2018 Reini Urban +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 17 + +AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[ + AC_REQUIRE([AC_PROG_SED]) + AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + + # Variable names + m4_define([ax_warn_cflags_variable], + [m4_normalize(ifelse([$1],,[WARN_CFLAGS],[$1]))]) + + AC_LANG_PUSH([C]) + + AC_COMPILE_IFELSE([AC_LANG_PROGRAM([ + [#ifndef __cplusplus + #error "no C++" + #endif]])], + [ax_compiler_cxx=yes;], + [ax_compiler_cxx=no;]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_cflags variable, and + # Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + # Check that -Wno-suggest-attribute=format is supported + AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[ + ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format" + ],[ + ax_compiler_no_suggest_attribute_flags="" + ]) + + # Base flags + AX_APPEND_COMPILE_FLAGS([ dnl + -fno-strict-aliasing dnl + $3 dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + if test "$ax_compiler_cxx" = "no" ; then + # C-only flags. Warn in C++ + AX_APPEND_COMPILE_FLAGS([ dnl + -Wnested-externs dnl + -Wmissing-prototypes dnl + -Wstrict-prototypes dnl + -Wdeclaration-after-statement dnl + -Wimplicit-function-declaration dnl + -Wold-style-definition dnl + -Wjump-misses-init dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + fi + + # "yes" flags + AX_APPEND_COMPILE_FLAGS([ dnl + -Wall dnl + -Wextra dnl + -Wundef dnl + -Wwrite-strings dnl + -Wpointer-arith dnl + -Wmissing-declarations dnl + -Wredundant-decls dnl + -Wno-unused-parameter dnl + -Wno-missing-field-initializers dnl + -Wformat=2 dnl + -Wcast-align dnl + -Wformat-nonliteral dnl + -Wformat-security dnl + -Wsign-compare dnl + -Wstrict-aliasing dnl + -Wshadow dnl + -Winline dnl + -Wpacked dnl + -Wmissing-format-attribute dnl + -Wmissing-noreturn dnl + -Winit-self dnl + -Wredundant-decls dnl + -Wmissing-include-dirs dnl + -Wunused-but-set-variable dnl + -Warray-bounds dnl + -Wreturn-type dnl + -Wswitch-enum dnl + -Wswitch-default dnl + -Wduplicated-cond dnl + -Wduplicated-branches dnl + -Wlogical-op dnl + -Wrestrict dnl + -Wnull-dereference dnl + -Wdouble-promotion dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_FLAG([-Werror],ax_warn_cflags_variable) + + AX_APPEND_COMPILE_FLAGS([ dnl + [$ax_compiler_no_suggest_attribute_flags] dnl + ],ax_warn_cflags_variable,[$ax_compiler_flags_test]) + ]) + + # In the flags below, when disabling specific flags, always add *both* + # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example) + # we enable -Werror, disable a flag, and a build bot passes CFLAGS=-Wall, + # which effectively turns that flag back on again as an error. + for flag in $ax_warn_cflags_variable; do + AS_CASE([$flag], + [-Wno-*=*],[], + [-Wno-*],[ + AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')], + ax_warn_cflags_variable, + [$ax_compiler_flags_test]) + ]) + done + + AC_LANG_POP([C]) + + # Substitute the variables + AC_SUBST(ax_warn_cflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_cxxflags.m4 b/m4/ax_compiler_flags_cxxflags.m4 new file mode 100644 index 0000000..3067d9b --- /dev/null +++ b/m4/ax_compiler_flags_cxxflags.m4 @@ -0,0 +1,136 @@ +# =============================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_cxxflags.html +# =============================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_CXXFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the C++ compiler to VARIABLE, which defaults to +# WARN_CXXFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the CXXFLAGS variable for each target in the code +# base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2015 David King +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +AC_DEFUN([AX_COMPILER_FLAGS_CXXFLAGS],[ + AC_REQUIRE([AC_PROG_SED]) + AX_REQUIRE_DEFINED([AX_APPEND_COMPILE_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + + # Variable names + m4_define([ax_warn_cxxflags_variable], + [m4_normalize(ifelse([$1],,[WARN_CXXFLAGS],[$1]))]) + + AC_LANG_PUSH([C++]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_cxxflags variable, + # and Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + # Check that -Wno-suggest-attribute=format is supported + AX_CHECK_COMPILE_FLAG([-Wno-suggest-attribute=format],[ + ax_compiler_no_suggest_attribute_flags="-Wno-suggest-attribute=format" + ],[ + ax_compiler_no_suggest_attribute_flags="" + ]) + + # Base flags + AX_APPEND_COMPILE_FLAGS([ dnl + -fno-strict-aliasing dnl + $3 dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_COMPILE_FLAGS([ dnl + -Wall dnl + -Wextra dnl + -Wundef dnl + -Wwrite-strings dnl + -Wpointer-arith dnl + -Wmissing-declarations dnl + -Wredundant-decls dnl + -Wno-unused-parameter dnl + -Wno-missing-field-initializers dnl + -Wformat=2 dnl + -Wcast-align dnl + -Wformat-nonliteral dnl + -Wformat-security dnl + -Wsign-compare dnl + -Wstrict-aliasing dnl + -Wshadow dnl + -Winline dnl + -Wpacked dnl + -Wmissing-format-attribute dnl + -Wmissing-noreturn dnl + -Winit-self dnl + -Wredundant-decls dnl + -Wmissing-include-dirs dnl + -Wunused-but-set-variable dnl + -Warray-bounds dnl + -Wreturn-type dnl + -Wno-overloaded-virtual dnl + -Wswitch-enum dnl + -Wswitch-default dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_FLAG([-Werror],ax_warn_cxxflags_variable) + + AX_APPEND_COMPILE_FLAGS([ dnl + [$ax_compiler_no_suggest_attribute_flags] dnl + ],ax_warn_cxxflags_variable,[$ax_compiler_flags_test]) + ]) + + # In the flags below, when disabling specific flags, always add *both* + # -Wno-foo and -Wno-error=foo. This fixes the situation where (for example) + # we enable -Werror, disable a flag, and a build bot passes CXXFLAGS=-Wall, + # which effectively turns that flag back on again as an error. + for flag in $ax_warn_cxxflags_variable; do + AS_CASE([$flag], + [-Wno-*=*],[], + [-Wno-*],[ + AX_APPEND_COMPILE_FLAGS([-Wno-error=$(AS_ECHO([$flag]) | $SED 's/^-Wno-//')], + ax_warn_cxxflags_variable, + [$ax_compiler_flags_test]) + ]) + done + + AC_LANG_POP([C++]) + + # Substitute the variables + AC_SUBST(ax_warn_cxxflags_variable) +])dnl AX_COMPILER_FLAGS_CXXFLAGS diff --git a/m4/ax_compiler_flags_gir.m4 b/m4/ax_compiler_flags_gir.m4 new file mode 100644 index 0000000..5b4924a --- /dev/null +++ b/m4/ax_compiler_flags_gir.m4 @@ -0,0 +1,60 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_gir.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_GIR([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the g-ir-scanner (from GObject Introspection) to +# VARIABLE, which defaults to WARN_SCANNERFLAGS. VARIABLE is AC_SUBST-ed +# by this macro, but must be manually added to the SCANNERFLAGS variable +# for each GIR target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2015 Philip Withnall +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 6 + +AC_DEFUN([AX_COMPILER_FLAGS_GIR],[ + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + + # Variable names + m4_define([ax_warn_scannerflags_variable], + [m4_normalize(ifelse([$1],,[WARN_SCANNERFLAGS],[$1]))]) + + # Base flags + AX_APPEND_FLAG([$3],ax_warn_scannerflags_variable) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_FLAG([ dnl + --warn-all dnl + $4 dnl + $5 dnl + $6 dnl + $7 dnl + ],ax_warn_scannerflags_variable) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags + AX_APPEND_FLAG([ dnl + --warn-error dnl + ],ax_warn_scannerflags_variable) + ]) + + # Substitute the variables + AC_SUBST(ax_warn_scannerflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_compiler_flags_ldflags.m4 b/m4/ax_compiler_flags_ldflags.m4 new file mode 100644 index 0000000..976d119 --- /dev/null +++ b/m4/ax_compiler_flags_ldflags.m4 @@ -0,0 +1,111 @@ +# ============================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_compiler_flags_ldflags.html +# ============================================================================== +# +# SYNOPSIS +# +# AX_COMPILER_FLAGS_LDFLAGS([VARIABLE], [IS-RELEASE], [EXTRA-BASE-FLAGS], [EXTRA-YES-FLAGS]) +# +# DESCRIPTION +# +# Add warning flags for the linker to VARIABLE, which defaults to +# WARN_LDFLAGS. VARIABLE is AC_SUBST-ed by this macro, but must be +# manually added to the LDFLAGS variable for each target in the code base. +# +# This macro depends on the environment set up by AX_COMPILER_FLAGS. +# Specifically, it uses the value of $ax_enable_compile_warnings to decide +# which flags to enable. +# +# LICENSE +# +# Copyright (c) 2014, 2015 Philip Withnall +# Copyright (c) 2017, 2018 Reini Urban +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 9 + +AC_DEFUN([AX_COMPILER_FLAGS_LDFLAGS],[ + AX_REQUIRE_DEFINED([AX_APPEND_LINK_FLAGS]) + AX_REQUIRE_DEFINED([AX_APPEND_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_COMPILE_FLAG]) + AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) + + # Variable names + m4_define([ax_warn_ldflags_variable], + [m4_normalize(ifelse([$1],,[WARN_LDFLAGS],[$1]))]) + + # Always pass -Werror=unknown-warning-option to get Clang to fail on bad + # flags, otherwise they are always appended to the warn_ldflags variable, + # and Clang warns on them for every compilation unit. + # If this is passed to GCC, it will explode, so the flag must be enabled + # conditionally. + AX_CHECK_COMPILE_FLAG([-Werror=unknown-warning-option],[ + ax_compiler_flags_test="-Werror=unknown-warning-option" + ],[ + ax_compiler_flags_test="" + ]) + + AX_CHECK_LINK_FLAG([-Wl,--as-needed], [ + AX_APPEND_LINK_FLAGS([-Wl,--as-needed], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,relro], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,relro], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,now], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,now], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + AX_CHECK_LINK_FLAG([-Wl,-z,noexecstack], [ + AX_APPEND_LINK_FLAGS([-Wl,-z,noexecstack], + [AM_LDFLAGS],[$ax_compiler_flags_test]) + ]) + # textonly, retpolineplt not yet + + # macOS and cygwin linker do not have --as-needed + AX_CHECK_LINK_FLAG([-Wl,--no-as-needed], [ + ax_compiler_flags_as_needed_option="-Wl,--no-as-needed" + ], [ + ax_compiler_flags_as_needed_option="" + ]) + + # macOS linker speaks with a different accent + ax_compiler_flags_fatal_warnings_option="" + AX_CHECK_LINK_FLAG([-Wl,--fatal-warnings], [ + ax_compiler_flags_fatal_warnings_option="-Wl,--fatal-warnings" + ]) + AX_CHECK_LINK_FLAG([-Wl,-fatal_warnings], [ + ax_compiler_flags_fatal_warnings_option="-Wl,-fatal_warnings" + ]) + + # Base flags + AX_APPEND_LINK_FLAGS([ dnl + $ax_compiler_flags_as_needed_option dnl + $3 dnl + ],ax_warn_ldflags_variable,[$ax_compiler_flags_test]) + + AS_IF([test "$ax_enable_compile_warnings" != "no"],[ + # "yes" flags + AX_APPEND_LINK_FLAGS([$4 $5 $6 $7], + ax_warn_ldflags_variable, + [$ax_compiler_flags_test]) + ]) + AS_IF([test "$ax_enable_compile_warnings" = "error"],[ + # "error" flags; -Werror has to be appended unconditionally because + # it's not possible to test for + # + # suggest-attribute=format is disabled because it gives too many false + # positives + AX_APPEND_LINK_FLAGS([ dnl + $ax_compiler_flags_fatal_warnings_option dnl + ],ax_warn_ldflags_variable,[$ax_compiler_flags_test]) + ]) + + # Substitute the variables + AC_SUBST(ax_warn_ldflags_variable) +])dnl AX_COMPILER_FLAGS diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4 new file mode 100644 index 0000000..9e9eaed --- /dev/null +++ b/m4/ax_cxx_compile_stdcxx.m4 @@ -0,0 +1,948 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional]) +# +# DESCRIPTION +# +# Check for baseline language coverage in the compiler for the specified +# version of the C++ standard. If necessary, add switches to CXX and +# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard) +# or '14' (for the C++14 standard). +# +# The second argument, if specified, indicates whether you insist on an +# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g. +# -std=c++11). If neither is specified, you get whatever works, with +# preference for an extended mode. +# +# The third argument, if specified 'mandatory' or if left unspecified, +# indicates that baseline support for the specified C++ standard is +# required and that the macro should error out if no mode with that +# support is found. If specified 'optional', then configuration proceeds +# regardless, after defining HAVE_CXX${VERSION} if and only if a +# supporting mode is found. +# +# LICENSE +# +# Copyright (c) 2008 Benjamin Kosnik +# Copyright (c) 2012 Zack Weinberg +# Copyright (c) 2013 Roy Stogner +# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov +# Copyright (c) 2015 Paul Norman +# Copyright (c) 2015 Moritz Klammler +# Copyright (c) 2016, 2018 Krzesimir Nowak +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 10 + +dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro +dnl (serial version number 13). + +AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl + m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"], + [$1], [14], [ax_cxx_compile_alternatives="14 1y"], + [$1], [17], [ax_cxx_compile_alternatives="17 1z"], + [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$2], [], [], + [$2], [ext], [], + [$2], [noext], [], + [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl + m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true], + [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true], + [$3], [optional], [ax_cxx_compile_cxx$1_required=false], + [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])]) + AC_LANG_PUSH([C++])dnl + ac_success=no + + m4_if([$2], [noext], [], [dnl + if test x$ac_success = xno; then + for alternative in ${ax_cxx_compile_alternatives}; do + switch="-std=gnu++${alternative}" + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + fi]) + + m4_if([$2], [ext], [], [dnl + if test x$ac_success = xno; then + dnl HP's aCC needs +std=c++11 according to: + dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf + dnl Cray's crayCC needs "-h std=c++11" + for alternative in ${ax_cxx_compile_alternatives}; do + for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do + cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch]) + AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch, + $cachevar, + [ac_save_CXX="$CXX" + CXX="$CXX $switch" + AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])], + [eval $cachevar=yes], + [eval $cachevar=no]) + CXX="$ac_save_CXX"]) + if eval test x\$$cachevar = xyes; then + CXX="$CXX $switch" + if test -n "$CXXCPP" ; then + CXXCPP="$CXXCPP $switch" + fi + ac_success=yes + break + fi + done + if test x$ac_success = xyes; then + break + fi + done + fi]) + AC_LANG_POP([C++]) + if test x$ax_cxx_compile_cxx$1_required = xtrue; then + if test x$ac_success = xno; then + AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.]) + fi + fi + if test x$ac_success = xno; then + HAVE_CXX$1=0 + AC_MSG_NOTICE([No compiler with C++$1 support was found]) + else + HAVE_CXX$1=1 + AC_DEFINE(HAVE_CXX$1,1, + [define if the compiler supports basic C++$1 syntax]) + fi + AC_SUBST(HAVE_CXX$1) +]) + + +dnl Test body for checking C++11 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 +) + + +dnl Test body for checking C++14 support + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 +) + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17], + _AX_CXX_COMPILE_STDCXX_testbody_new_in_11 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_14 + _AX_CXX_COMPILE_STDCXX_testbody_new_in_17 +) + +dnl Tests for new features in C++11 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[ + +// If the compiler admits that it is not ready for C++11, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201103L + +#error "This is not a C++11 compiler" + +#else + +namespace cxx11 +{ + + namespace test_static_assert + { + + template + struct check + { + static_assert(sizeof(int) <= sizeof(T), "not big enough"); + }; + + } + + namespace test_final_override + { + + struct Base + { + virtual void f() {} + }; + + struct Derived : public Base + { + virtual void f() override {} + }; + + } + + namespace test_double_right_angle_brackets + { + + template < typename T > + struct check {}; + + typedef check single_type; + typedef check> double_type; + typedef check>> triple_type; + typedef check>>> quadruple_type; + + } + + namespace test_decltype + { + + int + f() + { + int a = 1; + decltype(a) b = 2; + return a + b; + } + + } + + namespace test_type_deduction + { + + template < typename T1, typename T2 > + struct is_same + { + static const bool value = false; + }; + + template < typename T > + struct is_same + { + static const bool value = true; + }; + + template < typename T1, typename T2 > + auto + add(T1 a1, T2 a2) -> decltype(a1 + a2) + { + return a1 + a2; + } + + int + test(const int c, volatile int v) + { + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == false, ""); + auto ac = c; + auto av = v; + auto sumi = ac + av + 'x'; + auto sumf = ac + av + 1.0; + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == true, ""); + static_assert(is_same::value == false, ""); + static_assert(is_same::value == true, ""); + return (sumf > 0.0) ? sumi : add(c, v); + } + + } + + namespace test_noexcept + { + + int f() { return 0; } + int g() noexcept { return 0; } + + static_assert(noexcept(f()) == false, ""); + static_assert(noexcept(g()) == true, ""); + + } + + namespace test_constexpr + { + + template < typename CharT > + unsigned long constexpr + strlen_c_r(const CharT *const s, const unsigned long acc) noexcept + { + return *s ? strlen_c_r(s + 1, acc + 1) : acc; + } + + template < typename CharT > + unsigned long constexpr + strlen_c(const CharT *const s) noexcept + { + return strlen_c_r(s, 0UL); + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("1") == 1UL, ""); + static_assert(strlen_c("example") == 7UL, ""); + static_assert(strlen_c("another\0example") == 7UL, ""); + + } + + namespace test_rvalue_references + { + + template < int N > + struct answer + { + static constexpr int value = N; + }; + + answer<1> f(int&) { return answer<1>(); } + answer<2> f(const int&) { return answer<2>(); } + answer<3> f(int&&) { return answer<3>(); } + + void + test() + { + int i = 0; + const int c = 0; + static_assert(decltype(f(i))::value == 1, ""); + static_assert(decltype(f(c))::value == 2, ""); + static_assert(decltype(f(0))::value == 3, ""); + } + + } + + namespace test_uniform_initialization + { + + struct test + { + static const int zero {}; + static const int one {1}; + }; + + static_assert(test::zero == 0, ""); + static_assert(test::one == 1, ""); + + } + + namespace test_lambdas + { + + void + test1() + { + auto lambda1 = [](){}; + auto lambda2 = lambda1; + lambda1(); + lambda2(); + } + + int + test2() + { + auto a = [](int i, int j){ return i + j; }(1, 2); + auto b = []() -> int { return '0'; }(); + auto c = [=](){ return a + b; }(); + auto d = [&](){ return c; }(); + auto e = [a, &b](int x) mutable { + const auto identity = [](int y){ return y; }; + for (auto i = 0; i < a; ++i) + a += b--; + return x + identity(a + b); + }(0); + return a + b + c + d + e; + } + + int + test3() + { + const auto nullary = [](){ return 0; }; + const auto unary = [](int x){ return x; }; + using nullary_t = decltype(nullary); + using unary_t = decltype(unary); + const auto higher1st = [](nullary_t f){ return f(); }; + const auto higher2nd = [unary](nullary_t f1){ + return [unary, f1](unary_t f2){ return f2(unary(f1())); }; + }; + return higher1st(nullary) + higher2nd(nullary)(unary); + } + + } + + namespace test_variadic_templates + { + + template + struct sum; + + template + struct sum + { + static constexpr auto value = N0 + sum::value; + }; + + template <> + struct sum<> + { + static constexpr auto value = 0; + }; + + static_assert(sum<>::value == 0, ""); + static_assert(sum<1>::value == 1, ""); + static_assert(sum<23>::value == 23, ""); + static_assert(sum<1, 2>::value == 3, ""); + static_assert(sum<5, 5, 11>::value == 21, ""); + static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, ""); + + } + + // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae + // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function + // because of this. + namespace test_template_alias_sfinae + { + + struct foo {}; + + template + using member = typename T::member_type; + + template + void func(...) {} + + template + void func(member*) {} + + void test(); + + void test() { func(0); } + + } + +} // namespace cxx11 + +#endif // __cplusplus >= 201103L + +]]) + + +dnl Tests for new features in C++14 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[ + +// If the compiler admits that it is not ready for C++14, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201402L + +#error "This is not a C++14 compiler" + +#else + +namespace cxx14 +{ + + namespace test_polymorphic_lambdas + { + + int + test() + { + const auto lambda = [](auto&&... args){ + const auto istiny = [](auto x){ + return (sizeof(x) == 1UL) ? 1 : 0; + }; + const int aretiny[] = { istiny(args)... }; + return aretiny[0]; + }; + return lambda(1, 1L, 1.0f, '1'); + } + + } + + namespace test_binary_literals + { + + constexpr auto ivii = 0b0000000000101010; + static_assert(ivii == 42, "wrong value"); + + } + + namespace test_generalized_constexpr + { + + template < typename CharT > + constexpr unsigned long + strlen_c(const CharT *const s) noexcept + { + auto length = 0UL; + for (auto p = s; *p; ++p) + ++length; + return length; + } + + static_assert(strlen_c("") == 0UL, ""); + static_assert(strlen_c("x") == 1UL, ""); + static_assert(strlen_c("test") == 4UL, ""); + static_assert(strlen_c("another\0test") == 7UL, ""); + + } + + namespace test_lambda_init_capture + { + + int + test() + { + auto x = 0; + const auto lambda1 = [a = x](int b){ return a + b; }; + const auto lambda2 = [a = lambda1(x)](){ return a; }; + return lambda2(); + } + + } + + namespace test_digit_separators + { + + constexpr auto ten_million = 100'000'000; + static_assert(ten_million == 100000000, ""); + + } + + namespace test_return_type_deduction + { + + auto f(int& x) { return x; } + decltype(auto) g(int& x) { return x; } + + template < typename T1, typename T2 > + struct is_same + { + static constexpr auto value = false; + }; + + template < typename T > + struct is_same + { + static constexpr auto value = true; + }; + + int + test() + { + auto x = 0; + static_assert(is_same::value, ""); + static_assert(is_same::value, ""); + return x; + } + + } + +} // namespace cxx14 + +#endif // __cplusplus >= 201402L + +]]) + + +dnl Tests for new features in C++17 + +m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[ + +// If the compiler admits that it is not ready for C++17, why torture it? +// Hopefully, this will speed up the test. + +#ifndef __cplusplus + +#error "This is not a C++ compiler" + +#elif __cplusplus < 201703L + +#error "This is not a C++17 compiler" + +#else + +#include +#include +#include + +namespace cxx17 +{ + + namespace test_constexpr_lambdas + { + + constexpr int foo = [](){return 42;}(); + + } + + namespace test::nested_namespace::definitions + { + + } + + namespace test_fold_expression + { + + template + int multiply(Args... args) + { + return (args * ... * 1); + } + + template + bool all(Args... args) + { + return (args && ...); + } + + } + + namespace test_extended_static_assert + { + + static_assert (true); + + } + + namespace test_auto_brace_init_list + { + + auto foo = {5}; + auto bar {5}; + + static_assert(std::is_same, decltype(foo)>::value); + static_assert(std::is_same::value); + } + + namespace test_typename_in_template_template_parameter + { + + template typename X> struct D; + + } + + namespace test_fallthrough_nodiscard_maybe_unused_attributes + { + + int f1() + { + return 42; + } + + [[nodiscard]] int f2() + { + [[maybe_unused]] auto unused = f1(); + + switch (f1()) + { + case 17: + f1(); + [[fallthrough]]; + case 42: + f1(); + } + return f1(); + } + + } + + namespace test_extended_aggregate_initialization + { + + struct base1 + { + int b1, b2 = 42; + }; + + struct base2 + { + base2() { + b3 = 42; + } + int b3; + }; + + struct derived : base1, base2 + { + int d; + }; + + derived d1 {{1, 2}, {}, 4}; // full initialization + derived d2 {{}, {}, 4}; // value-initialized bases + + } + + namespace test_general_range_based_for_loop + { + + struct iter + { + int i; + + int& operator* () + { + return i; + } + + const int& operator* () const + { + return i; + } + + iter& operator++() + { + ++i; + return *this; + } + }; + + struct sentinel + { + int i; + }; + + bool operator== (const iter& i, const sentinel& s) + { + return i.i == s.i; + } + + bool operator!= (const iter& i, const sentinel& s) + { + return !(i == s); + } + + struct range + { + iter begin() const + { + return {0}; + } + + sentinel end() const + { + return {5}; + } + }; + + void f() + { + range r {}; + + for (auto i : r) + { + [[maybe_unused]] auto v = i; + } + } + + } + + namespace test_lambda_capture_asterisk_this_by_value + { + + struct t + { + int i; + int foo() + { + return [*this]() + { + return i; + }(); + } + }; + + } + + namespace test_enum_class_construction + { + + enum class byte : unsigned char + {}; + + byte foo {42}; + + } + + namespace test_constexpr_if + { + + template + int f () + { + if constexpr(cond) + { + return 13; + } + else + { + return 42; + } + } + + } + + namespace test_selection_statement_with_initializer + { + + int f() + { + return 13; + } + + int f2() + { + if (auto i = f(); i > 0) + { + return 3; + } + + switch (auto i = f(); i + 4) + { + case 17: + return 2; + + default: + return 1; + } + } + + } + + namespace test_template_argument_deduction_for_class_templates + { + + template + struct pair + { + pair (T1 p1, T2 p2) + : m1 {p1}, + m2 {p2} + {} + + T1 m1; + T2 m2; + }; + + void f() + { + [[maybe_unused]] auto p = pair{13, 42u}; + } + + } + + namespace test_non_type_auto_template_parameters + { + + template + struct B + {}; + + B<5> b1; + B<'a'> b2; + + } + + namespace test_structured_bindings + { + + int arr[2] = { 1, 2 }; + std::pair pr = { 1, 2 }; + + auto f1() -> int(&)[2] + { + return arr; + } + + auto f2() -> std::pair& + { + return pr; + } + + struct S + { + int x1 : 2; + volatile double y1; + }; + + S f3() + { + return {}; + } + + auto [ x1, y1 ] = f1(); + auto& [ xr1, yr1 ] = f1(); + auto [ x2, y2 ] = f2(); + auto& [ xr2, yr2 ] = f2(); + const auto [ x3, y3 ] = f3(); + + } + + namespace test_exception_spec_type_system + { + + struct Good {}; + struct Bad {}; + + void g1() noexcept; + void g2(); + + template + Bad + f(T*, T*); + + template + Good + f(T1*, T2*); + + static_assert (std::is_same_v); + + } + + namespace test_inline_variables + { + + template void f(T) + {} + + template inline T g(T) + { + return T{}; + } + + template<> inline void f<>(int) + {} + + template<> int g<>(int) + { + return 5; + } + + } + +} // namespace cxx17 + +#endif // __cplusplus < 201703L + +]]) diff --git a/m4/ax_cxx_compile_stdcxx_14.m4 b/m4/ax_cxx_compile_stdcxx_14.m4 new file mode 100644 index 0000000..094db0d --- /dev/null +++ b/m4/ax_cxx_compile_stdcxx_14.m4 @@ -0,0 +1,34 @@ +# ============================================================================= +# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_14.html +# ============================================================================= +# +# SYNOPSIS +# +# AX_CXX_COMPILE_STDCXX_14([ext|noext], [mandatory|optional]) +# +# DESCRIPTION +# +# Check for baseline language coverage in the compiler for the C++14 +# standard; if necessary, add switches to CXX and CXXCPP to enable +# support. +# +# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX +# macro with the version set to C++14. The two optional arguments are +# forwarded literally as the second and third argument respectively. +# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for +# more information. If you want to use this macro, you also need to +# download the ax_cxx_compile_stdcxx.m4 file. +# +# LICENSE +# +# Copyright (c) 2015 Moritz Klammler +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 5 + +AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX]) +AC_DEFUN([AX_CXX_COMPILE_STDCXX_14], [AX_CXX_COMPILE_STDCXX([14], [$1], [$2])]) diff --git a/m4/ax_file_escapes.m4 b/m4/ax_file_escapes.m4 new file mode 100644 index 0000000..a86fdc3 --- /dev/null +++ b/m4/ax_file_escapes.m4 @@ -0,0 +1,30 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_FILE_ESCAPES +# +# DESCRIPTION +# +# Writes the specified data to the specified file. +# +# LICENSE +# +# Copyright (c) 2008 Tom Howard +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AC_DEFUN([AX_FILE_ESCAPES],[ +AX_DOLLAR="\$" +AX_SRB="\\135" +AX_SLB="\\133" +AX_BS="\\\\" +AX_DQ="\"" +]) diff --git a/m4/ax_is_release.m4 b/m4/ax_is_release.m4 new file mode 100644 index 0000000..9097ddb --- /dev/null +++ b/m4/ax_is_release.m4 @@ -0,0 +1,80 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_is_release.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_IS_RELEASE(POLICY) +# +# DESCRIPTION +# +# Determine whether the code is being configured as a release, or from +# git. Set the ax_is_release variable to 'yes' or 'no'. +# +# If building a release version, it is recommended that the configure +# script disable compiler errors and debug features, by conditionalising +# them on the ax_is_release variable. If building from git, these +# features should be enabled. +# +# The POLICY parameter specifies how ax_is_release is determined. It can +# take the following values: +# +# * git-directory: ax_is_release will be 'no' if a '.git' directory exists +# * minor-version: ax_is_release will be 'no' if the minor version number +# in $PACKAGE_VERSION is odd; this assumes +# $PACKAGE_VERSION follows the 'major.minor.micro' scheme +# * micro-version: ax_is_release will be 'no' if the micro version number +# in $PACKAGE_VERSION is odd; this assumes +# $PACKAGE_VERSION follows the 'major.minor.micro' scheme +# * dash-version: ax_is_release will be 'no' if there is a dash '-' +# in $PACKAGE_VERSION, for example 1.2-pre3, 1.2.42-a8b9 +# or 2.0-dirty (in particular this is suitable for use +# with git-version-gen) +# * always: ax_is_release will always be 'yes' +# * never: ax_is_release will always be 'no' +# +# Other policies may be added in future. +# +# LICENSE +# +# Copyright (c) 2015 Philip Withnall +# Copyright (c) 2016 Collabora Ltd. +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. + +#serial 7 + +AC_DEFUN([AX_IS_RELEASE],[ + AC_BEFORE([AC_INIT],[$0]) + + m4_case([$1], + [git-directory],[ + # $is_release = (.git directory does not exist) + AS_IF([test -d ${srcdir}/.git],[ax_is_release=no],[ax_is_release=yes]) + ], + [minor-version],[ + # $is_release = ($minor_version is even) + minor_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]][[^.]]*.\([[^.]][[^.]]*\).*/\1/'` + AS_IF([test "$(( $minor_version % 2 ))" -ne 0], + [ax_is_release=no],[ax_is_release=yes]) + ], + [micro-version],[ + # $is_release = ($micro_version is even) + micro_version=`echo "$PACKAGE_VERSION" | sed 's/[[^.]]*\.[[^.]]*\.\([[^.]]*\).*/\1/'` + AS_IF([test "$(( $micro_version % 2 ))" -ne 0], + [ax_is_release=no],[ax_is_release=yes]) + ], + [dash-version],[ + # $is_release = ($PACKAGE_VERSION has a dash) + AS_CASE([$PACKAGE_VERSION], + [*-*], [ax_is_release=no], + [*], [ax_is_release=yes]) + ], + [always],[ax_is_release=yes], + [never],[ax_is_release=no], + [ + AC_MSG_ERROR([Invalid policy. Valid policies: git-directory, minor-version, micro-version, dash-version, always, never.]) + ]) +]) diff --git a/m4/ax_lib_readline.m4 b/m4/ax_lib_readline.m4 new file mode 100644 index 0000000..0d0822b --- /dev/null +++ b/m4/ax_lib_readline.m4 @@ -0,0 +1,107 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_lib_readline.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_LIB_READLINE +# +# DESCRIPTION +# +# Searches for a readline compatible library. If found, defines +# `HAVE_LIBREADLINE'. If the found library has the `add_history' function, +# sets also `HAVE_READLINE_HISTORY'. Also checks for the locations of the +# necessary include files and sets `HAVE_READLINE_H' or +# `HAVE_READLINE_READLINE_H' and `HAVE_READLINE_HISTORY_H' or +# 'HAVE_HISTORY_H' if the corresponding include files exists. +# +# The libraries that may be readline compatible are `libedit', +# `libeditline' and `libreadline'. Sometimes we need to link a termcap +# library for readline to work, this macro tests these cases too by trying +# to link with `libtermcap', `libcurses' or `libncurses' before giving up. +# +# Here is an example of how to use the information provided by this macro +# to perform the necessary includes or declarations in a C file: +# +# #ifdef HAVE_LIBREADLINE +# # if defined(HAVE_READLINE_READLINE_H) +# # include +# # elif defined(HAVE_READLINE_H) +# # include +# # else /* !defined(HAVE_READLINE_H) */ +# extern char *readline (); +# # endif /* !defined(HAVE_READLINE_H) */ +# char *cmdline = NULL; +# #else /* !defined(HAVE_READLINE_READLINE_H) */ +# /* no readline */ +# #endif /* HAVE_LIBREADLINE */ +# +# #ifdef HAVE_READLINE_HISTORY +# # if defined(HAVE_READLINE_HISTORY_H) +# # include +# # elif defined(HAVE_HISTORY_H) +# # include +# # else /* !defined(HAVE_HISTORY_H) */ +# extern void add_history (); +# extern int write_history (); +# extern int read_history (); +# # endif /* defined(HAVE_READLINE_HISTORY_H) */ +# /* no history */ +# #endif /* HAVE_READLINE_HISTORY */ +# +# LICENSE +# +# Copyright (c) 2008 Ville Laurikari +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 8 + +AU_ALIAS([VL_LIB_READLINE], [AX_LIB_READLINE]) +AC_DEFUN([AX_LIB_READLINE], [ + AC_CACHE_CHECK([for a readline compatible library], + ax_cv_lib_readline, [ + ORIG_LIBS="$LIBS" + for readline_lib in readline edit editline; do + for termcap_lib in "" termcap curses ncurses; do + if test -z "$termcap_lib"; then + TRY_LIB="-l$readline_lib" + else + TRY_LIB="-l$readline_lib -l$termcap_lib" + fi + LIBS="$ORIG_LIBS $TRY_LIB" + AC_LINK_IFELSE([AC_LANG_CALL([], [readline])], [ax_cv_lib_readline="$TRY_LIB"]) + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -n "$ax_cv_lib_readline"; then + break + fi + done + if test -z "$ax_cv_lib_readline"; then + ax_cv_lib_readline="no" + fi + LIBS="$ORIG_LIBS" + ]) + + if test "$ax_cv_lib_readline" != "no"; then + LIBS="$LIBS $ax_cv_lib_readline" + AC_DEFINE(HAVE_LIBREADLINE, 1, + [Define if you have a readline compatible library]) + AC_CHECK_HEADERS(readline.h readline/readline.h) + AC_CACHE_CHECK([whether readline supports history], + ax_cv_lib_readline_history, [ + ax_cv_lib_readline_history="no" + AC_LINK_IFELSE([AC_LANG_CALL([], [add_history])], [ax_cv_lib_readline_history="yes"]) + ]) + if test "$ax_cv_lib_readline_history" = "yes"; then + AC_DEFINE(HAVE_READLINE_HISTORY, 1, + [Define if your readline library has \`add_history']) + AC_CHECK_HEADERS(history.h readline/history.h) + fi + fi +])dnl diff --git a/m4/ax_require_defined.m4 b/m4/ax_require_defined.m4 new file mode 100644 index 0000000..17c3eab --- /dev/null +++ b/m4/ax_require_defined.m4 @@ -0,0 +1,37 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_require_defined.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_REQUIRE_DEFINED(MACRO) +# +# DESCRIPTION +# +# AX_REQUIRE_DEFINED is a simple helper for making sure other macros have +# been defined and thus are available for use. This avoids random issues +# where a macro isn't expanded. Instead the configure script emits a +# non-fatal: +# +# ./configure: line 1673: AX_CFLAGS_WARN_ALL: command not found +# +# It's like AC_REQUIRE except it doesn't expand the required macro. +# +# Here's an example: +# +# AX_REQUIRE_DEFINED([AX_CHECK_LINK_FLAG]) +# +# LICENSE +# +# Copyright (c) 2014 Mike Frysinger +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 2 + +AC_DEFUN([AX_REQUIRE_DEFINED], [dnl + m4_ifndef([$1], [m4_fatal([macro ]$1[ is not defined; is a m4 file missing?])]) +])dnl AX_REQUIRE_DEFINED diff --git a/m4/ax_valgrind_check.m4 b/m4/ax_valgrind_check.m4 new file mode 100644 index 0000000..7033798 --- /dev/null +++ b/m4/ax_valgrind_check.m4 @@ -0,0 +1,239 @@ +# =========================================================================== +# https://www.gnu.org/software/autoconf-archive/ax_valgrind_check.html +# =========================================================================== +# +# SYNOPSIS +# +# AX_VALGRIND_DFLT(memcheck|helgrind|drd|sgcheck, on|off) +# AX_VALGRIND_CHECK() +# +# DESCRIPTION +# +# AX_VALGRIND_CHECK checks whether Valgrind is present and, if so, allows +# running `make check` under a variety of Valgrind tools to check for +# memory and threading errors. +# +# Defines VALGRIND_CHECK_RULES which should be substituted in your +# Makefile; and $enable_valgrind which can be used in subsequent configure +# output. VALGRIND_ENABLED is defined and substituted, and corresponds to +# the value of the --enable-valgrind option, which defaults to being +# enabled if Valgrind is installed and disabled otherwise. Individual +# Valgrind tools can be disabled via --disable-valgrind-, the +# default is configurable via the AX_VALGRIND_DFLT command or is to use +# all commands not disabled via AX_VALGRIND_DFLT. All AX_VALGRIND_DFLT +# calls must be made before the call to AX_VALGRIND_CHECK. +# +# If unit tests are written using a shell script and automake's +# LOG_COMPILER system, the $(VALGRIND) variable can be used within the +# shell scripts to enable Valgrind, as described here: +# +# https://www.gnu.org/software/gnulib/manual/html_node/Running-self_002dtests-under-valgrind.html +# +# Usage example: +# +# configure.ac: +# +# AX_VALGRIND_DFLT([sgcheck], [off]) +# AX_VALGRIND_CHECK +# +# in each Makefile.am with tests: +# +# @VALGRIND_CHECK_RULES@ +# VALGRIND_SUPPRESSIONS_FILES = my-project.supp +# EXTRA_DIST = my-project.supp +# +# This results in a "check-valgrind" rule being added. Running `make +# check-valgrind` in that directory will recursively run the module's test +# suite (`make check`) once for each of the available Valgrind tools (out +# of memcheck, helgrind and drd) while the sgcheck will be skipped unless +# enabled again on the commandline with --enable-valgrind-sgcheck. The +# results for each check will be output to test-suite-$toolname.log. The +# target will succeed if there are zero errors and fail otherwise. +# +# Alternatively, a "check-valgrind-$TOOL" rule will be added, for $TOOL in +# memcheck, helgrind, drd and sgcheck. These are useful because often only +# some of those tools can be ran cleanly on a codebase. +# +# The macro supports running with and without libtool. +# +# LICENSE +# +# Copyright (c) 2014, 2015, 2016 Philip Withnall +# +# Copying and distribution of this file, with or without modification, are +# permitted in any medium without royalty provided the copyright notice +# and this notice are preserved. This file is offered as-is, without any +# warranty. + +#serial 17 + +dnl Configured tools +m4_define([valgrind_tool_list], [[memcheck], [helgrind], [drd], [sgcheck]]) +m4_set_add_all([valgrind_exp_tool_set], [sgcheck]) +m4_foreach([vgtool], [valgrind_tool_list], + [m4_define([en_dflt_valgrind_]vgtool, [on])]) + +AC_DEFUN([AX_VALGRIND_DFLT],[ + m4_define([en_dflt_valgrind_$1], [$2]) +])dnl + +AM_EXTRA_RECURSIVE_TARGETS([check-valgrind]) +m4_foreach([vgtool], [valgrind_tool_list], + [AM_EXTRA_RECURSIVE_TARGETS([check-valgrind-]vgtool)]) + +AC_DEFUN([AX_VALGRIND_CHECK],[ + dnl Check for --enable-valgrind + AC_ARG_ENABLE([valgrind], + [AS_HELP_STRING([--enable-valgrind], [Whether to enable Valgrind on the unit tests])], + [enable_valgrind=$enableval],[enable_valgrind=]) + + AS_IF([test "$enable_valgrind" != "no"],[ + # Check for Valgrind. + AC_CHECK_PROG([VALGRIND],[valgrind],[valgrind]) + AS_IF([test "$VALGRIND" = ""],[ + AS_IF([test "$enable_valgrind" = "yes"],[ + AC_MSG_ERROR([Could not find valgrind; either install it or reconfigure with --disable-valgrind]) + ],[ + enable_valgrind=no + ]) + ],[ + enable_valgrind=yes + ]) + ]) + + AM_CONDITIONAL([VALGRIND_ENABLED],[test "$enable_valgrind" = "yes"]) + AC_SUBST([VALGRIND_ENABLED],[$enable_valgrind]) + + # Check for Valgrind tools we care about. + [valgrind_enabled_tools=] + m4_foreach([vgtool],[valgrind_tool_list],[ + AC_ARG_ENABLE([valgrind-]vgtool, + m4_if(m4_defn([en_dflt_valgrind_]vgtool),[off],dnl +[AS_HELP_STRING([--enable-valgrind-]vgtool, [Whether to use ]vgtool[ during the Valgrind tests])],dnl +[AS_HELP_STRING([--disable-valgrind-]vgtool, [Whether to skip ]vgtool[ during the Valgrind tests])]), + [enable_valgrind_]vgtool[=$enableval], + [enable_valgrind_]vgtool[=]) + AS_IF([test "$enable_valgrind" = "no"],[ + enable_valgrind_]vgtool[=no], + [test "$enable_valgrind_]vgtool[" ]dnl +m4_if(m4_defn([en_dflt_valgrind_]vgtool), [off], [= "yes"], [!= "no"]),[ + AC_CACHE_CHECK([for Valgrind tool ]vgtool, + [ax_cv_valgrind_tool_]vgtool,[ + ax_cv_valgrind_tool_]vgtool[=no + m4_set_contains([valgrind_exp_tool_set],vgtool, + [m4_define([vgtoolx],[exp-]vgtool)], + [m4_define([vgtoolx],vgtool)]) + AS_IF([`$VALGRIND --tool=]vgtoolx[ --help >/dev/null 2>&1`],[ + ax_cv_valgrind_tool_]vgtool[=yes + ]) + ]) + AS_IF([test "$ax_cv_valgrind_tool_]vgtool[" = "no"],[ + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + AC_MSG_ERROR([Valgrind does not support ]vgtool[; reconfigure with --disable-valgrind-]vgtool) + ],[ + enable_valgrind_]vgtool[=no + ]) + ],[ + enable_valgrind_]vgtool[=yes + ]) + ]) + AS_IF([test "$enable_valgrind_]vgtool[" = "yes"],[ + valgrind_enabled_tools="$valgrind_enabled_tools ]m4_bpatsubst(vgtool,[^exp-])[" + ]) + AC_SUBST([ENABLE_VALGRIND_]vgtool,[$enable_valgrind_]vgtool) + ]) + AC_SUBST([valgrind_tools],["]m4_join([ ], valgrind_tool_list)["]) + AC_SUBST([valgrind_enabled_tools],[$valgrind_enabled_tools]) + +[VALGRIND_CHECK_RULES=' +# Valgrind check +# +# Optional: +# - VALGRIND_SUPPRESSIONS_FILES: Space-separated list of Valgrind suppressions +# files to load. (Default: empty) +# - VALGRIND_FLAGS: General flags to pass to all Valgrind tools. +# (Default: --num-callers=30) +# - VALGRIND_$toolname_FLAGS: Flags to pass to Valgrind $toolname (one of: +# memcheck, helgrind, drd, sgcheck). (Default: various) + +# Optional variables +VALGRIND_SUPPRESSIONS ?= $(addprefix --suppressions=,$(VALGRIND_SUPPRESSIONS_FILES)) +VALGRIND_FLAGS ?= --num-callers=30 +VALGRIND_memcheck_FLAGS ?= --leak-check=full --show-reachable=no +VALGRIND_helgrind_FLAGS ?= --history-level=approx +VALGRIND_drd_FLAGS ?= +VALGRIND_sgcheck_FLAGS ?= + +# Internal use +valgrind_log_files = $(addprefix test-suite-,$(addsuffix .log,$(valgrind_tools))) + +valgrind_memcheck_flags = --tool=memcheck $(VALGRIND_memcheck_FLAGS) +valgrind_helgrind_flags = --tool=helgrind $(VALGRIND_helgrind_FLAGS) +valgrind_drd_flags = --tool=drd $(VALGRIND_drd_FLAGS) +valgrind_sgcheck_flags = --tool=exp-sgcheck $(VALGRIND_sgcheck_FLAGS) + +valgrind_quiet = $(valgrind_quiet_$(V)) +valgrind_quiet_ = $(valgrind_quiet_$(AM_DEFAULT_VERBOSITY)) +valgrind_quiet_0 = --quiet +valgrind_v_use = $(valgrind_v_use_$(V)) +valgrind_v_use_ = $(valgrind_v_use_$(AM_DEFAULT_VERBOSITY)) +valgrind_v_use_0 = @echo " USE " $(patsubst check-valgrind-%-am,%,$''@):; + +# Support running with and without libtool. +ifneq ($(LIBTOOL),) +valgrind_lt = $(LIBTOOL) $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=execute +else +valgrind_lt = +endif + +# Use recursive makes in order to ignore errors during check +check-valgrind-am: +ifeq ($(VALGRIND_ENABLED),yes) + $(A''M_V_at)$(MAKE) $(AM_MAKEFLAGS) -k \ + $(foreach tool, $(valgrind_enabled_tools), check-valgrind-$(tool)) +else + @echo "Need to reconfigure with --enable-valgrind" +endif + +# Valgrind running +VALGRIND_TESTS_ENVIRONMENT = \ + $(TESTS_ENVIRONMENT) \ + env VALGRIND=$(VALGRIND) \ + G_SLICE=always-malloc,debug-blocks \ + G_DEBUG=fatal-warnings,fatal-criticals,gc-friendly + +VALGRIND_LOG_COMPILER = \ + $(valgrind_lt) \ + $(VALGRIND) $(VALGRIND_SUPPRESSIONS) --error-exitcode=1 $(VALGRIND_FLAGS) + +define valgrind_tool_rule +check-valgrind-$(1)-am: +ifeq ($$(VALGRIND_ENABLED)-$$(ENABLE_VALGRIND_$(1)),yes-yes) +ifneq ($$(TESTS),) + $$(valgrind_v_use)$$(MAKE) check-TESTS \ + TESTS_ENVIRONMENT="$$(VALGRIND_TESTS_ENVIRONMENT)" \ + LOG_COMPILER="$$(VALGRIND_LOG_COMPILER)" \ + LOG_FLAGS="$$(valgrind_$(1)_flags)" \ + TEST_SUITE_LOG=test-suite-$(1).log +endif +else ifeq ($$(VALGRIND_ENABLED),yes) + @echo "Need to reconfigure with --enable-valgrind-$(1)" +else + @echo "Need to reconfigure with --enable-valgrind" +endif +endef + +$(foreach tool,$(valgrind_tools),$(eval $(call valgrind_tool_rule,$(tool)))) + +A''M_DISTCHECK_CONFIGURE_FLAGS ?= +A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-valgrind + +MOSTLYCLEANFILES ?= +MOSTLYCLEANFILES += $(valgrind_log_files) + +.PHONY: check-valgrind $(add-prefix check-valgrind-,$(valgrind_tools)) +'] + + AC_SUBST([VALGRIND_CHECK_RULES]) + m4_ifdef([_AM_SUBST_NOTMAKE], [_AM_SUBST_NOTMAKE([VALGRIND_CHECK_RULES])]) +]) diff --git a/m4/guile-2.2.m4 b/m4/guile-2.2.m4 new file mode 100644 index 0000000..89823e9 --- /dev/null +++ b/m4/guile-2.2.m4 @@ -0,0 +1,394 @@ +## Autoconf macros for working with Guile. +## +## Copyright (C) 1998,2001, 2006, 2010, 2012, 2013, 2014 Free Software Foundation, Inc. +## +## This library is free software; you can redistribute it and/or +## modify it under the terms of the GNU Lesser General Public License +## as published by the Free Software Foundation; either version 3 of +## the License, or (at your option) any later version. +## +## This library is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +## Lesser General Public License for more details. +## +## You should have received a copy of the GNU Lesser General Public +## License along with this library; if not, write to the Free Software +## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +## 02110-1301 USA + +# serial 10 + +## Index +## ----- +## +## GUILE_PKG -- find Guile development files +## GUILE_PROGS -- set paths to Guile interpreter, config and tool programs +## GUILE_FLAGS -- set flags for compiling and linking with Guile +## GUILE_SITE_DIR -- find path to Guile "site" directories +## GUILE_CHECK -- evaluate Guile Scheme code and capture the return value +## GUILE_MODULE_CHECK -- check feature of a Guile Scheme module +## GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module +## GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable +## GUILE_MODULE_EXPORTS -- check if a module exports a variable +## GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable + +## Code +## ---- + +## NOTE: Comments preceding an AC_DEFUN (starting from "Usage:") are massaged +## into doc/ref/autoconf-macros.texi (see Makefile.am in that directory). + +# GUILE_PKG -- find Guile development files +# +# Usage: GUILE_PKG([VERSIONS]) +# +# This macro runs the @code{pkg-config} tool to find development files +# for an available version of Guile. +# +# By default, this macro will search for the latest stable version of +# Guile (e.g. 2.2), falling back to the previous stable version +# (e.g. 2.0) if it is available. If no guile-@var{VERSION}.pc file is +# found, an error is signalled. The found version is stored in +# @var{GUILE_EFFECTIVE_VERSION}. +# +# If @code{GUILE_PROGS} was already invoked, this macro ensures that the +# development files have the same effective version as the Guile +# program. +# +# @var{GUILE_EFFECTIVE_VERSION} is marked for substitution, as by +# @code{AC_SUBST}. +# +AC_DEFUN([GUILE_PKG], + [PKG_PROG_PKG_CONFIG + _guile_versions_to_search="m4_default([$1], [2.2 2.0 1.8])" + if test -n "$GUILE_EFFECTIVE_VERSION"; then + _guile_tmp="" + for v in $_guile_versions_to_search; do + if test "$v" = "$GUILE_EFFECTIVE_VERSION"; then + _guile_tmp=$v + fi + done + if test -z "$_guile_tmp"; then + AC_MSG_FAILURE([searching for guile development files for versions $_guile_versions_to_search, but previously found $GUILE version $GUILE_EFFECTIVE_VERSION]) + fi + _guile_versions_to_search=$GUILE_EFFECTIVE_VERSION + fi + GUILE_EFFECTIVE_VERSION="" + _guile_errors="" + for v in $_guile_versions_to_search; do + if test -z "$GUILE_EFFECTIVE_VERSION"; then + AC_MSG_NOTICE([checking for guile $v]) + PKG_CHECK_EXISTS([guile-$v], [GUILE_EFFECTIVE_VERSION=$v], []) + fi + done + + if test -z "$GUILE_EFFECTIVE_VERSION"; then + AC_MSG_ERROR([ +No Guile development packages were found. + +Please verify that you have Guile installed. If you installed Guile +from a binary distribution, please verify that you have also installed +the development packages. If you installed it yourself, you might need +to adjust your PKG_CONFIG_PATH; see the pkg-config man page for more. +]) + fi + AC_MSG_NOTICE([found guile $GUILE_EFFECTIVE_VERSION]) + AC_SUBST([GUILE_EFFECTIVE_VERSION]) + ]) + +# GUILE_FLAGS -- set flags for compiling and linking with Guile +# +# Usage: GUILE_FLAGS +# +# This macro runs the @code{pkg-config} tool to find out how to compile +# and link programs against Guile. It sets four variables: +# @var{GUILE_CFLAGS}, @var{GUILE_LDFLAGS}, @var{GUILE_LIBS}, and +# @var{GUILE_LTLIBS}. +# +# @var{GUILE_CFLAGS}: flags to pass to a C or C++ compiler to build code that +# uses Guile header files. This is almost always just one or more @code{-I} +# flags. +# +# @var{GUILE_LDFLAGS}: flags to pass to the compiler to link a program +# against Guile. This includes @code{-lguile-@var{VERSION}} for the +# Guile library itself, and may also include one or more @code{-L} flag +# to tell the compiler where to find the libraries. But it does not +# include flags that influence the program's runtime search path for +# libraries, and will therefore lead to a program that fails to start, +# unless all necessary libraries are installed in a standard location +# such as @file{/usr/lib}. +# +# @var{GUILE_LIBS} and @var{GUILE_LTLIBS}: flags to pass to the compiler or to +# libtool, respectively, to link a program against Guile. It includes flags +# that augment the program's runtime search path for libraries, so that shared +# libraries will be found at the location where they were during linking, even +# in non-standard locations. @var{GUILE_LIBS} is to be used when linking the +# program directly with the compiler, whereas @var{GUILE_LTLIBS} is to be used +# when linking the program is done through libtool. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_FLAGS], + [AC_REQUIRE([GUILE_PKG]) + PKG_CHECK_MODULES(GUILE, [guile-$GUILE_EFFECTIVE_VERSION]) + + dnl GUILE_CFLAGS and GUILE_LIBS are already defined and AC_SUBST'd by + dnl PKG_CHECK_MODULES. But GUILE_LIBS to pkg-config is GUILE_LDFLAGS + dnl to us. + + GUILE_LDFLAGS=$GUILE_LIBS + + dnl Determine the platform dependent parameters needed to use rpath. + dnl AC_LIB_LINKFLAGS_FROM_LIBS is defined in gnulib/m4/lib-link.m4 and needs + dnl the file gnulib/build-aux/config.rpath. + AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LIBS], [$GUILE_LDFLAGS], []) + GUILE_LIBS="$GUILE_LDFLAGS $GUILE_LIBS" + AC_LIB_LINKFLAGS_FROM_LIBS([GUILE_LTLIBS], [$GUILE_LDFLAGS], [yes]) + GUILE_LTLIBS="$GUILE_LDFLAGS $GUILE_LTLIBS" + + AC_SUBST([GUILE_EFFECTIVE_VERSION]) + AC_SUBST([GUILE_CFLAGS]) + AC_SUBST([GUILE_LDFLAGS]) + AC_SUBST([GUILE_LIBS]) + AC_SUBST([GUILE_LTLIBS]) + ]) + +# GUILE_SITE_DIR -- find path to Guile site directories +# +# Usage: GUILE_SITE_DIR +# +# This looks for Guile's "site" directories. The variable @var{GUILE_SITE} will +# be set to Guile's "site" directory for Scheme source files (usually something +# like PREFIX/share/guile/site). @var{GUILE_SITE_CCACHE} will be set to the +# directory for compiled Scheme files also known as @code{.go} files +# (usually something like +# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/site-ccache). +# @var{GUILE_EXTENSION} will be set to the directory for compiled C extensions +# (usually something like +# PREFIX/lib/guile/@var{GUILE_EFFECTIVE_VERSION}/extensions). The latter two +# are set to blank if the particular version of Guile does not support +# them. Note that this macro will run the macros @code{GUILE_PKG} and +# @code{GUILE_PROGS} if they have not already been run. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_SITE_DIR], + [AC_REQUIRE([GUILE_PKG]) + AC_REQUIRE([GUILE_PROGS]) + AC_MSG_CHECKING(for Guile site directory) + GUILE_SITE=`$PKG_CONFIG --print-errors --variable=sitedir guile-$GUILE_EFFECTIVE_VERSION` + AC_MSG_RESULT($GUILE_SITE) + if test "$GUILE_SITE" = ""; then + AC_MSG_FAILURE(sitedir not found) + fi + AC_SUBST(GUILE_SITE) + AC_MSG_CHECKING([for Guile site-ccache directory using pkgconfig]) + GUILE_SITE_CCACHE=`$PKG_CONFIG --variable=siteccachedir guile-$GUILE_EFFECTIVE_VERSION` + if test "$GUILE_SITE_CCACHE" = ""; then + AC_MSG_RESULT(no) + AC_MSG_CHECKING([for Guile site-ccache directory using interpreter]) + GUILE_SITE_CCACHE=`$GUILE -c "(display (if (defined? '%site-ccache-dir) (%site-ccache-dir) \"\"))"` + if test $? != "0" -o "$GUILE_SITE_CCACHE" = ""; then + AC_MSG_RESULT(no) + GUILE_SITE_CCACHE="" + AC_MSG_WARN([siteccachedir not found]) + fi + fi + AC_MSG_RESULT($GUILE_SITE_CCACHE) + AC_SUBST([GUILE_SITE_CCACHE]) + AC_MSG_CHECKING(for Guile extensions directory) + GUILE_EXTENSION=`$PKG_CONFIG --print-errors --variable=extensiondir guile-$GUILE_EFFECTIVE_VERSION` + AC_MSG_RESULT($GUILE_EXTENSION) + if test "$GUILE_EXTENSION" = ""; then + GUILE_EXTENSION="" + AC_MSG_WARN(extensiondir not found) + fi + AC_SUBST(GUILE_EXTENSION) + ]) + +# GUILE_PROGS -- set paths to Guile interpreter, config and tool programs +# +# Usage: GUILE_PROGS([VERSION]) +# +# This macro looks for programs @code{guile} and @code{guild}, setting +# variables @var{GUILE} and @var{GUILD} to their paths, respectively. +# The macro will attempt to find @code{guile} with the suffix of +# @code{-X.Y}, followed by looking for it with the suffix @code{X.Y}, and +# then fall back to looking for @code{guile} with no suffix. If +# @code{guile} is still not found, signal an error. The suffix, if any, +# that was required to find @code{guile} will be used for @code{guild} +# as well. +# +# By default, this macro will search for the latest stable version of +# Guile (e.g. 2.2). x.y or x.y.z versions can be specified. If an older +# version is found, the macro will signal an error. +# +# The effective version of the found @code{guile} is set to +# @var{GUILE_EFFECTIVE_VERSION}. This macro ensures that the effective +# version is compatible with the result of a previous invocation of +# @code{GUILE_FLAGS}, if any. +# +# As a legacy interface, it also looks for @code{guile-config} and +# @code{guile-tools}, setting @var{GUILE_CONFIG} and @var{GUILE_TOOLS}. +# +# The variables are marked for substitution, as by @code{AC_SUBST}. +# +AC_DEFUN([GUILE_PROGS], + [_guile_required_version="m4_default([$1], [$GUILE_EFFECTIVE_VERSION])" + if test -z "$_guile_required_version"; then + _guile_required_version=2.2 + fi + + _guile_candidates=guile + _tmp= + for v in `echo "$_guile_required_version" | tr . ' '`; do + if test -n "$_tmp"; then _tmp=$_tmp.; fi + _tmp=$_tmp$v + _guile_candidates="guile-$_tmp guile$_tmp $_guile_candidates" + done + + AC_PATH_PROGS(GUILE,[$_guile_candidates]) + if test -z "$GUILE"; then + AC_MSG_ERROR([guile required but not found]) + fi + + _guile_suffix=`echo "$GUILE" | sed -e 's,^.*/guile\(.*\)$,\1,'` + _guile_effective_version=`$GUILE -c "(display (effective-version))"` + if test -z "$GUILE_EFFECTIVE_VERSION"; then + GUILE_EFFECTIVE_VERSION=$_guile_effective_version + elif test "$GUILE_EFFECTIVE_VERSION" != "$_guile_effective_version"; then + AC_MSG_ERROR([found development files for Guile $GUILE_EFFECTIVE_VERSION, but $GUILE has effective version $_guile_effective_version]) + fi + + _guile_major_version=`$GUILE -c "(display (major-version))"` + _guile_minor_version=`$GUILE -c "(display (minor-version))"` + _guile_micro_version=`$GUILE -c "(display (micro-version))"` + _guile_prog_version="$_guile_major_version.$_guile_minor_version.$_guile_micro_version" + + AC_MSG_CHECKING([for Guile version >= $_guile_required_version]) + _major_version=`echo $_guile_required_version | cut -d . -f 1` + _minor_version=`echo $_guile_required_version | cut -d . -f 2` + _micro_version=`echo $_guile_required_version | cut -d . -f 3` + if test "$_guile_major_version" -gt "$_major_version"; then + true + elif test "$_guile_major_version" -eq "$_major_version"; then + if test "$_guile_minor_version" -gt "$_minor_version"; then + true + elif test "$_guile_minor_version" -eq "$_minor_version"; then + if test -n "$_micro_version"; then + if test "$_guile_micro_version" -lt "$_micro_version"; then + AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found]) + fi + fi + elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then + # Allow prereleases that have the right effective version. + true + else + as_fn_error $? "Guile $_guile_required_version required, but $_guile_prog_version found" "$LINENO" 5 + fi + elif test "$GUILE_EFFECTIVE_VERSION" = "$_major_version.$_minor_version" -a -z "$_micro_version"; then + # Allow prereleases that have the right effective version. + true + else + AC_MSG_ERROR([Guile $_guile_required_version required, but $_guile_prog_version found]) + fi + AC_MSG_RESULT([$_guile_prog_version]) + + AC_PATH_PROG(GUILD,[guild$_guile_suffix]) + AC_SUBST(GUILD) + + AC_PATH_PROG(GUILE_CONFIG,[guile-config$_guile_suffix]) + AC_SUBST(GUILE_CONFIG) + if test -n "$GUILD"; then + GUILE_TOOLS=$GUILD + else + AC_PATH_PROG(GUILE_TOOLS,[guile-tools$_guile_suffix]) + fi + AC_SUBST(GUILE_TOOLS) + ]) + +# GUILE_CHECK -- evaluate Guile Scheme code and capture the return value +# +# Usage: GUILE_CHECK_RETVAL(var,check) +# +# @var{var} is a shell variable name to be set to the return value. +# @var{check} is a Guile Scheme expression, evaluated with "$GUILE -c", and +# returning either 0 or non-#f to indicate the check passed. +# Non-0 number or #f indicates failure. +# Avoid using the character "#" since that confuses autoconf. +# +AC_DEFUN([GUILE_CHECK], + [AC_REQUIRE([GUILE_PROGS]) + $GUILE -c "$2" > /dev/null 2>&1 + $1=$? + ]) + +# GUILE_MODULE_CHECK -- check feature of a Guile Scheme module +# +# Usage: GUILE_MODULE_CHECK(var,module,featuretest,description) +# +# @var{var} is a shell variable name to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{featuretest} is an expression acceptable to GUILE_CHECK, q.v. +# @var{description} is a present-tense verb phrase (passed to AC_MSG_CHECKING). +# +AC_DEFUN([GUILE_MODULE_CHECK], + [AC_MSG_CHECKING([if $2 $4]) + GUILE_CHECK($1,(use-modules $2) (exit ((lambda () $3)))) + if test "$$1" = "0" ; then $1=yes ; else $1=no ; fi + AC_MSG_RESULT($$1) + ]) + +# GUILE_MODULE_AVAILABLE -- check availability of a Guile Scheme module +# +# Usage: GUILE_MODULE_AVAILABLE(var,module) +# +# @var{var} is a shell variable name to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# +AC_DEFUN([GUILE_MODULE_AVAILABLE], + [GUILE_MODULE_CHECK($1,$2,0,is available) + ]) + +# GUILE_MODULE_REQUIRED -- fail if a Guile Scheme module is unavailable +# +# Usage: GUILE_MODULE_REQUIRED(symlist) +# +# @var{symlist} is a list of symbols, WITHOUT surrounding parens, +# like: ice-9 common-list. +# +AC_DEFUN([GUILE_MODULE_REQUIRED], + [GUILE_MODULE_AVAILABLE(ac_guile_module_required, ($1)) + if test "$ac_guile_module_required" = "no" ; then + AC_MSG_ERROR([required guile module not found: ($1)]) + fi + ]) + +# GUILE_MODULE_EXPORTS -- check if a module exports a variable +# +# Usage: GUILE_MODULE_EXPORTS(var,module,modvar) +# +# @var{var} is a shell variable to be set to "yes" or "no". +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{modvar} is the Guile Scheme variable to check. +# +AC_DEFUN([GUILE_MODULE_EXPORTS], + [GUILE_MODULE_CHECK($1,$2,$3,exports `$3') + ]) + +# GUILE_MODULE_REQUIRED_EXPORT -- fail if a module doesn't export a variable +# +# Usage: GUILE_MODULE_REQUIRED_EXPORT(module,modvar) +# +# @var{module} is a list of symbols, like: (ice-9 common-list). +# @var{modvar} is the Guile Scheme variable to check. +# +AC_DEFUN([GUILE_MODULE_REQUIRED_EXPORT], + [GUILE_MODULE_EXPORTS(guile_module_required_export,$1,$2) + if test "$guile_module_required_export" = "no" ; then + AC_MSG_ERROR([module $1 does not export $2; required]) + fi + ]) + +## guile.m4 ends here diff --git a/m4/host-cpu-c-abi.m4 b/m4/host-cpu-c-abi.m4 new file mode 100644 index 0000000..6db2aa2 --- /dev/null +++ b/m4/host-cpu-c-abi.m4 @@ -0,0 +1,675 @@ +# host-cpu-c-abi.m4 serial 13 +dnl Copyright (C) 2002-2020 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Bruno Haible and Sam Steingold. + +dnl Sets the HOST_CPU variable to the canonical name of the CPU. +dnl Sets the HOST_CPU_C_ABI variable to the canonical name of the CPU with its +dnl C language ABI (application binary interface). +dnl Also defines __${HOST_CPU}__ and __${HOST_CPU_C_ABI}__ as C macros in +dnl config.h. +dnl +dnl This canonical name can be used to select a particular assembly language +dnl source file that will interoperate with C code on the given host. +dnl +dnl For example: +dnl * 'i386' and 'sparc' are different canonical names, because code for i386 +dnl will not run on SPARC CPUs and vice versa. They have different +dnl instruction sets. +dnl * 'sparc' and 'sparc64' are different canonical names, because code for +dnl 'sparc' and code for 'sparc64' cannot be linked together: 'sparc' code +dnl contains 32-bit instructions, whereas 'sparc64' code contains 64-bit +dnl instructions. A process on a SPARC CPU can be in 32-bit mode or in 64-bit +dnl mode, but not both. +dnl * 'mips' and 'mipsn32' are different canonical names, because they use +dnl different argument passing and return conventions for C functions, and +dnl although the instruction set of 'mips' is a large subset of the +dnl instruction set of 'mipsn32'. +dnl * 'mipsn32' and 'mips64' are different canonical names, because they use +dnl different sizes for the C types like 'int' and 'void *', and although +dnl the instruction sets of 'mipsn32' and 'mips64' are the same. +dnl * The same canonical name is used for different endiannesses. You can +dnl determine the endianness through preprocessor symbols: +dnl - 'arm': test __ARMEL__. +dnl - 'mips', 'mipsn32', 'mips64': test _MIPSEB vs. _MIPSEL. +dnl - 'powerpc64': test _BIG_ENDIAN vs. _LITTLE_ENDIAN. +dnl * The same name 'i386' is used for CPUs of type i386, i486, i586 +dnl (Pentium), AMD K7, Pentium II, Pentium IV, etc., because +dnl - Instructions that do not exist on all of these CPUs (cmpxchg, +dnl MMX, SSE, SSE2, 3DNow! etc.) are not frequently used. If your +dnl assembly language source files use such instructions, you will +dnl need to make the distinction. +dnl - Speed of execution of the common instruction set is reasonable across +dnl the entire family of CPUs. If you have assembly language source files +dnl that are optimized for particular CPU types (like GNU gmp has), you +dnl will need to make the distinction. +dnl See . +AC_DEFUN([gl_HOST_CPU_C_ABI], +[ + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_C_ASM]) + AC_CACHE_CHECK([host CPU and C ABI], [gl_cv_host_cpu_c_abi], + [case "$host_cpu" in + +changequote(,)dnl + i[34567]86 ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=i386 + ;; + + x86_64 ) + # On x86_64 systems, the C compiler may be generating code in one of + # these ABIs: + # - 64-bit instruction set, 64-bit pointers, 64-bit 'long': x86_64. + # - 64-bit instruction set, 64-bit pointers, 32-bit 'long': x86_64 + # with native Windows (mingw, MSVC). + # - 64-bit instruction set, 32-bit pointers, 32-bit 'long': x86_64-x32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': i386. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (defined __x86_64__ || defined __amd64__ \ + || defined _M_X64 || defined _M_AMD64) + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=x86_64-x32], + [gl_cv_host_cpu_c_abi=x86_64])], + [gl_cv_host_cpu_c_abi=i386]) + ;; + +changequote(,)dnl + alphaev[4-8] | alphaev56 | alphapca5[67] | alphaev6[78] ) +changequote([,])dnl + gl_cv_host_cpu_c_abi=alpha + ;; + + arm* | aarch64 ) + # Assume arm with EABI. + # On arm64 systems, the C compiler may be generating code in one of + # these ABIs: + # - aarch64 instruction set, 64-bit pointers, 64-bit 'long': arm64. + # - aarch64 instruction set, 32-bit pointers, 32-bit 'long': arm64-ilp32. + # - 32-bit instruction set, 32-bit pointers, 32-bit 'long': arm or armhf. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __aarch64__ + int ok; + #else + error fail + #endif + ]])], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __ILP32__ || defined _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=arm64-ilp32], + [gl_cv_host_cpu_c_abi=arm64])], + [# Don't distinguish little-endian and big-endian arm, since they + # don't require different machine code for simple operations and + # since the user can distinguish them through the preprocessor + # defines __ARMEL__ vs. __ARMEB__. + # But distinguish arm which passes floating-point arguments and + # return values in integer registers (r0, r1, ...) - this is + # gcc -mfloat-abi=soft or gcc -mfloat-abi=softfp - from arm which + # passes them in float registers (s0, s1, ...) and double registers + # (d0, d1, ...) - this is gcc -mfloat-abi=hard. GCC 4.6 or newer + # sets the preprocessor defines __ARM_PCS (for the first case) and + # __ARM_PCS_VFP (for the second case), but older GCC does not. + echo 'double ddd; void func (double dd) { ddd = dd; }' > conftest.c + # Look for a reference to the register d0 in the .s file. + AC_TRY_COMMAND(${CC-cc} $CFLAGS $CPPFLAGS $gl_c_asm_opt conftest.c) >/dev/null 2>&1 + if LC_ALL=C grep 'd0,' conftest.$gl_asmext >/dev/null; then + gl_cv_host_cpu_c_abi=armhf + else + gl_cv_host_cpu_c_abi=arm + fi + rm -f conftest* + ]) + ;; + + hppa1.0 | hppa1.1 | hppa2.0* | hppa64 ) + # On hppa, the C compiler may be generating 32-bit code or 64-bit + # code. In the latter case, it defines _LP64 and __LP64__. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef __LP64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=hppa64], + [gl_cv_host_cpu_c_abi=hppa]) + ;; + + ia64* ) + # On ia64 on HP-UX, the C compiler may be generating 64-bit code or + # 32-bit code. In the latter case, it defines _ILP32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef _ILP32 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=ia64-ilp32], + [gl_cv_host_cpu_c_abi=ia64]) + ;; + + mips* ) + # We should also check for (_MIPS_SZPTR == 64), but gcc keeps this + # at 32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _MIPS_SZLONG && (_MIPS_SZLONG == 64) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mips64], + [# In the n32 ABI, _ABIN32 is defined, _ABIO32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIN32. + # In the 32 ABI, _ABIO32 is defined, _ABIN32 is not defined (but + # may later get defined by ), and _MIPS_SIM == _ABIO32. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if (_MIPS_SIM == _ABIN32) + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=mipsn32], + [gl_cv_host_cpu_c_abi=mips])]) + ;; + + powerpc* ) + # Different ABIs are in use on AIX vs. Mac OS X vs. Linux,*BSD. + # No need to distinguish them here; the caller may distinguish + # them based on the OS. + # On powerpc64 systems, the C compiler may still be generating + # 32-bit code. And on powerpc-ibm-aix systems, the C compiler may + # be generating 64-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __powerpc64__ || defined _ARCH_PPC64 + int ok; + #else + error fail + #endif + ]])], + [# On powerpc64, there are two ABIs on Linux: The AIX compatible + # one and the ELFv2 one. The latter defines _CALL_ELF=2. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined _CALL_ELF && _CALL_ELF == 2 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=powerpc64-elfv2], + [gl_cv_host_cpu_c_abi=powerpc64]) + ], + [gl_cv_host_cpu_c_abi=powerpc]) + ;; + + rs6000 ) + gl_cv_host_cpu_c_abi=powerpc + ;; + + riscv32 | riscv64 ) + # There are 2 architectures (with variants): rv32* and rv64*. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if __riscv_xlen == 64 + int ok; + #else + error fail + #endif + ]])], + [cpu=riscv64], + [cpu=riscv32]) + # There are 6 ABIs: ilp32, ilp32f, ilp32d, lp64, lp64f, lp64d. + # Size of 'long' and 'void *': + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ + int ok; + #else + error fail + #endif + ]])], + [main_abi=lp64], + [main_abi=ilp32]) + # Float ABIs: + # __riscv_float_abi_double: + # 'float' and 'double' are passed in floating-point registers. + # __riscv_float_abi_single: + # 'float' are passed in floating-point registers. + # __riscv_float_abi_soft: + # No values are passed in floating-point registers. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_double + int ok; + #else + error fail + #endif + ]])], + [float_abi=d], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __riscv_float_abi_single + int ok; + #else + error fail + #endif + ]])], + [float_abi=f], + [float_abi='']) + ]) + gl_cv_host_cpu_c_abi="${cpu}-${main_abi}${float_abi}" + ;; + + s390* ) + # On s390x, the C compiler may be generating 64-bit (= s390x) code + # or 31-bit (= s390) code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __LP64__ || defined __s390x__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=s390x], + [gl_cv_host_cpu_c_abi=s390]) + ;; + + sparc | sparc64 ) + # UltraSPARCs running Linux have `uname -m` = "sparc64", but the + # C compiler still generates 32-bit code. + AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#if defined __sparcv9 || defined __arch64__ + int ok; + #else + error fail + #endif + ]])], + [gl_cv_host_cpu_c_abi=sparc64], + [gl_cv_host_cpu_c_abi=sparc]) + ;; + + *) + gl_cv_host_cpu_c_abi="$host_cpu" + ;; + esac + ]) + + dnl In most cases, $HOST_CPU and $HOST_CPU_C_ABI are the same. + HOST_CPU=`echo "$gl_cv_host_cpu_c_abi" | sed -e 's/-.*//'` + HOST_CPU_C_ABI="$gl_cv_host_cpu_c_abi" + AC_SUBST([HOST_CPU]) + AC_SUBST([HOST_CPU_C_ABI]) + + # This was + # AC_DEFINE_UNQUOTED([__${HOST_CPU}__]) + # AC_DEFINE_UNQUOTED([__${HOST_CPU_C_ABI}__]) + # earlier, but KAI C++ 3.2d doesn't like this. + sed -e 's/-/_/g' >> confdefs.h <&1 /dev/null 2>&1 \ + && { (PATH='/bin:/bin'; FPATH=$PATH; sh -c :) >/dev/null 2>&1 \ + || PATH_SEPARATOR=';' + } +fi + +if test -n "$LD"; then + AC_MSG_CHECKING([for ld]) +elif test "$GCC" = yes; then + AC_MSG_CHECKING([for ld used by $CC]) +elif test "$with_gnu_ld" = yes; then + AC_MSG_CHECKING([for GNU ld]) +else + AC_MSG_CHECKING([for non-GNU ld]) +fi +if test -n "$LD"; then + # Let the user override the test with a path. + : +else + AC_CACHE_VAL([acl_cv_path_LD], + [ + acl_cv_path_LD= # Final result of this test + ac_prog=ld # Program to search in $PATH + if test "$GCC" = yes; then + # Check if gcc -print-prog-name=ld gives a path. + case $host in + *-*-mingw*) + # gcc leaves a trailing carriage return which upsets mingw + acl_output=`($CC -print-prog-name=ld) 2>&5 | tr -d '\015'` ;; + *) + acl_output=`($CC -print-prog-name=ld) 2>&5` ;; + esac + case $acl_output in + # Accept absolute paths. + [[\\/]]* | ?:[[\\/]]*) + re_direlt='/[[^/]][[^/]]*/\.\./' + # Canonicalize the pathname of ld + acl_output=`echo "$acl_output" | sed 's%\\\\%/%g'` + while echo "$acl_output" | grep "$re_direlt" > /dev/null 2>&1; do + acl_output=`echo $acl_output | sed "s%$re_direlt%/%"` + done + # Got the pathname. No search in PATH is needed. + acl_cv_path_LD="$acl_output" + ac_prog= + ;; + "") + # If it fails, then pretend we aren't using GCC. + ;; + *) + # If it is relative, then search for the first ld in PATH. + with_gnu_ld=unknown + ;; + esac + fi + if test -n "$ac_prog"; then + # Search for $ac_prog in $PATH. + acl_save_ifs="$IFS"; IFS=$PATH_SEPARATOR + for ac_dir in $PATH; do + IFS="$acl_save_ifs" + test -z "$ac_dir" && ac_dir=. + if test -f "$ac_dir/$ac_prog" || test -f "$ac_dir/$ac_prog$ac_exeext"; then + acl_cv_path_LD="$ac_dir/$ac_prog" + # Check to see if the program is GNU ld. I'd rather use --version, + # but apparently some variants of GNU ld only accept -v. + # Break only if it was the GNU/non-GNU ld that we prefer. + case `"$acl_cv_path_LD" -v 2>&1 conftest.sh + . ./conftest.sh + rm -f ./conftest.sh + acl_cv_rpath=done + ]) + wl="$acl_cv_wl" + acl_libext="$acl_cv_libext" + acl_shlibext="$acl_cv_shlibext" + acl_libname_spec="$acl_cv_libname_spec" + acl_library_names_spec="$acl_cv_library_names_spec" + acl_hardcode_libdir_flag_spec="$acl_cv_hardcode_libdir_flag_spec" + acl_hardcode_libdir_separator="$acl_cv_hardcode_libdir_separator" + acl_hardcode_direct="$acl_cv_hardcode_direct" + acl_hardcode_minus_L="$acl_cv_hardcode_minus_L" + dnl Determine whether the user wants rpath handling at all. + AC_ARG_ENABLE([rpath], + [ --disable-rpath do not hardcode runtime library paths], + :, enable_rpath=yes) +]) + +dnl AC_LIB_FROMPACKAGE(name, package) +dnl declares that libname comes from the given package. The configure file +dnl will then not have a --with-libname-prefix option but a +dnl --with-package-prefix option. Several libraries can come from the same +dnl package. This declaration must occur before an AC_LIB_LINKFLAGS or similar +dnl macro call that searches for libname. +AC_DEFUN([AC_LIB_FROMPACKAGE], +[ + pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + define([acl_frompackage_]NAME, [$2]) + popdef([NAME]) + pushdef([PACK],[$2]) + pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + define([acl_libsinpackage_]PACKUP, + m4_ifdef([acl_libsinpackage_]PACKUP, [m4_defn([acl_libsinpackage_]PACKUP)[, ]],)[lib$1]) + popdef([PACKUP]) + popdef([PACK]) +]) + +dnl AC_LIB_LINKFLAGS_BODY(name [, dependencies]) searches for libname and +dnl the libraries corresponding to explicit and implicit dependencies. +dnl Sets the LIB${NAME}, LTLIB${NAME} and INC${NAME} variables. +dnl Also, sets the LIB${NAME}_PREFIX variable to nonempty if libname was found +dnl in ${LIB${NAME}_PREFIX}/$acl_libdirstem. +AC_DEFUN([AC_LIB_LINKFLAGS_BODY], +[ + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + pushdef([NAME],[m4_translit([$1],[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + pushdef([PACK],[m4_ifdef([acl_frompackage_]NAME, [acl_frompackage_]NAME, lib[$1])]) + pushdef([PACKUP],[m4_translit(PACK,[abcdefghijklmnopqrstuvwxyz./+-], + [ABCDEFGHIJKLMNOPQRSTUVWXYZ____])]) + pushdef([PACKLIBS],[m4_ifdef([acl_frompackage_]NAME, [acl_libsinpackage_]PACKUP, lib[$1])]) + dnl By default, look in $includedir and $libdir. + use_additional=yes + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + AC_ARG_WITH(PACK[-prefix], +[[ --with-]]PACK[[-prefix[=DIR] search for ]PACKLIBS[ in DIR/include and DIR/lib + --without-]]PACK[[-prefix don't search for ]PACKLIBS[ in includedir and libdir]], +[ + if test "X$withval" = "Xno"; then + use_additional=no + else + if test "X$withval" = "X"; then + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + else + additional_includedir="$withval/include" + additional_libdir="$withval/$acl_libdirstem" + if test "$acl_libdirstem2" != "$acl_libdirstem" \ + && test ! -d "$withval/$acl_libdirstem"; then + additional_libdir="$withval/$acl_libdirstem2" + fi + fi + fi +]) + dnl Search the library and its dependencies in $additional_libdir and + dnl $LDFLAGS. Using breadth-first-seach. + LIB[]NAME= + LTLIB[]NAME= + INC[]NAME= + LIB[]NAME[]_PREFIX= + dnl HAVE_LIB${NAME} is an indicator that LIB${NAME}, LTLIB${NAME} have been + dnl computed. So it has to be reset here. + HAVE_LIB[]NAME= + rpathdirs= + ltrpathdirs= + names_already_handled= + names_next_round='$1 $2' + while test -n "$names_next_round"; do + names_this_round="$names_next_round" + names_next_round= + for name in $names_this_round; do + already_handled= + for n in $names_already_handled; do + if test "$n" = "$name"; then + already_handled=yes + break + fi + done + if test -z "$already_handled"; then + names_already_handled="$names_already_handled $name" + dnl See if it was already located by an earlier AC_LIB_LINKFLAGS + dnl or AC_LIB_HAVE_LINKFLAGS call. + uppername=`echo "$name" | sed -e 'y|abcdefghijklmnopqrstuvwxyz./+-|ABCDEFGHIJKLMNOPQRSTUVWXYZ____|'` + eval value=\"\$HAVE_LIB$uppername\" + if test -n "$value"; then + if test "$value" = yes; then + eval value=\"\$LIB$uppername\" + test -z "$value" || LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$value" + eval value=\"\$LTLIB$uppername\" + test -z "$value" || LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$value" + else + dnl An earlier call to AC_LIB_HAVE_LINKFLAGS has determined + dnl that this library doesn't exist. So just drop it. + : + fi + else + dnl Search the library lib$name in $additional_libdir and $LDFLAGS + dnl and the already constructed $LIBNAME/$LTLIBNAME. + found_dir= + found_la= + found_so= + found_a= + eval libname=\"$acl_libname_spec\" # typically: libname=lib$name + if test -n "$acl_shlibext"; then + shrext=".$acl_shlibext" # typically: shrext=.so + else + shrext= + fi + if test $use_additional = yes; then + dir="$additional_libdir" + dnl The same code as in the loop below: + dnl First look for a shared library. + if test -n "$acl_shlibext"; then + if test -f "$dir/$libname$shrext"; then + found_dir="$dir" + found_so="$dir/$libname$shrext" + else + if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then + ver=`(cd "$dir" && \ + for f in "$libname$shrext".*; do echo "$f"; done \ + | sed -e "s,^$libname$shrext\\\\.,," \ + | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \ + | sed 1q ) 2>/dev/null` + if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then + found_dir="$dir" + found_so="$dir/$libname$shrext.$ver" + fi + else + eval library_names=\"$acl_library_names_spec\" + for f in $library_names; do + if test -f "$dir/$f"; then + found_dir="$dir" + found_so="$dir/$f" + break + fi + done + fi + fi + fi + dnl Then look for a static library. + if test "X$found_dir" = "X"; then + if test -f "$dir/$libname.$acl_libext"; then + found_dir="$dir" + found_a="$dir/$libname.$acl_libext" + fi + fi + if test "X$found_dir" != "X"; then + if test -f "$dir/$libname.la"; then + found_la="$dir/$libname.la" + fi + fi + fi + if test "X$found_dir" = "X"; then + for x in $LDFLAGS $LTLIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + case "$x" in + -L*) + dir=`echo "X$x" | sed -e 's/^X-L//'` + dnl First look for a shared library. + if test -n "$acl_shlibext"; then + if test -f "$dir/$libname$shrext"; then + found_dir="$dir" + found_so="$dir/$libname$shrext" + else + if test "$acl_library_names_spec" = '$libname$shrext$versuffix'; then + ver=`(cd "$dir" && \ + for f in "$libname$shrext".*; do echo "$f"; done \ + | sed -e "s,^$libname$shrext\\\\.,," \ + | sort -t '.' -n -r -k1,1 -k2,2 -k3,3 -k4,4 -k5,5 \ + | sed 1q ) 2>/dev/null` + if test -n "$ver" && test -f "$dir/$libname$shrext.$ver"; then + found_dir="$dir" + found_so="$dir/$libname$shrext.$ver" + fi + else + eval library_names=\"$acl_library_names_spec\" + for f in $library_names; do + if test -f "$dir/$f"; then + found_dir="$dir" + found_so="$dir/$f" + break + fi + done + fi + fi + fi + dnl Then look for a static library. + if test "X$found_dir" = "X"; then + if test -f "$dir/$libname.$acl_libext"; then + found_dir="$dir" + found_a="$dir/$libname.$acl_libext" + fi + fi + if test "X$found_dir" != "X"; then + if test -f "$dir/$libname.la"; then + found_la="$dir/$libname.la" + fi + fi + ;; + esac + if test "X$found_dir" != "X"; then + break + fi + done + fi + if test "X$found_dir" != "X"; then + dnl Found the library. + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$found_dir -l$name" + if test "X$found_so" != "X"; then + dnl Linking with a shared library. We attempt to hardcode its + dnl directory into the executable's runpath, unless it's the + dnl standard /usr/lib. + if test "$enable_rpath" = no \ + || test "X$found_dir" = "X/usr/$acl_libdirstem" \ + || test "X$found_dir" = "X/usr/$acl_libdirstem2"; then + dnl No hardcoding is needed. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + dnl Use an explicit option to hardcode DIR into the resulting + dnl binary. + dnl Potentially add DIR to ltrpathdirs. + dnl The ltrpathdirs will be appended to $LTLIBNAME at the end. + haveit= + for x in $ltrpathdirs; do + if test "X$x" = "X$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + ltrpathdirs="$ltrpathdirs $found_dir" + fi + dnl The hardcoding into $LIBNAME is system dependent. + if test "$acl_hardcode_direct" = yes; then + dnl Using DIR/libNAME.so during linking hardcodes DIR into the + dnl resulting binary. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then + dnl Use an explicit option to hardcode DIR into the resulting + dnl binary. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + dnl Potentially add DIR to rpathdirs. + dnl The rpathdirs will be appended to $LIBNAME at the end. + haveit= + for x in $rpathdirs; do + if test "X$x" = "X$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + rpathdirs="$rpathdirs $found_dir" + fi + else + dnl Rely on "-L$found_dir". + dnl But don't add it if it's already contained in the LDFLAGS + dnl or the already constructed $LIBNAME + haveit= + for x in $LDFLAGS $LIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$found_dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir" + fi + if test "$acl_hardcode_minus_L" != no; then + dnl FIXME: Not sure whether we should use + dnl "-L$found_dir -l$name" or "-L$found_dir $found_so" + dnl here. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_so" + else + dnl We cannot use $acl_hardcode_runpath_var and LD_RUN_PATH + dnl here, because this doesn't fit in flags passed to the + dnl compiler. So give up. No hardcoding. This affects only + dnl very old systems. + dnl FIXME: Not sure whether we should use + dnl "-L$found_dir -l$name" or "-L$found_dir $found_so" + dnl here. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name" + fi + fi + fi + fi + else + if test "X$found_a" != "X"; then + dnl Linking with a static library. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$found_a" + else + dnl We shouldn't come here, but anyway it's good to have a + dnl fallback. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$found_dir -l$name" + fi + fi + dnl Assume the include files are nearby. + additional_includedir= + case "$found_dir" in + */$acl_libdirstem | */$acl_libdirstem/) + basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem/"'*$,,'` + if test "$name" = '$1'; then + LIB[]NAME[]_PREFIX="$basedir" + fi + additional_includedir="$basedir/include" + ;; + */$acl_libdirstem2 | */$acl_libdirstem2/) + basedir=`echo "X$found_dir" | sed -e 's,^X,,' -e "s,/$acl_libdirstem2/"'*$,,'` + if test "$name" = '$1'; then + LIB[]NAME[]_PREFIX="$basedir" + fi + additional_includedir="$basedir/include" + ;; + esac + if test "X$additional_includedir" != "X"; then + dnl Potentially add $additional_includedir to $INCNAME. + dnl But don't add it + dnl 1. if it's the standard /usr/include, + dnl 2. if it's /usr/local/include and we are using GCC on Linux, + dnl 3. if it's already present in $CPPFLAGS or the already + dnl constructed $INCNAME, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_includedir" != "X/usr/include"; then + haveit= + if test "X$additional_includedir" = "X/usr/local/include"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + for x in $CPPFLAGS $INC[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-I$additional_includedir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_includedir"; then + dnl Really add $additional_includedir to $INCNAME. + INC[]NAME="${INC[]NAME}${INC[]NAME:+ }-I$additional_includedir" + fi + fi + fi + fi + fi + dnl Look for dependencies. + if test -n "$found_la"; then + dnl Read the .la file. It defines the variables + dnl dlname, library_names, old_library, dependency_libs, current, + dnl age, revision, installed, dlopen, dlpreopen, libdir. + save_libdir="$libdir" + case "$found_la" in + */* | *\\*) . "$found_la" ;; + *) . "./$found_la" ;; + esac + libdir="$save_libdir" + dnl We use only dependency_libs. + for dep in $dependency_libs; do + case "$dep" in + -L*) + additional_libdir=`echo "X$dep" | sed -e 's/^X-L//'` + dnl Potentially add $additional_libdir to $LIBNAME and $LTLIBNAME. + dnl But don't add it + dnl 1. if it's the standard /usr/lib, + dnl 2. if it's /usr/local/lib and we are using GCC on Linux, + dnl 3. if it's already present in $LDFLAGS or the already + dnl constructed $LIBNAME, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_libdir" != "X/usr/$acl_libdirstem" \ + && test "X$additional_libdir" != "X/usr/$acl_libdirstem2"; then + haveit= + if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem" \ + || test "X$additional_libdir" = "X/usr/local/$acl_libdirstem2"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + haveit= + for x in $LDFLAGS $LIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LIBNAME. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-L$additional_libdir" + fi + fi + haveit= + for x in $LDFLAGS $LTLIB[]NAME; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LTLIBNAME. + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-L$additional_libdir" + fi + fi + fi + fi + ;; + -R*) + dir=`echo "X$dep" | sed -e 's/^X-R//'` + if test "$enable_rpath" != no; then + dnl Potentially add DIR to rpathdirs. + dnl The rpathdirs will be appended to $LIBNAME at the end. + haveit= + for x in $rpathdirs; do + if test "X$x" = "X$dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + rpathdirs="$rpathdirs $dir" + fi + dnl Potentially add DIR to ltrpathdirs. + dnl The ltrpathdirs will be appended to $LTLIBNAME at the end. + haveit= + for x in $ltrpathdirs; do + if test "X$x" = "X$dir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + ltrpathdirs="$ltrpathdirs $dir" + fi + fi + ;; + -l*) + dnl Handle this in the next round. + names_next_round="$names_next_round "`echo "X$dep" | sed -e 's/^X-l//'` + ;; + *.la) + dnl Handle this in the next round. Throw away the .la's + dnl directory; it is already contained in a preceding -L + dnl option. + names_next_round="$names_next_round "`echo "X$dep" | sed -e 's,^X.*/,,' -e 's,^lib,,' -e 's,\.la$,,'` + ;; + *) + dnl Most likely an immediate library name. + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$dep" + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }$dep" + ;; + esac + done + fi + else + dnl Didn't find the library; assume it is in the system directories + dnl known to the linker and runtime loader. (All the system + dnl directories known to the linker should also be known to the + dnl runtime loader, otherwise the system is severely misconfigured.) + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }-l$name" + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-l$name" + fi + fi + fi + done + done + if test "X$rpathdirs" != "X"; then + if test -n "$acl_hardcode_libdir_separator"; then + dnl Weird platform: only the last -rpath option counts, the user must + dnl pass all path elements in one option. We can arrange that for a + dnl single library, but not when more than one $LIBNAMEs are used. + alldirs= + for found_dir in $rpathdirs; do + alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$found_dir" + done + dnl Note: acl_hardcode_libdir_flag_spec uses $libdir and $wl. + acl_save_libdir="$libdir" + libdir="$alldirs" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag" + else + dnl The -rpath options are cumulative. + for found_dir in $rpathdirs; do + acl_save_libdir="$libdir" + libdir="$found_dir" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + LIB[]NAME="${LIB[]NAME}${LIB[]NAME:+ }$flag" + done + fi + fi + if test "X$ltrpathdirs" != "X"; then + dnl When using libtool, the option that works for both libraries and + dnl executables is -R. The -R options are cumulative. + for found_dir in $ltrpathdirs; do + LTLIB[]NAME="${LTLIB[]NAME}${LTLIB[]NAME:+ }-R$found_dir" + done + fi + popdef([PACKLIBS]) + popdef([PACKUP]) + popdef([PACK]) + popdef([NAME]) +]) + +dnl AC_LIB_APPENDTOVAR(VAR, CONTENTS) appends the elements of CONTENTS to VAR, +dnl unless already present in VAR. +dnl Works only for CPPFLAGS, not for LIB* variables because that sometimes +dnl contains two or three consecutive elements that belong together. +AC_DEFUN([AC_LIB_APPENDTOVAR], +[ + for element in [$2]; do + haveit= + for x in $[$1]; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X$element"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + [$1]="${[$1]}${[$1]:+ }$element" + fi + done +]) + +dnl For those cases where a variable contains several -L and -l options +dnl referring to unknown libraries and directories, this macro determines the +dnl necessary additional linker options for the runtime path. +dnl AC_LIB_LINKFLAGS_FROM_LIBS([LDADDVAR], [LIBSVALUE], [USE-LIBTOOL]) +dnl sets LDADDVAR to linker options needed together with LIBSVALUE. +dnl If USE-LIBTOOL evaluates to non-empty, linking with libtool is assumed, +dnl otherwise linking without libtool is assumed. +AC_DEFUN([AC_LIB_LINKFLAGS_FROM_LIBS], +[ + AC_REQUIRE([AC_LIB_RPATH]) + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + $1= + if test "$enable_rpath" != no; then + if test -n "$acl_hardcode_libdir_flag_spec" && test "$acl_hardcode_minus_L" = no; then + dnl Use an explicit option to hardcode directories into the resulting + dnl binary. + rpathdirs= + next= + for opt in $2; do + if test -n "$next"; then + dir="$next" + dnl No need to hardcode the standard /usr/lib. + if test "X$dir" != "X/usr/$acl_libdirstem" \ + && test "X$dir" != "X/usr/$acl_libdirstem2"; then + rpathdirs="$rpathdirs $dir" + fi + next= + else + case $opt in + -L) next=yes ;; + -L*) dir=`echo "X$opt" | sed -e 's,^X-L,,'` + dnl No need to hardcode the standard /usr/lib. + if test "X$dir" != "X/usr/$acl_libdirstem" \ + && test "X$dir" != "X/usr/$acl_libdirstem2"; then + rpathdirs="$rpathdirs $dir" + fi + next= ;; + *) next= ;; + esac + fi + done + if test "X$rpathdirs" != "X"; then + if test -n ""$3""; then + dnl libtool is used for linking. Use -R options. + for dir in $rpathdirs; do + $1="${$1}${$1:+ }-R$dir" + done + else + dnl The linker is used for linking directly. + if test -n "$acl_hardcode_libdir_separator"; then + dnl Weird platform: only the last -rpath option counts, the user + dnl must pass all path elements in one option. + alldirs= + for dir in $rpathdirs; do + alldirs="${alldirs}${alldirs:+$acl_hardcode_libdir_separator}$dir" + done + acl_save_libdir="$libdir" + libdir="$alldirs" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + $1="$flag" + else + dnl The -rpath options are cumulative. + for dir in $rpathdirs; do + acl_save_libdir="$libdir" + libdir="$dir" + eval flag=\"$acl_hardcode_libdir_flag_spec\" + libdir="$acl_save_libdir" + $1="${$1}${$1:+ }$flag" + done + fi + fi + fi + fi + fi + AC_SUBST([$1]) +]) diff --git a/m4/lib-prefix.m4 b/m4/lib-prefix.m4 new file mode 100644 index 0000000..8adb17b --- /dev/null +++ b/m4/lib-prefix.m4 @@ -0,0 +1,249 @@ +# lib-prefix.m4 serial 14 +dnl Copyright (C) 2001-2005, 2008-2019 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Bruno Haible. + +dnl AC_LIB_PREFIX adds to the CPPFLAGS and LDFLAGS the flags that are needed +dnl to access previously installed libraries. The basic assumption is that +dnl a user will want packages to use other packages he previously installed +dnl with the same --prefix option. +dnl This macro is not needed if only AC_LIB_LINKFLAGS is used to locate +dnl libraries, but is otherwise very convenient. +AC_DEFUN([AC_LIB_PREFIX], +[ + AC_BEFORE([$0], [AC_LIB_LINKFLAGS]) + AC_REQUIRE([AC_PROG_CC]) + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([AC_LIB_PREPARE_MULTILIB]) + AC_REQUIRE([AC_LIB_PREPARE_PREFIX]) + dnl By default, look in $includedir and $libdir. + use_additional=yes + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + AC_ARG_WITH([lib-prefix], +[[ --with-lib-prefix[=DIR] search for libraries in DIR/include and DIR/lib + --without-lib-prefix don't search for libraries in includedir and libdir]], +[ + if test "X$withval" = "Xno"; then + use_additional=no + else + if test "X$withval" = "X"; then + AC_LIB_WITH_FINAL_PREFIX([ + eval additional_includedir=\"$includedir\" + eval additional_libdir=\"$libdir\" + ]) + else + additional_includedir="$withval/include" + additional_libdir="$withval/$acl_libdirstem" + fi + fi +]) + if test $use_additional = yes; then + dnl Potentially add $additional_includedir to $CPPFLAGS. + dnl But don't add it + dnl 1. if it's the standard /usr/include, + dnl 2. if it's already present in $CPPFLAGS, + dnl 3. if it's /usr/local/include and we are using GCC on Linux, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_includedir" != "X/usr/include"; then + haveit= + for x in $CPPFLAGS; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-I$additional_includedir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test "X$additional_includedir" = "X/usr/local/include"; then + if test -n "$GCC"; then + case $host_os in + linux* | gnu* | k*bsd*-gnu) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + if test -d "$additional_includedir"; then + dnl Really add $additional_includedir to $CPPFLAGS. + CPPFLAGS="${CPPFLAGS}${CPPFLAGS:+ }-I$additional_includedir" + fi + fi + fi + fi + dnl Potentially add $additional_libdir to $LDFLAGS. + dnl But don't add it + dnl 1. if it's the standard /usr/lib, + dnl 2. if it's already present in $LDFLAGS, + dnl 3. if it's /usr/local/lib and we are using GCC on Linux, + dnl 4. if it doesn't exist as a directory. + if test "X$additional_libdir" != "X/usr/$acl_libdirstem"; then + haveit= + for x in $LDFLAGS; do + AC_LIB_WITH_FINAL_PREFIX([eval x=\"$x\"]) + if test "X$x" = "X-L$additional_libdir"; then + haveit=yes + break + fi + done + if test -z "$haveit"; then + if test "X$additional_libdir" = "X/usr/local/$acl_libdirstem"; then + if test -n "$GCC"; then + case $host_os in + linux*) haveit=yes;; + esac + fi + fi + if test -z "$haveit"; then + if test -d "$additional_libdir"; then + dnl Really add $additional_libdir to $LDFLAGS. + LDFLAGS="${LDFLAGS}${LDFLAGS:+ }-L$additional_libdir" + fi + fi + fi + fi + fi +]) + +dnl AC_LIB_PREPARE_PREFIX creates variables acl_final_prefix, +dnl acl_final_exec_prefix, containing the values to which $prefix and +dnl $exec_prefix will expand at the end of the configure script. +AC_DEFUN([AC_LIB_PREPARE_PREFIX], +[ + dnl Unfortunately, prefix and exec_prefix get only finally determined + dnl at the end of configure. + if test "X$prefix" = "XNONE"; then + acl_final_prefix="$ac_default_prefix" + else + acl_final_prefix="$prefix" + fi + if test "X$exec_prefix" = "XNONE"; then + acl_final_exec_prefix='${prefix}' + else + acl_final_exec_prefix="$exec_prefix" + fi + acl_save_prefix="$prefix" + prefix="$acl_final_prefix" + eval acl_final_exec_prefix=\"$acl_final_exec_prefix\" + prefix="$acl_save_prefix" +]) + +dnl AC_LIB_WITH_FINAL_PREFIX([statement]) evaluates statement, with the +dnl variables prefix and exec_prefix bound to the values they will have +dnl at the end of the configure script. +AC_DEFUN([AC_LIB_WITH_FINAL_PREFIX], +[ + acl_save_prefix="$prefix" + prefix="$acl_final_prefix" + acl_save_exec_prefix="$exec_prefix" + exec_prefix="$acl_final_exec_prefix" + $1 + exec_prefix="$acl_save_exec_prefix" + prefix="$acl_save_prefix" +]) + +dnl AC_LIB_PREPARE_MULTILIB creates +dnl - a variable acl_libdirstem, containing the basename of the libdir, either +dnl "lib" or "lib64" or "lib/64", +dnl - a variable acl_libdirstem2, as a secondary possible value for +dnl acl_libdirstem, either the same as acl_libdirstem or "lib/sparcv9" or +dnl "lib/amd64". +AC_DEFUN([AC_LIB_PREPARE_MULTILIB], +[ + dnl There is no formal standard regarding lib and lib64. + dnl On glibc systems, the current practice is that on a system supporting + dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under + dnl $prefix/lib64 and 32-bit libraries go under $prefix/lib. We determine + dnl the compiler's default mode by looking at the compiler's library search + dnl path. If at least one of its elements ends in /lib64 or points to a + dnl directory whose absolute pathname ends in /lib64, we assume a 64-bit ABI. + dnl Otherwise we use the default, namely "lib". + dnl On Solaris systems, the current practice is that on a system supporting + dnl 32-bit and 64-bit instruction sets or ABIs, 64-bit libraries go under + dnl $prefix/lib/64 (which is a symlink to either $prefix/lib/sparcv9 or + dnl $prefix/lib/amd64) and 32-bit libraries go under $prefix/lib. + AC_REQUIRE([AC_CANONICAL_HOST]) + AC_REQUIRE([gl_HOST_CPU_C_ABI_32BIT]) + + case "$host_os" in + solaris*) + AC_CACHE_CHECK([for 64-bit host], [gl_cv_solaris_64bit], + [AC_COMPILE_IFELSE( + [AC_LANG_SOURCE( + [[#ifdef _LP64 + int ok; + #else + error fail + #endif + ]])], + [gl_cv_solaris_64bit=yes], + [gl_cv_solaris_64bit=no]) + ]);; + esac + + dnl Allow the user to override the result by setting acl_cv_libdirstems. + AC_CACHE_CHECK([for the common suffixes of directories in the library search path], + [acl_cv_libdirstems], + [acl_libdirstem=lib + acl_libdirstem2= + case "$host_os" in + solaris*) + dnl See Solaris 10 Software Developer Collection > Solaris 64-bit Developer's Guide > The Development Environment + dnl . + dnl "Portable Makefiles should refer to any library directories using the 64 symbolic link." + dnl But we want to recognize the sparcv9 or amd64 subdirectory also if the + dnl symlink is missing, so we set acl_libdirstem2 too. + if test $gl_cv_solaris_64bit = yes; then + acl_libdirstem=lib/64 + case "$host_cpu" in + sparc*) acl_libdirstem2=lib/sparcv9 ;; + i*86 | x86_64) acl_libdirstem2=lib/amd64 ;; + esac + fi + ;; + *) + dnl If $CC generates code for a 32-bit ABI, the libraries are + dnl surely under $prefix/lib, not $prefix/lib64. + if test "$HOST_CPU_C_ABI_32BIT" != yes; then + dnl The result is a property of the system. However, non-system + dnl compilers sometimes have odd library search paths. Therefore + dnl prefer asking /usr/bin/gcc, if available, rather than $CC. + searchpath=`(if test -f /usr/bin/gcc \ + && LC_ALL=C /usr/bin/gcc -print-search-dirs >/dev/null 2>/dev/null; then \ + LC_ALL=C /usr/bin/gcc -print-search-dirs; \ + else \ + LC_ALL=C $CC -print-search-dirs; \ + fi) 2>/dev/null \ + | sed -n -e 's,^libraries: ,,p' | sed -e 's,^=,,'` + if test -n "$searchpath"; then + acl_save_IFS="${IFS= }"; IFS=":" + for searchdir in $searchpath; do + if test -d "$searchdir"; then + case "$searchdir" in + */lib64/ | */lib64 ) acl_libdirstem=lib64 ;; + */../ | */.. ) + # Better ignore directories of this form. They are misleading. + ;; + *) searchdir=`cd "$searchdir" && pwd` + case "$searchdir" in + */lib64 ) acl_libdirstem=lib64 ;; + esac ;; + esac + fi + done + IFS="$acl_save_IFS" + fi + fi + ;; + esac + test -n "$acl_libdirstem2" || acl_libdirstem2="$acl_libdirstem" + acl_cv_libdirstems="$acl_libdirstem,$acl_libdirstem2" + ]) + # Decompose acl_cv_libdirstems into acl_libdirstem and acl_libdirstem2. + acl_libdirstem=`echo "$acl_cv_libdirstems" | sed -e 's/,.*//'` + acl_libdirstem2=`echo "$acl_cv_libdirstems" | sed -e '/,/s/.*,//'` +]) diff --git a/man/Makefile.am b/man/Makefile.am new file mode 100644 index 0000000..e055600 --- /dev/null +++ b/man/Makefile.am @@ -0,0 +1,37 @@ +## Copyright (C) 2008-2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +dist_man_MANS = \ + mu-add.1 \ + mu-bookmarks.5 \ + mu-cfind.1 \ + mu-easy.1 \ + mu-extract.1 \ + mu-find.1 \ + mu-help.1 \ + mu-index.1 \ + mu-info.1 \ + mu-init.1 \ + mu-mkdir.1 \ + mu-query.7 \ + mu-remove.1 \ + mu-server.1 \ + mu-script.1 \ + mu-verify.1 \ + mu-view.1 \ + mu.1 diff --git a/man/mu-add.1 b/man/mu-add.1 new file mode 100644 index 0000000..fb4e081 --- /dev/null +++ b/man/mu-add.1 @@ -0,0 +1,48 @@ +.TH MU ADD 1 "July 2012" "User Manuals" + +.SH NAME + +mu add\- add one or more messages to the database + +.SH SYNOPSIS + +.B mu add [] + +.SH DESCRIPTION + +\fBmu add\fR is the command to add specific message files to the +database. Each file must be specified with an absolute path. + +.SH OPTIONS + +\fBmu add\fR does not have its own options, but the general options for +determining the location of the database (\fI--muhome\fR) are available. See +\fBmu-index\fR(1) for more information. + +.SH RETURN VALUE + +\fBmu add\fR returns 0 upon success; in general, the following error codes are +returned: + +.nf +| code | meaning | +|------+-----------------------------------| +| 0 | ok | +| 1 | general error | +| 5 | some database update error | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-remove (1) diff --git a/man/mu-bookmarks.5 b/man/mu-bookmarks.5 new file mode 100644 index 0000000..f68a335 --- /dev/null +++ b/man/mu-bookmarks.5 @@ -0,0 +1,36 @@ +.TH MU-BOOKMARKS 5 "July 2019" "User Manuals" + +.SH NAME + +bookmarks \- file with bookmarks (shortcuts) for mu search expressions + +.SH DESCRIPTION + +Bookmarks are named shortcuts for search queries. They allow using a convenient +name for often-used queries. The bookmarks are also visible as shortcuts in the +mu experimental user interfaces, \fImug\fR and \fImug2\fR. + +The bookmarks file is read from \fI/bookmarks\fR. On Unix this would +typically be w be \fI~/.config/mu/bookmarks\fR, but this can be influenced using +the \fB\-\-muhome\fR parameter for \fBmu-find\fR(1) and \fBmug\fR(1). + +The bookmarks file is a typical key=value \fB.ini\fR-file, which is best shown +by means of an example: + +.nf + [mu] + inbox=maildir:/inbox # inbox + oldhat=maildir:/archive subject:hat # archived with subject containing 'hat' +.fi + +The \fB[mu]\fR group header is required. + +For practical uses of bookmarks, see \fBmu-find\fR(1). + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), mu-find (1) diff --git a/man/mu-cfind.1 b/man/mu-cfind.1 new file mode 100644 index 0000000..a3618e1 --- /dev/null +++ b/man/mu-cfind.1 @@ -0,0 +1,133 @@ +.TH MU CFIND 1 "April 2019" "User Manuals" + +.SH NAME + +\fBmu cfind\fR is the \fBmu\fR command to find contacts in the \fBmu\fR +database and export them for use in other programs. + +.SH SYNOPSIS + +.B mu cfind [options] [] + +.SH DESCRIPTION + +\fBmu cfind\fR is the \fBmu\fR command for finding \fIcontacts\fR (name and +e-mail address of people who were either an e-mail's sender or +receiver). There are different output formats available, for importing the +contacts into other programs. + +.SH SEARCHING CONTACTS + +When you index your messages (see \fBmu index\fR), \fBmu\fR creates a list of +unique e-mail addresses found and the accompanying name, and caches this +list. In case the same e-mail address is used with different names, the most +recent non-empty name is used. + +\fBmu cfind\fR starts a search for contacts that match a \fIregular +expression\fR. For example: + +.nf + $ mu cfind '@gmail\.com' +.fi + +would find all contacts with a gmail-address, while + +.nf + $ mu cfind Mary +.fi + +lists all contacts with Mary in either name or e-mail address. + +If you do not specify a search expression, \fBmu cfind\fR returns the full list +of contacts. Note, \fBmu cfind\fR uses a cache with the e-mail information, +which is populated during the indexing process. + +The regular expressions are Perl-compatible (as per the PCRE-library used by +GRegex). + +.SH OPTIONS + +.TP +\fB\-\-format\fR=\fIplain|mutt-alias|mutt-ab|wl|org-contact|bbdb|csv\fR +sets the output format to the given value. The following are available: + +.nf +| --format= | description | +|-------------+-----------------------------------| +| plain | default, simple list | +| mutt-alias | mutt alias-format | +| mutt-ab | mutt external address book format | +| wl | wanderlust addressbook format | +| org-contact | org-mode org-contact format | +| bbdb | BBDB format | +| csv | comma-separated values (*) | +.fi + + +(*) CSV is not fully standardized, but \fBmu cfind\fR follows some common +practices: any double-quote is replaced by a double-double quote (thus, "hello" +become ""hello"", and fields with commas are put in double-quotes. Normally, +this should only apply to name fields. + +.TP +\fB\-\-personal\fR only show addresses seen in messages where one of 'my' e-mail +addresses was seen in one of the address fields; this is to exclude addresses +only seen in mailing-list messages. See the \fB\-\-my-address\fR parameter in +\fBmu index\fR. + +.TP +\fB\-\-after=\fR\fI\fR only show addresses last seen after +\fI\fR. \fI\fR is a UNIX \fBtime_t\fR value, the number of +seconds since 1970-01-01 (in UTC). + +From the command line, you can use the \fBdate\fR command to get this value. For +example, only consider addresses last seen after 2009-06-01, you could specify +.nf + --after=`date +%s --date='2009-06-01'` +.fi + +.SH RETURN VALUE + +\fBmu cfind\fR returns 0 upon successful completion -- that is, at least one +contact was found. Anything else leads to a non-zero return value: + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | general error | +| 2 | no matches (for 'mu cfind') | +.fi + +.SH INTEGRATION WITH MUTT + +You can use \fBmu cfind\fR as an external address book server for \fBmutt\fR. +For this to work, add the following to your \fImuttrc\fR: + +.nf +set query_command = "mu cfind --format=mutt-ab '%s'" +.fi + +Now, in mutt, you can search for e-mail addresses using the \fBquery\fR-command, +which is (by default) accessible by pressing \fBQ\fR. + +.SH ENCODING + +\fBmu cfind\fR output is encoded according to the current locale except for +\fI--format=bbdb\fR. This is hard-coded to UTF-8, and as such specified in the +output-file, so emacs/bbdb can handle things correctly, without guessing. + +.SH BUGS + +Please report bugs if you find them at \fBhttps://github.com/djcb/mu/issues\fR. + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-find (1), +.BR pcrepattern(3) diff --git a/man/mu-easy.1 b/man/mu-easy.1 new file mode 100644 index 0000000..38953be --- /dev/null +++ b/man/mu-easy.1 @@ -0,0 +1,313 @@ +.TH MU-EASY 1 "February 2020" "User Manuals" + +.SH NAME + +mu easy \- a quick introduction to mu + +.SH DESCRIPTION + +\fBmu\fR is a set of tools for dealing with e-mail messages in Maildirs. There +are many options, which are all described in the man pages for the various +sub-commands. This man pages jumps over all of the details and gives examples of +some common use cases. If the use cases described here do not precisely do what +you want, please check the more extensive information in the man page about the +sub-command you are using -- for example, the \fBmu-index\fR(1) or +\fBmu-find\fR(1) man pages. + +\fBNOTE\fR: the \fBindex\fR command (and therefore, the ones that depend on +that, such as \fBfind\fR), require that you store your mail in the +Maildir-format. If you don't do so, you can still use the other commands, but +you won't be able to index/search your mail. + +By default, \fBmu\fR uses colorized output when it thinks your terminal is +capable of doing so. If you don't like color, you can use the \fB--nocolor\fR +command-line option, or set either the \fBMU_NOCOLOR\fR or the \fBNO_COLOR\fR +environment variable to non-empty. + +.SH SETTING THINGS UP + +The first time you run the mu commands, you need to initialize it. This is done +with the \fBinit\fR command. + +.nf + \fB$ mu init\fR +.fi + +This uses the defaults (see \fBmu-init(1)\fR for details on how to change that). + + +.SH INDEXING YOUR E-MAIL + +Before you can search e-mails, you'll first need to index them: + +.nf + \fB$ mu index\fR +.fi + +The process can take a few minutes, depending on the amount of mail +you have, the speed of your computer, hard drive etc. Usually, +indexing should be able to reach a speed of a few hundred messages per +second. + +\fBmu index\fR guesses the top-level Maildir to do its job; if it +guesses wrongly, you can use the \fI--maildir\fR option to specify the +top-level directory that should be processed. See the \fBmu-index\fR(1) +man page for more details. + +Normally, \fBmu index\fR visits all the directories under the +top-level Maildir; however, you can exclude certain directories (say, +the 'trash' or 'spam' folders) by creating a file called +\fI.noindex\fR in the directory. When \fBmu\fR sees such a file, it +will exclude this directory and its sub-directories from indexing. +Also see \fB.noupdate\fR in the \fBmu-index\fR(1) manpage. + +.SH SEARCHING YOUR E-MAIL + +After you have indexed your mail, you can start searching it. By +default, the search results are printed on standard output. +Alternatively, the output can take the form of Maildir with symbolic +links to the found messages. This enables integration with e-mail +clients; see the \fBmu-find\fR(1) man page for details, the syntax of the +search parameters and so on. Here, we just give some examples for +common cases. + +First, let's search for all messages sent to Julius (Caesar) regarding +fruit: + +.nf +\fB$ mu find t:julius fruit\fR +.fi + +This should return something like: + +.nf + 2008-07-31T21:57:25 EEST John Milton Fere libenter homines id quod volunt credunt +.fi + +This means there is a message to 'julius' with 'fruit' somewhere in +the message. In this case, it's a message from John Milton. Note that +the date format depends on your the language/locale you are using. + +How do we know that the message was sent to Julius Caesar? Well, it's +not visible from the results above, because the default fields that +are shown are date/sender/subject. However, we can change this using +the \fI--fields\fR parameter (see the \fBmu-find\fR(1) man page for the +details): + +.nf + \fB$ mu find --fields="t s" t:julius fruit\fR +.fi + +In other words, display the 'To:'-field (t) and the subject (s). This should +return something like: +.nf + Julius Caesar Fere libenter homines id quod volunt credunt +.fi + +This is the same message found before, only with some different fields +displayed. + +By default, \fBmu\fR uses the logical AND for the search parameters -- that +is, it displays messages that match all the parameters. However, we can use +logical OR as well: + +.nf + \fB$ mu find t:julius OR f:socrates\fR +.fi + +In other words, display messages that are either sent to Julius Caesar +\fBor\fR are from Socrates. This could return something like: + +.nf + 2008-07-31T21:57:25 EEST Socrates cool stuff + 2008-07-31T21:57:25 EEST John Milton Fere libenter homines id quod volunt credunt +.fi + +What if we want to see some of the body of the message? You can get +a 'summary' of the first lines of the message using the \fI--summary-len\fR +option, which will 'summarize' the first \fIn\fR lines of the message: + +.nf + \fB$ mu find --summary-len=3 napoleon m:/archive\fR +.fi + +.nf + 1970-01-01T02:00:00 EET Napoleon Bonaparte rock on dude + Summary: Le 24 février 1815, la vigie de Notre-Dame de la Garde signala le + trois-mâts le Pharaon, venant de Smyrne, Trieste et Naples. Comme + d'habitude, un pilote côtier partit aussitôt du port, rasa le château +.fi + +The summary consists of the first n lines of the message with all superfluous +whitespace removed. + +Also note the \fBm:/archive\fR parameter in the query. This means that we only +match messages in a maildir called '/archive'. + +.SH MORE QUERIES + +Let's list a few more queries that may be interesting; please note that +searches for message flags, priority and date ranges are only available in mu +version 0.9 or later. + +Get all important messages which are signed: +.nf + \fB$ mu find flag:signed prio:high \fR +.fi + +Get all messages from Jim without an attachment: +.nf + \fB$ mu find from:jim AND NOT flag:attach\fR +.fi + +Get all messages where Jack is in one of the contact fields: +.nf + \fB$ mu find contact:jack\fR +.fi +This uses the special contact: pseudo-field which matches (\fBfrom\fR, +\fBto\fR, \fBcc\fR and \fBbcc\fR). + +Get all messages in the Sent Items folder about yoghurt: +.nf + \fB$mu find maildir:'/Sent Items' yoghurt\fR +.fi +Note how we need to quote search terms that include spaces. + + +Get all unread messages where the subject mentions Ångström: +.nf + \fB$ mu find subject:Ångström flag:unread\fR +.fi +which is equivalent to: +.nf + \fB$ mu find subject:angstrom flag:unread\fR +.fi +because does mu is case-insensitive and accent-insensitive. + +Get all unread messages between March 2002 and August 2003 about some bird (or +a Swedish rock band): +.nf + \fB$ mu find date:20020301..20030831 nightingale flag:unread\fR +.fi + +Get all messages received today: +.nf + \fB$ mu find date:today..now\fR +.fi + +Get all messages we got in the last two weeks about emacs: +.nf + \fB$ mu find date:2w..now emacs\fR +.fi + +Another powerful feature (since 0.9.6) are wildcard searches, where you can +search for the last \fIn\fR characters in a word. For example, you can search +for: +.nf + \fB$ mu find 'subject:soc*'\fR +.fi +and get mails about soccer, Socrates, society, and so on. Note, it's important +to quote the search query, otherwise the shell will interpret +the '*'. + +You can also search for messages with a certain attachment using their +filename, for example: + +.nf + \fB$ mu find 'file:pic*'\fR +.fi +will get you all messages with an attachment starting with 'pic'. + +If you want to find attachments with a certain MIME-type, you can use the +following: + +Get all messages with PDF attachments: +.nf + \fB$ mu find mime:application/pdf\fR +.fi + +or even: + +Get all messages with image attachments: +.nf + \fB$ mu find 'mime:image/*'\fR +.fi + + +Note that (1) the '*' wildcard can only be used as the rightmost thing in a +search query, and (2) that you need to quote the search term, because +otherwise your shell will interpret the '*' (expanding it to all files in the +current directory -- probably not what you want). + +.SH DISPLAYING MESSAGES + +We might also want to display the complete messages instead of the header +information. This can be done using \fBmu view\fR command. Note that this +command does not use the database; you simply provide it the path to a +message. + +Therefore, if you want to display some message from a search query, you'll +need its path. To get the path (think \fBl\fRocation) for our first example we +can use: + +.nf + \fB$ mu find --fields="l" t:julius fruit\fR +.fi + +And we'll get something like: +.nf + /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2, +.fi +We can now display this message: + +.nf + \fB$ mu view /home/someuser/Maildir/archive/cur/1266188485_0.6850.cthulhu:2,\fR + + From: John Milton + To: Julius Caesar + Subject: Fere libenter homines id quod volunt credunt + Date: 2008-07-31T21:57:25 EEST + + OF Mans First Disobedience, and the Fruit + Of that Forbidden Tree, whose mortal taste + Brought Death into the World, and all our woe, + [...] +.fi + +.SH FINDING CONTACTS + +While \fBmu find\fR searches for messages, there is also \fBmu cfind\fR to +find \fIcontacts\fR, that is, names + addresses. Without any search +expression, \fBmu cfind\fR lists all of your contacts. + +.nf + \fB$ mu cfind julius\fR +.fi + +will find all contacts with 'julius' in either name or e-mail address. Note +that \fBmu cfind\fR accepts a \fIregular expression\fR. + +\fBmu cfind\fR also supports a \fI--format=\fR-parameter, which sets the +output to some specific format, so the results can be imported into another +program. For example, to export your contact information to a \fBmutt\fR +address book file, you can use something like: + +.nf + \fB$ mu cfind --format=mutt-alias > ~/mutt-aliases \fR +.fi + +Then, you can use them in \fBmutt\fR if you add something like \fBsource +~/mutt-aliases\fR to your \fImuttrc\fR. + +.SH AUTHOR +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu (1), +.BR mu-init (1), +.BR mu-index (1), +.BR mu-find (1), +.BR mu-mfind (1), +.BR mu-mkdir (1), +.BR mu-view (1), +.BR mu-extract (1) diff --git a/man/mu-extract.1 b/man/mu-extract.1 new file mode 100644 index 0000000..35d96ce --- /dev/null +++ b/man/mu-extract.1 @@ -0,0 +1,100 @@ +.TH MU EXTRACT 1 "July 2012" "User Manuals" + +.SH NAME + +\fBmu extract\fR is the \fBmu\fR command to display and save message parts +(attachments), and open them with other tools. + +.SH SYNOPSIS + +.B mu extract [options] + +.B mu extract [options] + +.SH DESCRIPTION + +\fBmu extract\fR is the \fBmu\fR sub-command for extracting MIME-parts (e.g., +attachments) from mail messages. The sub-command works on message files, and +does not require the message to be indexed in the database. + +For attachments, the file name used when saving it is the name of the +attachment in the message. If there is no such name, or when saving +non-attachment MIME-parts, a name is derived from the message-id of the +message. + +If you specify a pattern (a case-insensitive regular expression) as the second +argument, all attachments with filenames matching that pattern will be +extracted. The regular expressions are Perl-compatible (as per the +PCRE-library). + +Without any options, \fBmu extract\fR simply outputs the list of leaf +MIME-parts in the message. Only 'leaf' MIME-parts (including RFC822 +attachments) are considered, \fBmultipart/*\fR etc. are ignored. + +.SH OPTIONS + +.TP +\fB\-a\fR, \fB\-\-save\-attachments\fR +save all MIME-parts that look like attachments. + +.TP +\fB\-\-save\-all\fR +save all non-multipart MIME-parts. + +.TP +\fB\-\-parts\fR= +only consider the following numbered parts +(comma-separated list). The numbers for the parts can be seen from running +\fBmu extract\fR without any options but only the message file. + +.TP +\fB\-\-target\-dir\fR= +save the parts in the target directory rather than +the current working directory. + +.TP +\fB\-\-overwrite\fR +overwrite existing files with the same name; by default overwriting is not +allowed. + +.TP +\fB\-\-play\fR Try to 'play' (open) the attachment with the default +application for the particular file type. On MacOS, this uses the \fBopen\fR +program, on other platforms it uses \fBxdg-open\fR. You can choose a different +program by setting the \fBMU_PLAY_PROGRAM\fR environment variable. + +.SH EXAMPLES + +To display information about all the MIME-parts in a message file: +.nf + $ mu extract msgfile +.fi + +To extract MIME-part 3 and 4 from this message, overwriting existing files +with the same name: +.nf + $ mu extract --parts=3,4 --overwrite msgfile +.fi + +To extract all files ending in '.jpg' (case-insensitive): +.nf + $ mu extract msgfile '.*\.jpg' +.fi + +To extract an mp3-file, and play it in the default mp3-playing application: +.nf + $ mu extract --play msgfile 'whoopsididitagain.mp3' +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1) diff --git a/man/mu-find.1 b/man/mu-find.1 new file mode 100644 index 0000000..5e46487 --- /dev/null +++ b/man/mu-find.1 @@ -0,0 +1,389 @@ +.TH MU FIND 1 "19 April 2015" "User Manuals" + +.SH NAME + +mu find \- find e-mail messages in the \fBmu\fR database. + +mu mfind \- find e-mail messages in the \fBmu\fR database with mu4e defaults. + +.SH SYNOPSIS + +.B mu find [options] + +.SH DESCRIPTION + +\fBmu find\fR is the \fBmu\fR command for searching e-mail message +that were stored earlier using \fBmu index\fR(1). + +\fBmu mfind\fR is a version of \fBmu find\fR that defaults to +\f--include-related\fR and \fB--skip-dups\fR, just like \fBmu4e\fR does. + +.SH SEARCHING MAIL + +\fBmu find\fR starts a search for messages in the database that match +some search pattern. The search patterns are described in detail in +.BR mu-query (7). +. + +For example: + +.nf + $ mu find subject:snow and date:2017.. +.fi + +would find all messages in 2017 with 'snow' in the subject field, e.g: + +.nf + 2009-03-05 17:57:33 EET Lucia running in the snow + 2009-03-05 18:38:24 EET Marius Re: running in the snow +.fi + +Note, this the default, plain-text output, which is the default, so you don't +have to use \fB--format=plain\fR. For other types of output (such as symlinks, +XML or s-expressions), see the discussion in the \fBOPTIONS\fR-section +below about \fB--format\fR. + +The search pattern is taken as a command-line parameter. If the search +parameter consists of multiple parts (as in the example) they are +treated as if there were a logical \fBand\fR between them. + +For details on the possible queries, see +.BR mu-query (7). + +.SH OPTIONS + +Note, some of the important options are described in the \fBmu\fR(1) man-page +and not here, as they apply to multiple mu-commands. + +The \fBfind\fR-command has various options that influence the way \fBmu\fR +displays the results. If you don't specify anything, the defaults are +\fI\-\-fields="d f s"\fR, \fI\-\-sortfield=date\fR and \fI\-\-reverse\fR. + +.TP +\fB\-f\fR, \fB\-\-fields\fR=\fI\fR +specifies a string that determines which fields are shown in the output. This +string consists of a number of characters (such as 's' for subject or 'f' for +from), which will replace with the actual field in the output. Fields that are +not known will be output as-is, allowing for some simple formatting. + +For example: + +.nf + $ mu find subject:snow --fields "d f s" +.fi + +would list the date, subject and sender of all messages with 'snow' in the +their subject. + +The table of replacement characters is superset of the list mentions for +search parameters; the complete list: + +.nf + t \fBt\fRo: recipient + c \fBc\fRc: (carbon-copy) recipient + h Bcc: (blind carbon-copy, \fBh\fRidden) recipient + d Sent \fBd\fRate of the message + f Message sender (\fBf\fRrom:) + g Message flags (fla\fBg\fRs) + l Full path to the message (\fBl\fRocation) + p Message \fBp\fRriority (high, normal, low) + s Message \fBs\fRubject + i Message-\fBi\fRd + m \fBm\fRaildir + v Mailing-list Id +.fi + + +The message flags are described in \fBmu-query\fR(7). As an example, a +message which is 'seen', has an attachment and is signed would +have 'asz' as its corresponding output string, while an encrypted new +message would have 'nx'. + +.TP +\fB\-s\fR, \fB\-\-sortfield\fR \fR=\fI\fR and \fB\-z\fR, +\fB\-\-reverse\fR specifies the field to sort the search results by, and the +direction (i.e., 'reverse' means that the sort should be reverted - Z-A). The +following fields are supported: + +.nf + cc,c Cc (carbon-copy) recipient(s) + bcc,h Bcc (blind-carbon-copy) recipient(s) + date,d Message sent date + from,f Message sender + maildir,m Maildir + msgid,i Message id + prio,p Nessage priority + subject,s Message subject + to,t To:-recipient(s) + list,v Mailing-list id +.fi + +Thus, for example, to sort messages by date, you could specify: + +.nf + $ mu find fahrrad --fields "d f s" --sortfield=date --reverse +.fi + +Note, if you specify a sortfield, by default, messages are sorted in reverse +(descending) order (e.g., from lowest to highest). This is usually a good +choice, but for dates it may be more useful to sort in the opposite direction. + +.TP +\fB\-n\fR, \fB\-\-maxnum=\fR +If > 0, display maximally that number of entries. If not specified, all matching entries are displayed. + +.TP +\fB\-\-summary-len=\fR +If > 0, use that number of lines of the message to provide a summary. + +.TP +\fB\-\-format\fR=\fIplain|links|xquery|xml|sexp\fR +output results in the specified format. + +The default is \fBplain\fR, i.e normal output with one line per message. + +\fBlinks\fR outputs the results as a maildir with symbolic links to the found +messages. This enables easy integration with mail-clients (see below for more +information). + +\fBxml\fR formats the search results as XML. + +\fBsexp\fR formats the search results as an s-expression as used in Lisp +programming environments. + +\fBxquery\fR shows the Xapian query corresponding to your search terms. This +is meant for for debugging purposes. + +.TP +\fB\-\-linksdir\fR \fR=\fI\fR and \fB\-c\fR, \fB\-\-clearlinks\fR +output the results as a maildir with symbolic links to the found +messages. This enables easy integration with mail-clients (see below +for more information). \fBmu\fR will create the maildir if it does not +exist yet. + +If you specify \fB\-\-clearlinks\fR, all existing symlinks will be +cleared from the target directories; this allows for re-use of the +same maildir. However, this option will delete any symlink it finds, +so be careful. + +.nf + $ mu find grolsch --linksdir=~/Maildir/search --clearlinks +.fi + +will store links to found messages in \fI~/Maildir/search\fR. If the directory +does not exist yet, it will be created. + +Note: when \fBmu\fR creates a Maildir for these links, it automatically +inserts a \fI.noindex\fR file, to exclude the directory from \fBmu +index\fR. + +.TP +\fB\-\-after=\fR\fI\fR only show messages whose message files were +last modified (\fBmtime\fR) after \fI\fR. \fI\fR is a +UNIX \fBtime_t\fR value, the number of seconds since 1970-01-01 (in UTC). + +From the command line, you can use the \fBdate\fR command to get this +value. For example, only consider messages modified (or created) in the last 5 +minutes, you could specify +.nf + --after=`date +%s --date='5 min ago'` +.fi +This is assuming the GNU \fBdate\fR command. + + +.TP +\fB\-\-exec\fR=\fI\fR +the \fB\-\-exec\fR command causes the \fIcommand\fR to be executed on each +matched message; for example, to see the raw text of all messages +matching 'milkshake', you could use: +.nf + $ mu find milkshake --exec='less' +.fi +which is roughly equivalent to: +.nf + $ mu find milkshake --fields="l" | xargs less +.fi + + +.TP +\fB\-b\fR, \fB\-\-bookmark\fR=\fI\fR +use a bookmarked search query. Using this option, a query from your bookmark +file will be prepended to other search queries. See \fBmu-bookmarks\fR(1) for the +details of the bookmarks file. + + +.TP +\fB\-\-skip\-dups\fR,\fB-u\fR whenever there are multiple messages with the +same name, only show the first one. This is useful if you have copies of the +same message, which is a common occurrence when using e.g. Gmail together with +\fBofflineimap\fR. + +.TP +\fB\-\-include\-related\fR,\fB-r\fR also include messages being referred to by +the matched messages -- i.e.. include messages that are part of the same +message thread as some matched messages. This is useful if you want +Gmail-style 'conversations'. Note, finding these related messages make +searches slower. + +.TP +\fB\-t\fR, \fB\-\-threads\fR show messages in a 'threaded' format -- that is, +with indentation and arrows showing the conversation threads in the list of +matching messages. + +Messages in the threaded list are indented based on the depth in the +discussion, and are prefix with a kind of arrow with thread-related +information about the message, as in the following table: + +.nf +| | normal | orphan | duplicate | +|-------------+--------+--------+-----------| +| first child | `-> | `*> | `=> | +| other | |-> | |*> | |=> | +.fi + +Here, an 'orphan' is a message without a parent message (in the list of +matches), and a duplicate is a message whose message-id was already seen +before; not this may not really be the same message, if the message-id was +copied. + +The algorithm used for determining the threads is based on Jamie Zawinksi's +description: +.BR http://www.jwz.org/doc/threading.html + + +.SS Integrating mu find with mail clients + +.TP + +\fBmutt\fR + +For \fBmutt\fR you can use the following in your \fImuttrc\fR; pressing the F8 +key will start a search, and F9 will take you to the results. + +.nf +# mutt macros for mu +macro index "mu find --clearlinks --format=links --linksdir=~/Maildir/search " \\ + "mu find" +macro index "~/Maildir/search" \\ + "mu find results" +.fi + + +.TP + +\fBWanderlust\fR + +\fBSam B\fR suggested the following on the \fBmu\fR-mailing list. First add +the following to your Wanderlust configuration file: + +.nf +(require 'elmo-search) +(elmo-search-register-engine + 'mu 'local-file + :prog "/usr/local/bin/mu" ;; or wherever you've installed it + :args '("find" pattern "--fields" "l") :charset 'utf-8) + +(setq elmo-search-default-engine 'mu) +;; for when you type "g" in folder or summary. +(setq wl-default-spec "[") +.fi + +Now, you can search using the \fBg\fR key binding; you can also create +permanent virtual folders when the messages matching some expression by adding +something like the following to your \fIfolders\fR file. + +.nf +VFolders { + [date:today..now]!mu "Today" + + [size:1m..100m]!mu "Big" + + [flag:unread]!mu "Unread" +} +.fi + +After restarting Wanderlust, the virtual folders should appear. + + +\fBWanderlust (old)\fR + +Another way to integrate \fBmu\fR and \fBwanderlust\fR is shown below; the +aforementioned method is recommended, but if that does not work for some +reason, the below can be an alternative. + +.nf +(defvar mu-wl-mu-program "/usr/local/bin/mu") +(defvar mu-wl-search-folder "search") + +(defun mu-wl-search () + "search for messages with `mu', and jump to the results" + (let* ((muexpr (read-string "Find messages matching: ")) + (sfldr (concat elmo-maildir-folder-path "/" + mu-wl-search-folder)) + (cmdline (concat mu-wl-mu-program " find " + "--clearlinks --format=links --linksdir='" sfldr "' " + muexpr)) + (rv (shell-command cmdline))) + (cond + ((= rv 0) (message "Query succeeded")) + ((= rv 2) (message "No matches found")) + (t (message "Error running query"))) + (= rv 0))) + +(defun mu-wl-search-and-goto () + "search and jump to the folder with the results" + (interactive) + (when (mu-wl-search) + (wl-summary-goto-folder-subr + (concat "." mu-wl-search-folder) + 'force-update nil nil t) + (wl-summary-sort-by-date))) + +;; querying both in summary and folder +(define-key wl-summary-mode-map (kbd "Q") ;; => query + '(lambda()(interactive)(mu-wl-search-and-goto))) +(define-key wl-folder-mode-map (kbd "Q") ;; => query + '(lambda()(interactive)(mu-wl-search-and-goto))) + +.fi + + +.SH RETURN VALUE + +\fBmu find\fR returns 0 upon successful completion; if the search was +performed, there needs to be a least one match. Anything else leads to a +non-zero return value, for example: + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | general error | +| 2 | no matches (for 'mu find') | +| 4 | database is corrupted | +.fi + + +.SH ENCODING + +\fBmu find\fR output is encoded according the locale for \fI--format=plain\fR +(the default), and UTF-8 for all other formats (\fIsexp\fR, +\fIxml\fR). + + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues +If you have specific messages which are not matched correctly, please attach +them (appropriately censored if needed). + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-query (7) diff --git a/man/mu-help.1 b/man/mu-help.1 new file mode 100644 index 0000000..80ba090 --- /dev/null +++ b/man/mu-help.1 @@ -0,0 +1,34 @@ +.TH MU HELP 1 "July 2012" "User Manuals" + +.SH NAME + +\fBmu help\fR is a \fBmu\fR command that gives help information about mu +commands. + +.SH SYNOPSIS + +.B mu help + +.SH DESCRIPTION + +\fBmu help\fR provides help information about mu commands. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu-index (1), +.BR mu-find (1), +.BR mu-cfind (1), +.BR mu-mkdir (1), +.BR mu-view (1), +.BR mu-extract (1), +.BR mu-easy (1), +.BR mu-bookmarks (5) diff --git a/man/mu-index.1 b/man/mu-index.1 new file mode 100644 index 0000000..c5b6008 --- /dev/null +++ b/man/mu-index.1 @@ -0,0 +1,188 @@ +.TH MU-INDEX 1 "February 2020" "User Manuals" + +.SH NAME + +mu index \- index e-mail messages stored in Maildirs + +.SH SYNOPSIS + +.B mu index [options] + +.SH DESCRIPTION + +\fBmu index\fR is the \fBmu\fR command for scanning the contents of Maildir +directories and storing the results in a Xapian database. The data can then be +queried using +.BR mu-find (1)\. + +Note that before the first time you run \fBmu index\fR, you must run \fBmu +init\fR to initialize the database. + +\fBindex\fR understands Maildirs as defined by Daniel Bernstein for +\fBqmail\fR(7). In addition, it understands recursive Maildirs (Maildirs +within Maildirs), Maildir++. It can also deal with VFAT-based Maildirs +which use '!' as the separators instead of ':'. + +E-mail messages which are not stored in something resembling a maildir +leaf-directory (\fIcur\fR and \fInew\fR) are ignored, as are the cache +directories for \fInotmuch\fR and \fIgnus\fR, and any dot-directory. + +The maildir must be on a single file-system; symlinks are not followed. + +If there is a file called \fI.noindex\fR in a directory, the contents of that +directory and all of its subdirectories will be ignored. This can be useful to +exclude certain directories from the indexing process, for example directories +with spam-messages. + +If there is a file called \fI.noupdate\fR in a directory, the contents of that +directory and all of its subdirectories will be ignored, unless we do a full +rebuild (with \fBmu init\fR). This can be useful to speed up things you have +some maildirs that never change. Note that you can still search for these +messages, this only affects updating the database. + +There also the \fB--lazy-check\fR which can greatly speed up indexing; +see below for details. + +The first run of \fBmu index\fR may take a few minutes if you have a +lot of mail (tens of thousands of messages). Fortunately, such a full +scan needs to be done only once; after that it suffices to index the +changes, which goes much faster. See the 'Note on performance +(i,ii,iii)' below for more information. + +The optional 'phase two' of the indexing-process is the removal of messages +from the database for which there is no longer a corresponding file in the +Maildir. If you do not want this, you can use \fB\-n\fR, \fB\-\-nocleanup\fR. + +When \fBmu index\fR catches one of the signals \fBSIGINT\fR, \fBSIGHUP\fR or +\fBSIGTERM\fR (e.g., when you press Ctrl-C during the indexing process), it +tries to shutdown gracefully; it tries to save and commit data, and close the +database etc. If it receives another signal (e.g., when pressing Ctrl-C once +more), \fBmu index\fR will terminate immediately. + +.SH OPTIONS + +Note, some of the general options are described in the \fBmu(1)\fR man-page +and not here, as they apply to multiple mu commands. + +.TP +\fB\-\-lazy-check\fR +in lazy-check mode, \fBmu\fR does not consider messages for which the +time-stamp (ctime) of the directory they reside in has not changed +since the previous indexing run. This is much faster than the non-lazy +check, but won't update messages that have change (rather than having +been added or removed), since merely editing a message does not update +the directory time-stamp. Of course, you can run \fBmu-index\fR +occasionally without \fB\-\-lazy-check\fR, to pick up such messages. + +.TP +\fB\-\-nocleanup\fR +disables the database cleanup that \fBmu\fR does by default after indexing. + +.SS A note on performance (i) +As a non-scientific benchmark, a simple test on the author's machine (a +Thinkpad X61s laptop using Linux 2.6.35 and an ext3 file system) with no +existing database, and a maildir with 27273 messages: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 66,65s user 6,05s system 27% cpu 4:24,20 total +.fi +(about 103 messages per second) + +A second run, which is the more typical use case when there is a database +already, goes much faster: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 0,48s user 0,76s system 10% cpu 11,796 total +.fi +(more than 56818 messages per second) + +Note that each test flushes the caches first; a more common use case might +be to run \fBmu index\fR when new mail has arrived; the cache may stay +quite 'warm' in that case: + +.nf + $ time mu index --quiet + 0,33s user 0,40s system 80% cpu 0,905 total +.fi +which is more than 30000 messages per second. + + +.SS A note on performance (ii) +As per June 2012, we did the same non-scientific benchmark, this time with an +Intel i5-2500 CPU @ 3.30GHz, an ext4 file system and a maildir with 22589 +messages. We start without an existing database. + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 27,79s user 2,17s system 48% cpu 1:01,47 total +.fi +(about 813 messages per second) + +A second run, which is the more typical use case when there is a database +already, goes much faster: + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 0,13s user 0,30s system 19% cpu 2,162 total +.fi +(more than 173000 messages per second) + + +.SS A note on performance (iii) +As per July 2016, we did the same non-scientific benchmark, again with +the Intel i5-2500 CPU @ 3.30GHz, an ext4 file system. This time, the +maildir contains 72525 messages. + +.nf + $ sudo sh -c 'sync && echo 3 > /proc/sys/vm/drop_caches' + $ time mu index --quiet + 40,34s user 2,56s system 64% cpu 1:06,17 total +.fi +(about 1099 messages per second). + +As shown, \fBmu\fR has been getting faster with each release, even +with relatively expensive new features such as text-normalization (for +case-insensitve/accent-insensitive matching). The profiles are +dominated by operations in the Xapian database now. + +.SH FILES +\fBmu\fR stores logs of its operations and queries in \fI/mu.log\fR +(by default, this is \fI~/.cache/mu/mu.log\fR). Upon startup, \fBmu\fR checks the +size of this log file. If it exceeds 1 MB, it will be moved to +\fI~/.cache/mu/mu.log.old\fR, overwriting any existing file of that name, and start +with an empty log file. This scheme allows for continued use of \fBmu\fR +without the need for any manual maintenance of log files. + +.SH ENVIRONMENT + +\fBmu index\fR uses \fBMAILDIR\fR to find the user's Maildir if it has not +been specified explicitly with \fB\-\-maildir\fR=\fI\fR. If +\fBMAILDIR\fR is not set, \fBmu index\fR will try \fI~/Maildir\fR. + +.SH RETURN VALUE + +\fBmu index\fR return 0 upon successful completion, and any other number +greater than 0 signals an error. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-init (1), +.BR mu-find (1), +.BR mu-cfind (1) diff --git a/man/mu-info.1 b/man/mu-info.1 new file mode 100644 index 0000000..43032be --- /dev/null +++ b/man/mu-info.1 @@ -0,0 +1,46 @@ +.TH MU-INFO 1 "February 2020" "User Manuals" + +.SH NAME + +mu info \- show information about the mu database + +.SH SYNOPSIS + +.B mu info [options] + +.SH DESCRIPTION + +\fBmu info\fR is the \fBmu\fR command for getting information about the mu +database. + +.SH OPTIONS + +Note, some of the general options are described in the \fBmu(1)\fR man-page and +not here, as they apply to multiple mu commands. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of +\fBmu\fR defaulted to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR. + +.SH RETURN VALUE + +\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if +there was some error. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-index (1) diff --git a/man/mu-init.1 b/man/mu-init.1 new file mode 100644 index 0000000..34f95d8 --- /dev/null +++ b/man/mu-init.1 @@ -0,0 +1,69 @@ +.TH MU-INIT 1 "February 2020" "User Manuals" + +.SH NAME + +mu init \- initialize the mu message database + +.SH SYNOPSIS + +.B mu init [options] + +.SH DESCRIPTION + +\fBmu init\fR is the \fBmu\fR command for setting up the mu message +database. After \fBmu init\fR has completed, you can run \fBmu index\fR + +.SH OPTIONS + +Note, some of the general options are described in the \fBmu(1)\fR man-page and +not here, as they apply to multiple mu commands. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +this defaults to \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of +\fBmu\fR defaulted to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR. + +.TP +\fB\-m\fR, \fB\-\-maildir\fR=\fI\fR +starts searching at \fI\fR. By default, \fBmu\fR uses whatever the +\fBMAILDIR\fR environment variable is set to; if it is not set, it tries +\fI~/Maildir\fR. The maildir must be on a single file-system; and symbolic links +are not supported. + +.TP +\fB\-\-my-address\fR=\fI\fR + +specifies that some e-mail addresses are 'my-address' (\fB\-\-my-address\fR can +be used multiple times). This is used by \fBmu cfind\fR -- any e-mail address +found in the address fields of a message which also has \fI\fR +in one of its address fields is considered a \fIpersonal\fR e-mail address. This +allows you, for example, to filter out (\fBmu cfind --personal\fR) addresses +which were merely seen in mailing list messages. + +.SH ENVIRONMENT + +\fBmu init\fR uses \fBMAILDIR\fR to find the user's Maildir if it has not been +specified explicitly with \fB\-\-maildir\fR=\fI\fR. If \fBMAILDIR\fR is +not set, \fBmu init\fR uses \fI~/Maildir\fR. + +.SH RETURN VALUE + +\fBmu init\fR returns 0 upon successful completion, or a non-zero exit code if +there was some error. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR mu-index (1) diff --git a/man/mu-mkdir.1 b/man/mu-mkdir.1 new file mode 100644 index 0000000..6f9ddd2 --- /dev/null +++ b/man/mu-mkdir.1 @@ -0,0 +1,45 @@ +.TH MU MKDIR 1 "July 2012" "User Manuals" + +.SH NAME + +mu mkdir\- create a new Maildir + +.SH SYNOPSIS + +.B mu mkdir [options] [] + +.SH DESCRIPTION + +\fBmu mkdir\fR is the \fBmu\fR command for creating Maildirs. It does +\fBnot\fR use the mu database. With the \fBmkdir\fR command, you can create +new Maildirs with permissions 0755. For example, + +.nf + mu mkdir tom dick harry +.fi + +creates three maildirs, \fItom\fR, \fIdick\fR and \fIharry\fR. + +If creation fails for any reason, \fBno\fR attempt is made to remove any parts +that were created. This is for safety reasons. + +.SH OPTIONS + +.TP +\fB\-\-mode\fR= +set the file access mode for the new maildir(s) as in \fBchmod(1)\fR. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR maildir (5), +.BR mu (1), +.BR chmod (1) diff --git a/man/mu-query.7 b/man/mu-query.7 new file mode 100644 index 0000000..c3f3f5c --- /dev/null +++ b/man/mu-query.7 @@ -0,0 +1,358 @@ +.TH MU QUERY 7 "28 December 2017" "User Manuals" + +.SH NAME + +mu query language \- a language for finding messages in \fBmu\fR +databases. + +.SH DESCRIPTION + +The mu query language is a language used by \fBmu find\fR and +\fBmu4e\fR to find messages in \fBmu\fR's Xapian databases. The +language is quite similar to Xapian's default query-parser, but is an +independent implementation that is customized for the mu/mu4e +use-case. + +In this article, we give a structured but informal overview of the +query language and provide examples. + +\fBNOTE:\fR t if you use queries on the command-line (say, for \fBmu +find\fR), you need to quote any characters that would otherwise be +interpreted by the shell, such as \fB""\fR, \fB(\fR and \fB)\fR and +whitespace. + +.de EX1 +.nf +.RS +.. + +.de EX2 +.RE +.fi +.. + +.SH TERMS + +The basic building blocks of a query are \fBterms\fR; these are just +normal words like 'banana' or 'hello', or words prefixed with a +field-name which make them apply to just that field. See +.BR mu find +for all the available fields. + +Some example queries: +.EX1 +vacation +subject:capybara +maildir:/inbox +.EX2 + +Terms without an explicit field-prefix, (like 'vacation' above) are +interpreted like: +.EX1 +to:vacation or subject:vacation or body:vacation or ... +.EX2 + +The language is case-insensitive for terms and attempts to 'flatten' +any diacritics, so \fIangtrom\fR matches \fIÅngström\fR. + +.PP +If terms contain whitespace, they need to be quoted: +.EX1 +subject:"hi there" +.EX2 +This is a so-called \fIphrase query\fR, which means that we match +against subjects that contain the literal phrase "hi there". + +Remember that you need to escape those quotes when using this from the +command-line: +.EX1 +mu find subject:\\"hi there\\" +.EX2 + +.SH LOGICAL OPERATORS + +We can combine terms with logical operators -- binary ones: \fBand\fR, +\fBor\fR, \fBxor\fR and the unary \fBnot\fR, with the conventional +rules for precedence and association, and are case-insensitive. + +.PP +You can also group things with \fB(\fR and \fB)\fR, so you can do +things like: +.EX1 +(subject:beethoven or subject:bach) and not body:elvis +.EX2 + +If you do not explicitly specify an operator between terms, \fBand\fR +is implied, so the queries +.EX1 +subject:chip subject:dale +.EX2 +.EX1 +subject:chip AND subject:dale +.EX2 +are equivalent. For readability, we recommend the second version. + +Note that a \fIpure not\fR - e.g. searching for \fBnot apples\fR is +quite a 'heavy' query. + +.SH REGULAR EXPRESSIONS AND WILDCARDS + +The language supports matching regular expressions that follow +ECMAScript; for details, see + +.BR http://www.cplusplus.com/reference/regex/ECMAScript/ + +Regular expressions must be enclosed in \fB//\fR. Some examples: +.EX1 +subject:/h.llo/ # match hallo, hello, ... +subject:/ +.EX2 + +Note the difference between 'maildir:/foo' and 'maildir:/foo/'; the +former matches messages in the '/foo' maildir, while the latter +matches all messages in all maildirs that match 'foo', such +as '/foo', '/bar/cuux/foo', '/fooishbar' etc. + +Wildcards are an older mechanism for matching where a term with a +rightmost \fB*\fR (and \fIonly\fR in that position) matches any term +that starts with the part before the \fB*\fR; they are supported for +backward compatibility and \fBmu\fR translates them to regular +expressions internally: +.EX1 +foo* +.EX2 +is equivalent to +.EX1 +/foo.*/ +.EX2 + +As a note of caution, certain wild-cards and regular expression can +take quite a bit longer than 'normal' queries. + +.SH FIELDS + +We already saw a number of search fields, such as \fBsubject:\fR and +\fBbody:\fR. Here is the full table, a shortcut character and a +description. +.EX1 + cc,c Cc (carbon-copy) recipient(s) + bcc,h Bcc (blind-carbon-copy) recipient(s) + from,f Message sender + to,t To: recipient(s) + subject,s Message subject + body,b Message body + maildir,m Maildir + msgid,i Message-ID + prio,p Message priority (\fIlow\fR, \fInormal\fR or \fIhigh\fR) + flag,g Message Flags + date,d Date range + size,z Message size range + embed,e Search inside embedded text parts + file,j Attachment filename + mime,y MIME-type of one or more message parts + tag,x Tags for the message + list,v Mailing list (e.g. the List-Id value) +.EX2 +The shortcut character can be used instead of the full name: +.EX1 +f:foo@bar +.EX2 +is the same as +.EX1 +from:foo@bar +.EX2 +For queries that are not one-off, we would recommend the longer name +for readability. + +There are also the special fields \fBcontact:\fR, which matches all +contact-fields (\fIfrom\fR, \fIto\fR, \fIcc\fR and \fIbcc\fR), and +\fBrecip\fR, which matches all recipient-fields (\fIto\fR, \fIcc\fR +and \fIbcc\fR). Hence, for instance, +.EX1 +contact:fnorb@example.com +.EX2 +is equivalent to +.EX1 +(from:fnorb@example.com or to:fnorb@example.com or + cc:from:fnorb@example.com or bcc:fnorb@example.com) +.EX2 + +.SH DATE RANGES + +The \fBdate:\fR field takes a date-range, expressed as the lower and +upper bound, separated by \fB..\fR. Either lower or upper (but not +both) can be omitted to create an open range. + +Dates are expressed in local time and using ISO-8601 format +(YYYY-MM-DD HH:MM:SS); you can leave out the right part, and \fBmu\fR +adds the rest, depending on whether this is the beginning or end of +the range (e.g., as a lower bound, '2015' would be interpreted as the +start of that year; as an upper bound as the end of the year). + +You can use '/' , '.', '-' and 'T' to make dates more human readable. + +Some examples: +.EX1 +date:20170505..20170602 +date:2017-05-05..2017-06-02 +date:..2017-10-01T12:00 +date:2015-06-01.. +date:2016..2016 +.EX2 + +You can also use the special 'dates' \fBnow\fR and \fBtoday\fR: +.EX1 +date:20170505..now +date:today.. +.EX2 + +Finally, you can use relative 'ago' times which express some time +before now and consist of a number followed by a unit, with units +\fBs\fR for seconds, \fBM\fR for minutes, \fBh\fR for hours, \fBd\fR +for days, \fBw\fR for week, \fBm\fR for months and \fBy\fR for years. +Some examples: + +.EX1 +date:3m.. +date:2017.01.01..5w +.EX2 + +.SH SIZE RANGES + +The \fBsize\fR or \fBz\fR field allows you to match \fIsize ranges\fR +-- that is, match messages that have a byte-size within a certain +range. Units (b (for bytes), K (for 1000 bytes) and M (for 1000 * 1000 +bytes) are supported). Some examples: + +.EX1 +size:10k..2m +size:10m.. +.EX2 + +.SH FLAG FIELDS + +The \fBflag\fR/\fBg\fR field allows you to match message flags. The +following fields are available: +.EX1 + a,attach Message with attachment + d,draft Draft Message + f,flagged Flagged + l,list Mailing-list message + n,new New message (in new/ Maildir) + p,passed Passed ('Handled') + r,replied Replied + s,seen Seen + t,trashed Marked for deletion + u,unread new OR NOT seen + x,encrypted Encrypted message + z,signed Signed message +.EX2 + +Some examples: +.EX1 +flag:attach +flag:replied +g:x +.EX2 + +Encrypted messages may be signed as well, but this is only visible +after decrypting and thus, invisible to \fBmu\fR. + +.SH PRIORITY FIELD + +The message priority field (\fBprio:\fR) has three possible values: +\fBlow\fR, \fBnormal\fR or \fBhigh\fR. For instance, to match +high-priority messages: +.EX1 + prio:high +.EX2 + +.SH MAILDIR + +The Maildir field describes the directory path starting \fBafter\fR +the Maildir-base path, and before the \fI/cur/\fR or \fI/new/\fR part. +So for example, if there's a message with the file name +\fI~/Maildir/lists/running/cur/1234.213:2,\fR, you could find it (and +all the other messages in the same maildir) with: +.EX1 +maildir:/lists/running +.EX2 + +Note the starting '/'. If you want to match mails in the 'root' +maildir, you can do with a single '/': +.EX1 +maildir:/ +.EX2 + +If you have maildirs (or any fields) that include spaces, you need to +quote them, ie. +.EX1 +maildir:"/Sent Items" +.EX2 + +Note that from the command-line, such queries must be quoted: +.EX1 +mu find 'maildir:"/Sent Items"' +.EX2 + +.SH MORE EXAMPLES + +Here are some simple examples of \fBmu\fR queries; you can make many +more complicated queries using various logical operators, parentheses +and so on, but in the author's experience, it's usually faster to find +a message with a simple query just searching for some words. + +Find all messages with both 'bee' and 'bird' (in any field) +.EX1 +bee AND bird +.EX2 + +Find all messages with either Frodo or Sam: +.EX1 +Frodo OR Sam +.EX2 + +Find all messages with the 'wombat' as subject, and 'capibara' anywhere: +.EX1 +subject:wombat and capibara +.EX2 + +Find all messages in the 'Archive' folder from Fred: +.EX1 +from:fred and maildir:/Archive +.EX2 + +Find all unread messages with attachments: +.EX1 +flag:attach and flag:unread +.EX2 + + +Find all messages with PDF-attachments: +.EX1 +mime:application/pdf +.EX2 + +Find all messages with attached images: +.EX1 +mime:image/* +.EX2 + +.SH CAVEATS + +With current Xapian versions, the apostroph character is considered +part of a word. Thus, you cannot find \fID'Artagnan\fR by searching +for \fIArtagnan\fR. So, include the apostroph in search or use a +regexp search. + +Matching on spaces has changed compared to the old query-parser; this +applies e.g. to Maildirs that have spaces in their name, such as +\fISent Items\fR. See \fBMAILDIR\fR above. + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu-find (1) diff --git a/man/mu-remove.1 b/man/mu-remove.1 new file mode 100644 index 0000000..36c25c2 --- /dev/null +++ b/man/mu-remove.1 @@ -0,0 +1,49 @@ +.TH MU REMOVE 1 "July 2012" "User Manuals" + +.SH NAME + +\fBmu remove\fR is the \fBmu\fR command to remove messages from the database. + +.SH SYNOPSIS + +.B mu remove [options] [] + +.SH DESCRIPTION + +\fBmu remove\fR removes specific messages from the database, each of them +specified by their filename. The files do not have to exist in the file +system. + +.SH OPTIONS + +\fBmu remove\fR does not have its own options, but the general options for +determining the location of the database (\fI--muhome\fR) are available. See +\fBmu-index(1)\fR for more information. + +.SH RETURN VALUE + +\fBmu remove\fR returns 0 upon success; in general, the following error codes are +returned: + +.nf +| code | meaning | +|------+-----------------------------------| +| 0 | ok | +| 1 | general error | +| 5 | some database update error | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR mu-add (1) diff --git a/man/mu-script.1 b/man/mu-script.1 new file mode 100644 index 0000000..aa4159b --- /dev/null +++ b/man/mu-script.1 @@ -0,0 +1,83 @@ +.TH MU SCRIPT 1 "June 2013" "User Manuals" + +.SH NAME + +mu script\- show the available mu scripts, and run them. + +.SH SYNOPSIS + +.B mu script [options] [] + +.B mu [] + +.SH DESCRIPTION + +\fBmu script\fR is the \fBmu\fR command to list available \fBmu\fR scripts. +The scripts are to be implemented in the Guile programming language, and +therefore only work if your \fBmu\fR is built with support for Guile. In +addition, many scripts require you to have \fBgnuplot\fR installed. + +Without any parameters, \fBmu script\fR lists the available scripts. If you +provide a pattern (a regular expression), only the scripts whose name or +one-line description match this pattern are listed. See the examples below. + +\fBmu\fR ships with a number of scripts. + +.SH OPTIONS + +.TP +\fB\-\-verbose\fR,\fB\-v\fR +when listing the available scripts, show the long descriptions. + +\fB\-\-\fR +all options on the right side of the \fB\-\-\fR are passed to the script. + +.SH EXAMPLES + +List all available scripts (one-line descriptions): +.nf + $ mu script +.fi + +List all available scripts matching \fImonth\fR (long descriptions): +.nf + $ mu script -v month +.fi + +Run the \fImsgs-per-month\fR script for messages matching 'hello', and pass it +the \fI--textonly\fR parameter: +.nf + $ mu msgs-per-month --query=hello --textonly +.fi + +.SH RETURN VALUE + +\fBmu script\fR returns 0 when all went well, and returns some non-zero error +code when this is not the case. + +.SH FILES + +You can make your own Scheme scripts accessible through \fBmu script\fR by +putting them in \fI/scripts\fR (which is typically +\fI~/.mu/scripts\fR). It is a good idea to document the scripts by using some +special comments in the source code: +.nf +;; INFO: this is my script -- one-line description +;; INFO: (longer description) +;; INFO: --option1= (describe option1) +;; INFO: etc. +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR guile (1) diff --git a/man/mu-server.1 b/man/mu-server.1 new file mode 100644 index 0000000..087483c --- /dev/null +++ b/man/mu-server.1 @@ -0,0 +1,61 @@ +.TH MU-SERVER 1 "January 2020" "User Manuals" + +.SH NAME + +mu server \- the mu backend for the mu4e e-mail client + +.SH SYNOPSIS + +.B mu server [options] + +.SH DESCRIPTION + +\fBmu server\fR starts a simple shell in which one can query and manipulate the +mu database. The output uses s-expressions. \fBmu server\fR is not meant for use +by humans, except for debugging purposes. Instead, it is designed specifically +for the \fBmu4e\fR e-mail client. + +In this man-page, we document the commands \fBmu server\fR accepts, as well as +their responses. In general, the commands sent to the server are s-expressions +of the form: + +.nf + ( :param1 value1 :param2 value2) +.fi + +For example, to view a certain message, the command would be: + +.nf + (view :docid 12345) +.fi + +Parameters can be sent in any order; they must be of the correct type though. +See \fBlib/utils/mu-sexp-parser.hh\fR and \fBlib/utils/mu-sexp-parser.cc\fR in +source-tree for the details. + + +.SH OUTPUT FORMAT + +\fBmu server\fR accepts a number of commands, and delivers its results in +the form: + +.nf + \\376\\377 +.fi + +\\376 (one byte 0xfe), followed by the length of the s-expression expressed as +an hexadecimal number, followed by another \\377 (one byte 0xff), followed by +the actual s-expression. + +By prefixing the expression with its length, it can be processed more +efficiently. The \\376 and \\377 were chosen since they never occur in valid +UTF-8 (in which the s-expressions are encoded). + +.sh COMMANDS + + +.SH AUTHOR +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu (1) diff --git a/man/mu-verify.1 b/man/mu-verify.1 new file mode 100644 index 0000000..38640f7 --- /dev/null +++ b/man/mu-verify.1 @@ -0,0 +1,79 @@ +.TH MU VERIFY 1 "June 2015" "User Manuals" + +.SH NAME + +mu verify\- verify message signatures and display information about them + +.SH SYNOPSIS + +.B mu verify [options] + +.SH DESCRIPTION + +\fBmu verify\fR is the \fBmu\fR command for verifying message signatures (such +as PGP/GPG signatures) and displaying information about them. The sub-command +works on message files, and does not require the message to be indexed in the +database. + +\fBmu verify\fR depends on \fBgpg\fR, and uses the one it finds in your +\fBPATH\fR. If you want to use another one, you need to set \fBMU_GPG_PATH\fB +to the full path to the desired \fBgpg\fR. + +.SH OPTIONS + +.TP +\fB\-r\fR, \fB\-\-auto\-retrieve\fR +attempt to find keys online (see the \fBauto-key-retrieve\fR option in the +\fBgnupg(1)\fR documentation). + +\" .TP +\" \fB\-u\fR, \fB\-\-use\-agent\fR attempt to use the GPG-agent (see the the +\" \fBgnupg-agent(1)\fR documentation). Note that GPG-agent is running many +\" desktop-evironment; you can check whether this is the case using: +\" .nf +\" $ env | grep GPG_AGENT_INFO +\" .fi + +.SH EXAMPLES + +To display aggregated (one-line) information about the signatures in a message: +.nf + $ mu verify msgfile +.fi + +To display information about all the signatures: +.nf + $ mu verify --verbose msgfile +.fi + +If you only want to use the exit code, you can use: +.nf + $ mu verify --quiet msgfile +.fi +which does not give any output. + +.SH RETURN VALUE + +\fBmu verify\fR returns 0 when all signatures could be verified to be good, +and returns some non-zero error code when this is not the case. + +.nf +| code | meaning | +|------+--------------------------------| +| 0 | ok | +| 1 | some non-verified signature(s) | +.fi + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR gpg (1) diff --git a/man/mu-view.1 b/man/mu-view.1 new file mode 100644 index 0000000..05ecd7d --- /dev/null +++ b/man/mu-view.1 @@ -0,0 +1,54 @@ +.TH MU VIEW 1 "June 2013" "User Manuals" + +.SH NAME + +mu view\- display an e-mail message file + +.SH SYNOPSIS + +.B mu view [options] [] + +.SH DESCRIPTION + +\fBmu view\fR is the \fBmu\fR command for displaying e-mail message files. It +works on message files and does \fInot\fR require the message to be indexed in +the database. + +The command shows some common headers (From:, To:, Cc:, Bcc:, Subject: and +Date:), the list of attachments and the plain-text body of the message (if +any). + +.SH OPTIONS + +.TP +\fB\-\-summary-len\fR=\fI\fR +instead of displaying the full message, output a summary based upon the first +\fI\fR lines of the message. + +.TP +\fB\-\-terminate\fR +terminate messages with \\f (\fIform-feed\fR) characters when displaying +them. This is useful when you want to further process them. + +.TP +\fB\-\-decrypt\fR +attempt to decrypt encrypted message bodies. This is only possible if \fBmu\fR +was built with crypto-support. Users are strongly recommended to use +\fBgpg-agent\fR; however, if needed, \fBmu\fR will request the user password +from the console. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" + +.BR mu (1), +.BR mu-index (1), +.BR gpg (1), +.BR gpg-agent (1) diff --git a/man/mu.1 b/man/mu.1 new file mode 100644 index 0000000..6998f33 --- /dev/null +++ b/man/mu.1 @@ -0,0 +1,182 @@ +.TH MU 1 "February 2020" "User Manuals" + +.SH NAME + +mu \- a set of tools to deal with Maildirs and message files, in particular to +index and search e-mail messages. + +.SH SYNOPSIS + +In alphabetical order: + +.B mu [options] +general mu command. + +.B mu add +add specific messages to the database. See +.BR mu-add(1) + +.B mu cfind [options] [] +find contacts. See +.BR mu-cfind(1) + +.B mu extract [options] [] [] +extract attachments and other MIME-parts. See +.BR mu-extract(1) + +.B mu find [options] +find messages. See +.BR mu-find(1) + +.B mu help [command] +get help for some command. See +.BR mu-help(1) + +.B mu index [options] +(re)index the messages in a Maildir. See +.BR mu-index(1) + +.B mu info [options] +show information about the mu database +.BR mu-info(1) + +.B mu init [options] +initialize the mu database +.BR mu-init(1) + +.B mu mkdir [options] [] +create a new Maildir. See +.BR mu-mkdir(1) + +.B mu remove [options] +remove specific messages from the database. See +.BR mu-remove(1) + +.B mu script [options] +run a mu (Guile) script. See +.BR mu-script(1) + +.B mu server [options] +start a server process (for \fBmu4e\fR-internal use). See +.BR mu-server(1) + +.B mu view [] +view a specific message. See +.BR mu-view(1) + +.SH DESCRIPTION + +\fBmu\fR is a set of tools for dealing with Maildirs and the e-mail messages +in them. + +\fBmu\fR's main purpose is to enable searching of e-mail messages. It +does so by periodically scanning a Maildir directory tree and +analyzing the e-mail messages found (this is called 'indexing'). The +results of this analysis are stored in a database, which can then be +queried. + +In addition to indexing and searching, \fBmu\fR also offers +functionality for viewing messages, extracting attachments and +creating maildirs, and searching and exporting contact information. + +\fBmu\fR can be used from the command line or can be integrated with various +e-mail clients. + +This manpage gives a general overview of the available commands +(\fBindex\fR, \fBfind\fR, etc.); each \fBmu\fR command has its own +man-page as well. + +.SH COLORS + +Some \fBmu\fR sub-commands support colorized output, and do so by +default. If you don't want colors, you can use \fI--nocolor\fR. + +Currently, \fBmu find\fR, \fBmu view\fR, \fBmu cfind\fR and \fBmu extract\fR +support colors. + +.SH ENCODING + +\fBmu\fR's output is in the current locale, with the exceptions of the output +specifically meant for output to UTF8-encoded files. In practice, this means +that the output of commands \fBindex\fR, \fBview\fR, +\fBextract\fR is always encoded according to the current locale. + +The same is true for \fBfind\fR and \fBcfind\fR, with some exceptions, where +the output is always UTF-8, regardless of the locale. + +For \fBcfind\fR the exception is \fI--format=bbdb\fR. This is hard-coded to +UTF-8, and as such specified in the output-file, so emacs/bbdb can handle it +correctly without guessing. + +For \fBfind\fR the output is encoded according the locale for +\fI--format=plain\fR (the default), and UTF-8 for all other formats +(\fIjson\fR, \fIsexp\fR, \fIxml\fR). + +.SH DATABASE AND FILE + +Commands \fBmu index\fR and \fBfind\fR and \fBcfind\fR work with the database, +while the other ones work on individual mail files. Hence, running \fBview\fR, +\fBmkdir\fR and \fBextract\fR does not require the mu database. + +The various commands are discussed in more detail in their own separate +man-pages; here the general options are discussed. + +.SH OPTIONS + +\fBmu\fR offers several general options that apply to all commands, +including \fBmu\fR without any command. + +.TP +\fB\-\-muhome\fR +use an alternative directory to store and read the database, write the logs, +etc. By default, \fBmu\fR uses XDG Base Directory Specification (e.g. on Linux +by default \fI~/.cache/mu\fR, \fI~/.config/mu\fR). Earlier versions of \fBmu\fR defaulted +to \fI~/.mu\fR, which now requires \fI\-\-muhome=~/.mu\fR. + +.TP +\fB\-d\fR, \fB\-\-debug\fR +makes \fBmu\fR generate extra debug information, +useful for debugging the program itself. By default, debug information goes to +the log file, \fI~/.cache/mu/mu.log\fR. It can safely be deleted when \fBmu\fR is +not running. When running with \fB--debug\fR option, the log file can grow +rather quickly. See the note on logging below. + +.TP +\fB\-q\fR, \fB\-\-quiet\fR +causes \fBmu\fR not to output informational +messages and progress information to standard output, but only to the log +file. Error messages will still be sent to standard error. Note that \fBmu +index\fR is \fBmuch\fR faster with \fB\-\-quiet\fR, so it is recommended you +use this option when using \fBmu\fR from scripts etc. + +.TP +\fB\-\-log-stderr\fR +causes \fBmu\fR to \fBnot\fR output log messages to standard error, in +addition to sending them to the log file. + +.TP +\fB\-v\fR, \fB\-\-version\fR +prints \fBmu\fR version and copyright information. + +.TP +\fB\-h\fR, \fB\-\-help\fR +lists the various command line options. + +.SH ERROR CODES + +The various mu subcommands typically exit with 0 (zero) upon success, and +non-zero when some error occurred. + +.SH BUGS + +Please report bugs if you find them: +.BR https://github.com/djcb/mu/issues + +.SH AUTHOR + +Dirk-Jan C. Binnema + +.SH "SEE ALSO" +.BR mu-index (1), mu-find (1), mu-cfind (1), mu-mkdir (1), mu-view (1), +.BR mu-extract (1), mu-easy (1), mu-bookmarks (5), mu-query (7) +.BR https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html diff --git a/mu/Makefile.am b/mu/Makefile.am new file mode 100644 index 0000000..b0f8d8a --- /dev/null +++ b/mu/Makefile.am @@ -0,0 +1,120 @@ +## Copyright (C) 2010-2020 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +include $(top_srcdir)/gtest.mk + +AM_CPPFLAGS= \ + -I${top_srcdir}/lib \ + $(GLIB_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) + +# don't use -Werror, as it might break on other compilers +# use -Wno-unused-parameters, because some callbacks may not +# really need all the params they get +AM_CFLAGS= \ + $(JSON_GLIB_CFLAGS) \ + $(ASAN_CFLAGS) \ + $(WARN_CFLAGS) \ + $(CODE_COVERAGE_CFLAGS) \ + -Wno-switch-enum \ + -DMU_SCRIPTS_DIR="\"$(pkgdatadir)/scripts/\"" + +AM_CXXFLAGS= \ + $(ASAN_CXXCFLAGS) \ + $(WARN_CXXFLAGS) \ + $(CODE_COVERAGE_CFLAGS) + +AM_LDFLAGS= \ + $(ASAN_LDFLAGS) + +bin_PROGRAMS= \ + mu + +# note, mu.cc is only '.cc' and not '.c' because libmu must explicitly +# be linked as c++, not c. +mu_SOURCES= \ + mu.cc \ + mu-cmd-cfind.c \ + mu-config.c \ + mu-config.h \ + mu-cmd-extract.c \ + mu-cmd-find.c \ + mu-cmd-index.c \ + mu-cmd-server.cc \ + mu-cmd-script.c \ + mu-cmd.c \ + mu-cmd.h + +BUILT_SOURCES= \ + mu-help-strings.h + +mu-help-strings.h: mu-help-strings.txt mu-help-strings.awk + $(AM_V_GEN) $(AWK) -f ${top_srcdir}/mu/mu-help-strings.awk < $< > $@ + +mu_LDADD= \ + ${top_builddir}/lib/libmu.la \ + ${top_builddir}/lib/utils/libmu-utils.la \ + $(GLIB_LIBS) \ + $(READLINE_LIBS) \ + $(CODE_COVERAGE_LIBS) + +EXTRA_DIST= \ + mu-help-strings.awk \ + mu-help-strings.txt + +noinst_PROGRAMS= $(TEST_PROGS) + +test_cflags= \ + ${AM_CFLAGS} \ + -DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \ + -DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \ + -DMU_TESTMAILDIR3=\"${abs_top_srcdir}/lib/testdir3\" \ + -DMU_TESTMAILDIR4=\"${abs_top_srcdir}/lib/testdir4\" \ + -DMU_PROGRAM=\"${abs_top_builddir}/mu/mu\" \ + -DABS_CURDIR=\"${abs_builddir}\" \ + -DABS_SRCDIR=\"${abs_srcdir}\" + +TEST_PROGS += test-mu-query +test_mu_query_SOURCES= test-mu-query.c dummy.cc +test_mu_query_CFLAGS=$(test_cflags) +test_mu_query_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS) + +TEST_PROGS += test-mu-cmd +test_mu_cmd_SOURCES= test-mu-cmd.c dummy.cc +test_mu_cmd_CFLAGS=$(test_cflags) +test_mu_cmd_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS) + +TEST_PROGS += test-mu-cmd-cfind +test_mu_cmd_cfind_SOURCES= test-mu-cmd-cfind.c dummy.cc +test_mu_cmd_cfind_CFLAGS=$(test_cflags) +test_mu_cmd_cfind_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS) + +TEST_PROGS += test-mu-threads +test_mu_threads_SOURCES= test-mu-threads.c dummy.cc +test_mu_threads_CFLAGS=$(test_cflags) +test_mu_threads_LDADD=${top_builddir}/lib/libtestmucommon.la $(CODE_COVERAGE_LIBS) + +# we need to use dummy.cc to enforce c++ linking... +BUILT_SOURCES+= \ + dummy.cc +dummy.cc: + touch dummy.cc + +TESTS=$(TEST_PROGS) +include $(top_srcdir)/aminclude_static.am + +CLEANFILES= \ + $(BUILT_SOURCES) diff --git a/mu/mu-cmd-cfind.c b/mu/mu-cmd-cfind.c new file mode 100644 index 0000000..4157bb0 --- /dev/null +++ b/mu/mu-cmd-cfind.c @@ -0,0 +1,470 @@ +/* +** Copyright (C) 2011-2019 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it under +** the terms of the GNU General Public License as published by the Free Software +** Foundation; either version 3, or (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, but WITHOUT +** ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +** FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +** details. +** +** You should have received a copy of the GNU General Public License along with +** this program; if not, write to the Free Software Foundation, Inc., 51 +** Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include +#include +#include +#include + +#include "mu-cmd.h" +#include "mu-contacts.hh" +#include "mu-runtime.h" + +#include "utils/mu-util.h" +#include "utils/mu-str.h" +#include "utils/mu-date.h" + +/** + * guess the last name for the given name; clearly, + * this is just a rough guess for setting an initial value. + * + * @param name a name + * + * @return the last name, as a newly allocated string (free with + * g_free) + */ +static gchar* +guess_last_name (const char *name) +{ + const gchar *lastsp; + + if (!name) + return g_strdup (""); + + lastsp = g_strrstr (name, " "); + + return g_strdup (lastsp ? lastsp + 1 : ""); +} + +/** + * guess the first name for the given name; clearly, + * this is just a rough guess for setting an initial value. + * + * @param name a name + * + * @return the first name, as a newly allocated string (free with + * g_free) + */ +static gchar* +guess_first_name (const char *name) +{ + const gchar *lastsp; + + if (!name) + return g_strdup (""); + + lastsp = g_strrstr (name, " "); + + if (lastsp) + return g_strndup (name, lastsp - name); + else + return g_strdup (name); +} + +/** + * guess some nick name for the given name; if we can determine an + * first name, last name, the nick will be first name + the first char + * of the last name. otherwise, it's just the first name. clearly, + * this is just a rough guess for setting an initial value for nicks. + * + * @param name a name + * + * @return the guessed nick, as a newly allocated string (free with g_free) + */ +static gchar* +cleanup_str (const char* str) +{ + gchar *s; + const gchar *cur; + unsigned i; + + if (mu_str_is_empty(str)) + return g_strdup (""); + + s = g_new0 (char, strlen(str) + 1); + + for (cur = str, i = 0; *cur; ++cur) { + if (ispunct(*cur) || isspace(*cur)) + continue; + else + s[i++] = *cur; + } + + return s; +} + +static char* +uniquify_nick (const char *nick, GHashTable *nicks) +{ + guint u; + + for (u = 2; u != 1000; ++u) { + char *cand; + cand = g_strdup_printf ("%s%u", nick, u); + if (!g_hash_table_contains (nicks, cand)) + return cand; + } + + return g_strdup (nick); /* if all else fails */ +} + + +static gchar* +guess_nick (const char* name, GHashTable *nicks) +{ + gchar *fname, *lname, *nick; + gchar initial[7]; + + fname = guess_first_name (name); + lname = guess_last_name (name); + + /* if there's no last name, use first name as the nick */ + if (mu_str_is_empty(fname) || mu_str_is_empty(lname)) { + g_free (lname); + nick = fname; + goto leave; + } + + memset (initial, 0, sizeof(initial)); + /* couldn't we get an initial for the last name? */ + if (g_unichar_to_utf8 (g_utf8_get_char (lname), initial) == 0) { + g_free (lname); + nick = fname; + goto leave; + } + + nick = g_strdup_printf ("%s%s", fname, initial); + g_free (fname); + g_free (lname); + +leave: + { + gchar *tmp; + tmp = cleanup_str (nick); + g_free (nick); + nick = tmp; + } + + if (g_hash_table_contains (nicks, nick)) { + char *tmp; + tmp = uniquify_nick (nick, nicks); + g_free (nick); + nick = tmp; + } + + g_hash_table_add (nicks, g_strdup(nick)); + + return nick; +} + + + +static void +print_header (MuConfigFormat format) +{ + switch (format) { + case MU_CONFIG_FORMAT_BBDB: + g_print (";; -*-coding: utf-8-emacs;-*-\n" + ";;; file-version: 6\n"); + break; + case MU_CONFIG_FORMAT_MUTT_AB: + g_print ("Matching addresses in the mu database:\n"); + break; + default: + break; + } +} + +static void +each_contact_bbdb (const char *email, const char *name, time_t tstamp) +{ + char *fname, *lname, *now, *timestamp; + + fname = guess_first_name (name); + lname = guess_last_name (name); + now = mu_date_str ("%Y-%m-%d", time(NULL)); + timestamp = mu_date_str ("%Y-%m-%d", tstamp); + + g_print ("[\"%s\" \"%s\" nil nil nil nil (\"%s\") " + "((creation-date . \"%s\") (time-stamp . \"%s\")) nil]\n", + fname, lname, email, now, timestamp); + + g_free (now); + g_free (timestamp); + g_free (fname); + g_free (lname); +} + + +static void +each_contact_mutt_alias (const char *email, const char *name, + GHashTable *nicks) +{ + + gchar *nick; + + if (!name) + return; + + nick = guess_nick (name, nicks); + mu_util_print_encoded ("alias %s %s <%s>\n", + nick, name, email); + g_free (nick); + +} + + +static void +each_contact_wl (const char *email, const char *name, GHashTable *nicks) +{ + gchar *nick; + + if (!name) + return; + + nick = guess_nick (name, nicks); + mu_util_print_encoded ("%s \"%s\" \"%s\"\n", + email, nick, name); + g_free (nick); +} + + +static void +each_contact_org_contact (const char *email, const char *name) +{ + if (name) + mu_util_print_encoded ( + "* %s\n:PROPERTIES:\n:EMAIL: %s\n:END:\n\n", + name, email); +} + + +static void +print_csv_field (const char *str) +{ + char *s; + + if (!str) + return; + + s = mu_str_replace (str, "\"", "\"\""); + if (strchr (s, ',')) + mu_util_print_encoded ("\"%s\"", s); + else + mu_util_print_encoded ("%s", s); + + g_free (s); +} + +static void +each_contact_csv (const char *email, const char *name) +{ + print_csv_field (name); + mu_util_print_encoded (","); + print_csv_field (email); + mu_util_print_encoded ("\n"); +} + + + +static void +print_plain (const char *email, const char *name, gboolean color) +{ + if (name) { + if (color) fputs (MU_COLOR_MAGENTA, stdout); + mu_util_fputs_encoded (name, stdout); + fputs (" ", stdout); + } + + if (color) + fputs (MU_COLOR_GREEN, stdout); + + mu_util_fputs_encoded (email, stdout); + + if (color) + fputs (MU_COLOR_DEFAULT, stdout); + + fputs ("\n", stdout); +} + +typedef struct { + MuConfigFormat format; + gboolean color, personal; + time_t after; + GRegex *rx; + GHashTable *nicks; + size_t n; +} ECData; + + +static void +each_contact (const char *full_address, + const char *email, const char *name, gboolean personal, + time_t last_seen, size_t freq, gint64 tstamp, + ECData *ecdata) +{ + if (ecdata->personal && !personal) + return; + + if (tstamp < ecdata->after) + return; + + if (ecdata->rx && + !g_regex_match (ecdata->rx, email, 0, NULL) && + !g_regex_match (ecdata->rx, name ? name : "", 0, NULL)) + return; + + ++ecdata->n; + + switch (ecdata->format) { + case MU_CONFIG_FORMAT_MUTT_ALIAS: + each_contact_mutt_alias (email, name, ecdata->nicks); + break; + case MU_CONFIG_FORMAT_MUTT_AB: + mu_util_print_encoded ("%s\t%s\t\n", + email, name ? name : ""); + break; + case MU_CONFIG_FORMAT_WL: + each_contact_wl (email, name, ecdata->nicks); + break; + case MU_CONFIG_FORMAT_ORG_CONTACT: + each_contact_org_contact (email, name); + break; + case MU_CONFIG_FORMAT_BBDB: + each_contact_bbdb (email, name, last_seen); + break; + case MU_CONFIG_FORMAT_CSV: + each_contact_csv (email, name); + break; + case MU_CONFIG_FORMAT_DEBUG: { + char datebuf[32]; + strftime(datebuf, sizeof(datebuf), "%F %T", + gmtime(&last_seen)); + g_print ("%s\n\tname: %s\n\t%s\n\tpersonal: %s\n\tfreq: %zu\n" + "\tlast-seen: %s\n", + email, + name ? name : "", + full_address, + personal ? "yes" : "no", + freq, + datebuf); + } break; + default: + print_plain (email, name, ecdata->color); + } +} + + +static MuError +run_cmd_cfind (MuStore *store, + const char* pattern, + gboolean personal, + time_t after, + MuConfigFormat format, + gboolean color, + GError **err) +{ + gboolean rv; + ECData ecdata; + + memset(&ecdata, 0, sizeof(ecdata)); + + if (pattern) { + ecdata.rx = g_regex_new (pattern, + G_REGEX_CASELESS|G_REGEX_OPTIMIZE, + 0, err); + if (!ecdata.rx) + return MU_ERROR_CONTACTS; + } + + ecdata.personal = personal; + ecdata.n = 0; + ecdata.after = after; + ecdata.format = format; + ecdata.color = color; + ecdata.nicks = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + + print_header (format); + rv = mu_contacts_foreach (mu_store_contacts(store), + (MuContactsForeachFunc)each_contact, &ecdata); + g_hash_table_unref (ecdata.nicks); + + if (ecdata.rx) + g_regex_unref (ecdata.rx); + + if (ecdata.n == 0) { + g_warning ("no matching contacts found"); + return MU_ERROR_NO_MATCHES; + } + + return rv ? MU_OK : MU_ERROR_CONTACTS; +} + +static gboolean +cfind_params_valid (MuConfig *opts) +{ + switch (opts->format) { + case MU_CONFIG_FORMAT_PLAIN: + case MU_CONFIG_FORMAT_MUTT_ALIAS: + case MU_CONFIG_FORMAT_MUTT_AB: + case MU_CONFIG_FORMAT_WL: + case MU_CONFIG_FORMAT_BBDB: + case MU_CONFIG_FORMAT_CSV: + case MU_CONFIG_FORMAT_ORG_CONTACT: + case MU_CONFIG_FORMAT_DEBUG: + break; + default: + g_warning ("invalid output format %s", + opts->formatstr ? opts->formatstr : ""); + return FALSE; + } + + /* only one pattern allowed */ + if (opts->params[1] && opts->params[2]) { + g_warning ("usage: mu cfind [options] []"); + return FALSE; + } + + return TRUE; +} + +MuError +mu_cmd_cfind (MuStore *store, MuConfig *opts, GError **err) +{ + g_return_val_if_fail (store, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_CFIND, + MU_ERROR_INTERNAL); + + if (!cfind_params_valid (opts)) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS, + "invalid parameters"); + return MU_ERROR_IN_PARAMETERS; + } + + return run_cmd_cfind (store, + opts->params[1], + opts->personal, + opts->after, + opts->format, + !opts->nocolor, + err); +} diff --git a/mu/mu-cmd-extract.c b/mu/mu-cmd-extract.c new file mode 100644 index 0000000..f0b90bc --- /dev/null +++ b/mu/mu-cmd-extract.c @@ -0,0 +1,428 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2010-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include + +#include "mu-msg.h" +#include "mu-msg-part.h" +#include "mu-cmd.h" +#include "utils/mu-util.h" +#include "utils/mu-str.h" + + +static gboolean +save_part (MuMsg *msg, const char *targetdir, guint partidx, MuConfig *opts) +{ + GError *err; + gchar *filepath; + gboolean rv; + MuMsgOptions msgopts; + + err = NULL; + rv = FALSE; + + msgopts = mu_config_get_msg_options (opts); + + filepath = mu_msg_part_get_path (msg, msgopts, targetdir, partidx, &err); + if (!filepath) + goto exit; + + if (!mu_msg_part_save (msg, msgopts, filepath, partidx, &err)) + goto exit; + + if (opts->play) + rv = mu_util_play (filepath, TRUE, FALSE, &err); + else + rv = TRUE; +exit: + if (err) { + g_warning ("error with MIME-part: %s", err->message); + g_clear_error (&err); + } + + g_free (filepath); + return rv; +} + + + +static gboolean +save_numbered_parts (MuMsg *msg, MuConfig *opts) +{ + gboolean rv; + char **parts, **cur; + + parts = g_strsplit (opts->parts, ",", 0); + + for (rv = TRUE, cur = parts; cur && *cur; ++cur) { + + unsigned idx; + int i; + char *endptr; + + idx = (unsigned)(i = strtol (*cur, &endptr, 10)); + if (i < 0 || *cur == endptr) { + g_warning ("invalid MIME-part index '%s'", *cur); + rv = FALSE; + break; + } + + if (!save_part (msg, opts->targetdir, idx, opts)) { + g_warning ("failed to save MIME-part %d", idx); + rv = FALSE; + break; + } + } + + g_strfreev (parts); + return rv; +} + +static GRegex* +anchored_regex (const char* pattern) +{ + GRegex *rx; + GError *err; + gchar *anchored; + + + anchored = g_strdup_printf + ("%s%s%s", + pattern[0] == '^' ? "" : "^", + pattern, + pattern[strlen(pattern)-1] == '$' ? "" : "$"); + + err = NULL; + rx = g_regex_new (anchored, G_REGEX_CASELESS|G_REGEX_OPTIMIZE, 0, + &err); + g_free (anchored); + + if (!rx) { + g_warning ("error in regular expression '%s': %s", + pattern, err->message ? err->message : "error"); + g_error_free (err); + return NULL; + } + + return rx; +} + + +static gboolean +save_part_with_filename (MuMsg *msg, const char *pattern, MuConfig *opts) +{ + GSList *lst, *cur; + GRegex *rx; + gboolean rv; + MuMsgOptions msgopts; + + msgopts = mu_config_get_msg_options (opts); + + /* 'anchor' the pattern with '^...$' if not already */ + rx = anchored_regex (pattern); + if (!rx) + return FALSE; + + lst = mu_msg_find_files (msg, msgopts, rx); + g_regex_unref (rx); + if (!lst) { + g_warning ("no matching attachments found"); + return FALSE; + } + + for (cur = lst, rv = TRUE; cur; cur = g_slist_next (cur)) + rv = rv && save_part (msg, opts->targetdir, + GPOINTER_TO_UINT(cur->data), opts); + g_slist_free (lst); + + return rv; +} + +struct _SaveData { + gboolean result; + guint saved_num; + MuConfig *opts; +}; +typedef struct _SaveData SaveData; + + +static gboolean +ignore_part (MuMsg *msg, MuMsgPart *part, SaveData *sd) +{ + /* something went wrong somewhere; stop */ + if (!sd->result) + return TRUE; + + /* only consider leaf parts */ + if (!(part->part_type & MU_MSG_PART_TYPE_LEAF)) + return TRUE; + + /* filter out non-attachments? */ + if (!sd->opts->save_all && + !(mu_msg_part_maybe_attachment (part))) + return TRUE; + + return FALSE; +} + + +static void +save_part_if (MuMsg *msg, MuMsgPart *part, SaveData *sd) +{ + gchar *filepath; + gboolean rv; + GError *err; + MuMsgOptions msgopts; + + if (ignore_part (msg, part, sd)) + return; + + rv = FALSE; + filepath = NULL; + err = NULL; + + msgopts = mu_config_get_msg_options (sd->opts); + filepath = mu_msg_part_get_path (msg, msgopts, + sd->opts->targetdir, + part->index, &err); + if (!filepath) + goto exit; + + if (!mu_msg_part_save (msg, msgopts, filepath, part->index, &err)) + goto exit; + + if (sd->opts->play) + rv = mu_util_play (filepath, TRUE, FALSE, &err); + else + rv = TRUE; + + ++sd->saved_num; +exit: + if (err) + g_warning ("error saving MIME part: %s", err->message); + + g_free (filepath); + g_clear_error (&err); + + sd->result = rv; + +} + +static gboolean +save_certain_parts (MuMsg *msg, MuConfig *opts) +{ + SaveData sd; + MuMsgOptions msgopts; + + sd.result = TRUE; + sd.saved_num = 0; + sd.opts = opts; + + msgopts = mu_config_get_msg_options (opts); + mu_msg_part_foreach (msg, msgopts, + (MuMsgPartForeachFunc)save_part_if, &sd); + + if (sd.saved_num == 0) { + g_warning ("no %s extracted from this message", + opts->save_attachments ? "attachments" : "parts"); + sd.result = FALSE; + } + + return sd.result; +} + + +static gboolean +save_parts (const char *path, const char *filename, MuConfig *opts) +{ + MuMsg* msg; + gboolean rv; + GError *err; + + err = NULL; + msg = mu_msg_new_from_file (path, NULL, &err); + if (!msg) { + if (err) { + g_warning ("error: %s", err->message); + g_error_free (err); + } + return FALSE; + } + + /* note, mu_cmd_extract already checks whether what's in opts + * is somewhat, so no need for extensive checking here */ + + /* should we save some explicit parts? */ + if (opts->parts) + rv = save_numbered_parts (msg, opts); + else if (filename) + rv = save_part_with_filename (msg, filename, opts); + else + rv = save_certain_parts (msg, opts); + + mu_msg_unref (msg); + + return rv; +} + +#define color_maybe(C) do{ if (color) fputs ((C),stdout);}while(0) + +static const char* +disp_str (MuMsgPartType ptype) +{ + if (ptype & MU_MSG_PART_TYPE_ATTACHMENT) + return "attach"; + if (ptype & MU_MSG_PART_TYPE_INLINE) + return "inline"; + return ""; +} + +static void +each_part_show (MuMsg *msg, MuMsgPart *part, gboolean color) +{ + /* index */ + g_print (" %u ", part->index); + + /* filename */ + color_maybe (MU_COLOR_GREEN); { + gchar *fname; + fname = mu_msg_part_get_filename (part, FALSE); + mu_util_fputs_encoded (fname ? fname : "", stdout); + g_free (fname); + } + /* content-type */ + color_maybe (MU_COLOR_BLUE); + mu_util_print_encoded ( + " %s/%s ", + part->type ? part->type : "", + part->subtype ? part->subtype : ""); + + /* /\* disposition *\/ */ + color_maybe (MU_COLOR_MAGENTA); + mu_util_print_encoded ("[%s]", disp_str(part->part_type)); + + /* size */ + if (part->size > 0) { + color_maybe (MU_COLOR_CYAN); + g_print (" (%s)", mu_str_size_s (part->size)); + } + + color_maybe (MU_COLOR_DEFAULT); + fputs ("\n", stdout); +} + + +static gboolean +show_parts (const char* path, MuConfig *opts, GError **err) +{ + MuMsg *msg; + MuMsgOptions msgopts; + + msg = mu_msg_new_from_file (path, NULL, err); + if (!msg) + return FALSE; + + msgopts = mu_config_get_msg_options (opts); + + /* TODO: update this for crypto */ + g_print ("MIME-parts in this message:\n"); + mu_msg_part_foreach + (msg, msgopts, + (MuMsgPartForeachFunc)each_part_show, + GUINT_TO_POINTER(!opts->nocolor)); + + mu_msg_unref (msg); + + return TRUE; + +} + + +static gboolean +check_params (MuConfig *opts, GError **err) +{ + size_t param_num; + + param_num = mu_config_param_num (opts); + + if (param_num < 2) { + mu_util_g_set_error + (err, MU_ERROR_IN_PARAMETERS, + "parameters missing"); + return FALSE; + } + + if (opts->save_attachments || opts->save_all) + if (opts->parts || param_num == 3) { + mu_util_g_set_error + (err, MU_ERROR_IN_PARAMETERS, + "--save-attachments and --save-all don't " + "accept a filename pattern or --parts"); + return FALSE; + } + + if (opts->save_attachments && opts->save_all) { + mu_util_g_set_error + (err, MU_ERROR_IN_PARAMETERS, + "only one of --save-attachments and" + " --save-all is allowed"); + return FALSE; + } + + return TRUE; +} + +MuError +mu_cmd_extract (MuConfig *opts, GError **err) +{ + int rv; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_EXTRACT, + MU_ERROR_INTERNAL); + + if (!check_params (opts, err)) + return MU_ERROR_IN_PARAMETERS; + + if (!opts->params[2] && !opts->parts && + !opts->save_attachments && !opts->save_all) + /* show, don't save */ + rv = show_parts (opts->params[1], opts, err); + else { + rv = mu_util_check_dir(opts->targetdir, FALSE, TRUE); + if (!rv) + mu_util_g_set_error + (err, MU_ERROR_FILE_CANNOT_WRITE, + "target '%s' is not a writable directory", + opts->targetdir); + else + rv = save_parts (opts->params[1], + opts->params[2], + opts); /* save */ + } + + return rv ? MU_OK : MU_ERROR; +} diff --git a/mu/mu-cmd-find.c b/mu/mu-cmd-find.c new file mode 100644 index 0000000..47e2d13 --- /dev/null +++ b/mu/mu-cmd-find.c @@ -0,0 +1,768 @@ +/* +** Copyright (C) 2008-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include +#include +#include +#include +#include + +#include "mu-msg.h" +#include "mu-maildir.h" +#include "mu-index.h" +#include "mu-query.h" +#include "mu-msg-iter.h" +#include "mu-bookmarks.h" +#include "mu-runtime.h" + +#include "utils/mu-util.h" +#include "utils/mu-str.h" +#include "utils/mu-date.h" + +#include "mu-cmd.h" +#include "mu-threader.h" + +#ifdef HAVE_JSON_GLIB +#include +#endif /*HAVE_JSON_GLIB*/ + +typedef gboolean (OutputFunc) (MuMsg *msg, MuMsgIter *iter, + MuConfig *opts, GError **err); + +static gboolean +print_internal (MuQuery *query, const gchar *expr, gboolean xapian, + gboolean warn, GError **err) +{ + char *str; + + if (xapian) + str = mu_query_internal_xapian (query, expr, err); + else + str = mu_query_internal (query, expr, warn, err); + + if (str) { + g_print ("%s\n", str); + g_free (str); + } + + return str != NULL; +} + + +/* returns MU_MSG_FIELD_ID_NONE if there is an error */ +static MuMsgFieldId +sort_field_from_string (const char* fieldstr, GError **err) +{ + MuMsgFieldId mfid; + + mfid = mu_msg_field_id_from_name (fieldstr, FALSE); + + /* not found? try a shortcut */ + if (mfid == MU_MSG_FIELD_ID_NONE && + strlen(fieldstr) == 1) + mfid = mu_msg_field_id_from_shortcut(fieldstr[0], + FALSE); + if (mfid == MU_MSG_FIELD_ID_NONE) + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS, + "not a valid sort field: '%s'\n", fieldstr); + return mfid; +} + +static MuMsg* +get_message (MuMsgIter *iter, time_t after) +{ + MuMsg *msg; + + if (mu_msg_iter_is_done (iter)) + return NULL; + + msg = mu_msg_iter_get_msg_floating (iter); + if (!msg) + return NULL; /* error */ + + if (!mu_msg_is_readable (msg)) { + mu_msg_iter_next (iter); + return get_message (iter, after); + } + + if (after != 0 && after > mu_msg_get_timestamp (msg)) { + mu_msg_iter_next (iter); + return get_message (iter, after); + } + + return msg; +} + +static MuMsgIter* +run_query (MuQuery *xapian, const gchar *query, MuConfig *opts, GError **err) +{ + MuMsgIter *iter; + MuMsgFieldId sortid; + MuQueryFlags qflags; + + sortid = MU_MSG_FIELD_ID_NONE; + if (opts->sortfield) { + sortid = sort_field_from_string (opts->sortfield, err); + if (sortid == MU_MSG_FIELD_ID_NONE) /* error occurred? */ + return FALSE; + } + + qflags = MU_QUERY_FLAG_NONE; + if (opts->reverse) + qflags |= MU_QUERY_FLAG_DESCENDING; + if (opts->skip_dups) + qflags |= MU_QUERY_FLAG_SKIP_DUPS; + if (opts->include_related) + qflags |= MU_QUERY_FLAG_INCLUDE_RELATED; + if (opts->threads) + qflags |= MU_QUERY_FLAG_THREADS; + + iter = mu_query_run (xapian, query, sortid, opts->maxnum, qflags, err); + return iter; +} + +static gboolean +exec_cmd (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ + gint status; + char *cmdline, *escpath; + gboolean rv; + + escpath = g_shell_quote (mu_msg_get_path (msg)); + cmdline = g_strdup_printf ("%s %s", opts->exec, escpath); + + rv = g_spawn_command_line_sync (cmdline, NULL, NULL, &status, err); + + g_free (cmdline); + g_free (escpath); + + return rv; +} + +static gchar* +resolve_bookmark (MuConfig *opts, GError **err) +{ + MuBookmarks *bm; + char* val; + const gchar *bmfile; + + bmfile = mu_runtime_path (MU_RUNTIME_PATH_BOOKMARKS); + bm = mu_bookmarks_new (bmfile); + if (!bm) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_FILE_CANNOT_OPEN, + "failed to open bookmarks file '%s'", bmfile); + return FALSE; + } + + val = (gchar*)mu_bookmarks_lookup (bm, opts->bookmark); + if (!val) + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_NO_MATCHES, + "bookmark '%s' not found", opts->bookmark); + else + val = g_strdup (val); + + mu_bookmarks_destroy (bm); + return val; +} + +static gchar* +get_query (MuConfig *opts, GError **err) +{ + gchar *query, *bookmarkval; + + /* params[0] is 'find', actual search params start with [1] */ + if (!opts->bookmark && !opts->params[1]) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS, + "error in parameters"); + return NULL; + } + + bookmarkval = NULL; + if (opts->bookmark) { + bookmarkval = resolve_bookmark (opts, err); + if (!bookmarkval) + return NULL; + } + + query = g_strjoinv (" ", &opts->params[1]); + if (bookmarkval) { + gchar *tmp; + tmp = g_strdup_printf ("%s %s", bookmarkval, query); + g_free (query); + query = tmp; + } + + g_free (bookmarkval); + + return query; +} + +static MuQuery* +get_query_obj (MuStore *store, GError **err) +{ + MuQuery *mquery; + unsigned count; + + count = mu_store_count (store, err); + + if (count == (unsigned)-1) + return NULL; + + if (count == 0) { + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_XAPIAN_NEEDS_REINDEX, + "the database is empty"); + return NULL; + } + + mquery = mu_query_new (store, err); + if (!mquery) + return NULL; + + return mquery; +} + +static gboolean +prepare_links (MuConfig *opts, GError **err) +{ + /* note, mu_maildir_mkdir simply ignores whatever part of the + * mail dir already exists */ + + if (!mu_maildir_mkdir (opts->linksdir, 0700, TRUE, err)) { + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_MKDIR, + "error creating %s", opts->linksdir); + return FALSE; + } + + if (opts->clearlinks && + !mu_maildir_clear_links (opts->linksdir, err)) { + mu_util_g_set_error (err, MU_ERROR_FILE, + "error clearing links under %s", + opts->linksdir); + return FALSE; + } + + return TRUE; +} + +static gboolean +output_link (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ + if (mu_msg_iter_is_first (iter) && !prepare_links (opts, err)) + return FALSE; + + return mu_maildir_link (mu_msg_get_path (msg), + opts->linksdir, err); +} + +static void +ansi_color_maybe (MuMsgFieldId mfid, gboolean color) +{ + const char* ansi; + + if (!color) + return; /* nothing to do */ + + switch (mfid) { + + case MU_MSG_FIELD_ID_FROM: + ansi = MU_COLOR_CYAN; break; + + case MU_MSG_FIELD_ID_TO: + case MU_MSG_FIELD_ID_CC: + case MU_MSG_FIELD_ID_BCC: + ansi = MU_COLOR_BLUE; break; + + case MU_MSG_FIELD_ID_SUBJECT: + ansi = MU_COLOR_GREEN; break; + + case MU_MSG_FIELD_ID_DATE: + ansi = MU_COLOR_MAGENTA; break; + + default: + if (mu_msg_field_type(mfid) == MU_MSG_FIELD_TYPE_STRING) + ansi = MU_COLOR_YELLOW; + else + ansi = MU_COLOR_RED; + } + + fputs (ansi, stdout); +} + +static void +ansi_reset_maybe (MuMsgFieldId mfid, gboolean color) +{ + if (!color) + return; /* nothing to do */ + + fputs (MU_COLOR_DEFAULT, stdout); + +} + +static const char* +field_string_list (MuMsg *msg, MuMsgFieldId mfid) +{ + char *str; + const GSList *lst; + static char buf[80]; + + lst = mu_msg_get_field_string_list (msg, mfid); + if (!lst) + return NULL; + + str = mu_str_from_list (lst, ','); + if (str) { + strncpy (buf, str, sizeof(buf)-1); + buf[sizeof(buf)-1]='\0'; + g_free (str); + return buf; + } + + return NULL; +} + +static const char* +display_field (MuMsg *msg, MuMsgFieldId mfid) +{ + gint64 val; + + switch (mu_msg_field_type(mfid)) { + case MU_MSG_FIELD_TYPE_STRING: { + const gchar *str; + str = mu_msg_get_field_string (msg, mfid); + return str ? str : ""; + } + case MU_MSG_FIELD_TYPE_INT: + + if (mfid == MU_MSG_FIELD_ID_PRIO) { + val = mu_msg_get_field_numeric (msg, mfid); + return mu_msg_prio_name ((MuMsgPrio)val); + } else if (mfid == MU_MSG_FIELD_ID_FLAGS) { + val = mu_msg_get_field_numeric (msg, mfid); + return mu_str_flags_s ((MuFlags)val); + } else /* as string */ + return mu_msg_get_field_string (msg, mfid); + + case MU_MSG_FIELD_TYPE_TIME_T: + val = mu_msg_get_field_numeric (msg, mfid); + return mu_date_str_s ("%c", (time_t)val); + + case MU_MSG_FIELD_TYPE_BYTESIZE: + val = mu_msg_get_field_numeric (msg, mfid); + return mu_str_size_s ((unsigned)val); + case MU_MSG_FIELD_TYPE_STRING_LIST: { + const char *str; + str = field_string_list (msg, mfid); + return str ? str : ""; + } + default: + g_return_val_if_reached (NULL); + } +} + +static void +print_summary (MuMsg *msg, MuConfig *opts) +{ + const char* body; + char *summ; + MuMsgOptions msgopts; + + msgopts = mu_config_get_msg_options (opts); + body = mu_msg_get_body_text(msg, msgopts); + + if (body) + summ = mu_str_summarize (body, (unsigned)opts->summary_len); + else + summ = NULL; + + g_print ("Summary: "); + mu_util_fputs_encoded (summ ? summ : "", stdout); + g_print ("\n"); + + g_free (summ); +} + +static void +thread_indent (MuMsgIter *iter) +{ + const MuMsgIterThreadInfo *ti; + const char* threadpath; + int i; + gboolean is_root, first_child, empty_parent, is_dup; + + ti = mu_msg_iter_get_thread_info (iter); + if (!ti) { + g_warning ("cannot get thread-info for message %u", + mu_msg_iter_get_docid (iter)); + return; + } + + threadpath = ti->threadpath; + /* fputs (threadpath, stdout); */ + /* fputs (" ", stdout); */ + + is_root = ti->prop & MU_MSG_ITER_THREAD_PROP_ROOT; + first_child = ti->prop & MU_MSG_ITER_THREAD_PROP_FIRST_CHILD; + empty_parent = ti->prop & MU_MSG_ITER_THREAD_PROP_EMPTY_PARENT; + is_dup = ti->prop & MU_MSG_ITER_THREAD_PROP_DUP; + + /* FIXME: count the colons... */ + for (i = 0; *threadpath; ++threadpath) + i += (*threadpath == ':') ? 1 : 0; + + /* indent */ + while (i --> 0) + fputs (" ", stdout); + + if (!is_root) { + fputs (first_child ? "`" : "|", stdout); + fputs (empty_parent ? "*> " : is_dup ? "=> " : "-> ", stdout); + } +} + +static void +output_plain_fields (MuMsg *msg, const char *fields, + gboolean color, gboolean threads) +{ + const char* myfields; + int nonempty; + + g_return_if_fail (fields); + + for (myfields = fields, nonempty = 0; *myfields; ++myfields) { + + MuMsgFieldId mfid; + mfid = mu_msg_field_id_from_shortcut (*myfields, FALSE); + + if (mfid == MU_MSG_FIELD_ID_NONE || + (!mu_msg_field_xapian_value (mfid) && + !mu_msg_field_xapian_contact (mfid))) + nonempty += printf ("%c", *myfields); + + else { + ansi_color_maybe (mfid, color); + nonempty += mu_util_fputs_encoded + (display_field (msg, mfid), stdout); + ansi_reset_maybe (mfid, color); + } + } + + if (nonempty) + fputs ("\n", stdout); +} + +static gboolean +output_plain (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ + /* we reuse the color (whatever that may be) + * for message-priority for threads, too */ + ansi_color_maybe (MU_MSG_FIELD_ID_PRIO, !opts->nocolor); + if (opts->threads) + thread_indent (iter); + + output_plain_fields (msg, opts->fields, !opts->nocolor, opts->threads); + + if (opts->summary_len > 0) + print_summary (msg, opts); + + return TRUE; +} + +static gboolean +output_sexp (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ + char *sexp; + const MuMsgIterThreadInfo *ti; + + ti = opts->threads ? mu_msg_iter_get_thread_info (iter) : NULL; + sexp = mu_msg_to_sexp (msg, mu_msg_iter_get_docid (iter), + ti, MU_MSG_OPTION_HEADERS_ONLY); + fputs (sexp, stdout); + g_free (sexp); + + return TRUE; +} + +static gboolean +output_json (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ +#ifdef HAVE_JSON_GLIB + JsonNode *node; + const MuMsgIterThreadInfo *ti; + char *s; + + if (mu_msg_iter_is_first(iter)) + g_print ("[\n"); + + ti = opts->threads ? mu_msg_iter_get_thread_info (iter) : NULL; + node = mu_msg_to_json (msg, mu_msg_iter_get_docid (iter), + ti, MU_MSG_OPTION_HEADERS_ONLY); + + s = json_to_string (node, TRUE); + json_node_free (node); + + fputs (s, stdout); + g_free (s); + + if (mu_msg_iter_is_last(iter)) + fputs("]\n", stdout); + else + fputs (",\n", stdout); + + return TRUE; +#else + g_set_error (err, MU_ERROR_DOMAIN, MU_ERROR_IN_PARAMETERS, + "this mu was built without json support"); + return FALSE; +#endif /*HAVE_JSON_GLIB*/ + +} + +static void +print_attr_xml (const char* elm, const char *str) +{ + gchar *esc; + + if (mu_str_is_empty(str)) + return; /* empty: don't include */ + + esc = g_markup_escape_text (str, -1); + g_print ("\t\t<%s>%s\n", elm, esc, elm); + g_free (esc); +} + +static gboolean +output_xml (MuMsg *msg, MuMsgIter *iter, MuConfig *opts, GError **err) +{ + if (mu_msg_iter_is_first(iter)) { + g_print ("\n"); + g_print ("\n"); + } + + g_print ("\t\n"); + print_attr_xml ("from", mu_msg_get_from (msg)); + print_attr_xml ("to", mu_msg_get_to (msg)); + print_attr_xml ("cc", mu_msg_get_cc (msg)); + print_attr_xml ("subject", mu_msg_get_subject (msg)); + g_print ("\t\t%u\n", + (unsigned)mu_msg_get_date (msg)); + g_print ("\t\t%u\n", (unsigned)mu_msg_get_size (msg)); + print_attr_xml ("msgid", mu_msg_get_msgid (msg)); + print_attr_xml ("path", mu_msg_get_path (msg)); + print_attr_xml ("maildir", mu_msg_get_maildir (msg)); + g_print ("\t\n"); + + if (mu_msg_iter_is_last(iter)) + g_print ("\n"); + + return TRUE; +} + +static OutputFunc* +get_output_func (MuConfig *opts, GError **err) +{ + switch (opts->format) { + case MU_CONFIG_FORMAT_LINKS: return output_link; + case MU_CONFIG_FORMAT_EXEC: return exec_cmd; + case MU_CONFIG_FORMAT_PLAIN: return output_plain; + case MU_CONFIG_FORMAT_XML: return output_xml; + case MU_CONFIG_FORMAT_SEXP: return output_sexp; + case MU_CONFIG_FORMAT_JSON: return output_json; + + default: + g_return_val_if_reached (NULL); + return NULL; + } +} + +static gboolean +output_query_results (MuMsgIter *iter, MuConfig *opts, GError **err) +{ + int count; + gboolean rv; + OutputFunc *output_func; + + output_func = get_output_func (opts, err); + if (!output_func) + return FALSE; + + for (count = 0, rv = TRUE; !mu_msg_iter_is_done(iter); + mu_msg_iter_next (iter)) { + + MuMsg *msg; + + if (count == opts->maxnum) + break; + msg = get_message (iter, opts->after); + if (!msg) + break; + /* { */ + /* const char* thread_id; */ + /* thread_id = mu_msg_iter_get_thread_id (iter); */ + /* g_print ("%s ", thread_id ? thread_id : ""); */ + + /* } */ + rv = output_func (msg, iter, opts, err); + if (!rv) + break; + else + ++count; + } + + if (rv && count == 0) { + mu_util_g_set_error (err, MU_ERROR_NO_MATCHES, + "no matches for search expression"); + return FALSE; + } + + return rv; +} + +static gboolean +process_query (MuQuery *xapian, const gchar *query, MuConfig *opts, GError **err) +{ + MuMsgIter *iter; + gboolean rv; + + iter = run_query (xapian, query, opts, err); + if (!iter) + return FALSE; + + rv = output_query_results (iter, opts, err); + mu_msg_iter_destroy (iter); + + return rv; +} + +static gboolean +execute_find (MuStore *store, MuConfig *opts, GError **err) +{ + char *query_str; + MuQuery *oracle; + gboolean rv; + + oracle = get_query_obj (store, err); + if (!oracle) + return FALSE; + + query_str = get_query (opts, err); + if (!query_str) { + mu_query_destroy (oracle); + return FALSE; + } + + if (opts->format == MU_CONFIG_FORMAT_XQUERY) + rv = print_internal (oracle, query_str, TRUE, FALSE, err); + else if (opts->format == MU_CONFIG_FORMAT_MQUERY) + rv = print_internal (oracle, query_str, FALSE, + opts->verbose, err); + else + rv = process_query (oracle, query_str, opts, err); + + mu_query_destroy (oracle); + g_free (query_str); + + return rv; +} + +static gboolean +format_params_valid (MuConfig *opts, GError **err) +{ + switch (opts->format) { + case MU_CONFIG_FORMAT_EXEC: + break; + case MU_CONFIG_FORMAT_PLAIN: + case MU_CONFIG_FORMAT_SEXP: + case MU_CONFIG_FORMAT_JSON: + case MU_CONFIG_FORMAT_LINKS: + case MU_CONFIG_FORMAT_XML: + case MU_CONFIG_FORMAT_XQUERY: + case MU_CONFIG_FORMAT_MQUERY: + if (opts->exec) { + mu_util_g_set_error + (err, MU_ERROR_IN_PARAMETERS, + "--exec and --format cannot be combined"); + return FALSE; + } + break; + default: mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "invalid output format %s", + opts->formatstr ? opts->formatstr : ""); + return FALSE; + } + + if (opts->format == MU_CONFIG_FORMAT_LINKS && !opts->linksdir) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing --linksdir argument"); + return FALSE; + } + + if (opts->linksdir && opts->format != MU_CONFIG_FORMAT_LINKS) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "--linksdir is only valid with --format=links"); + return FALSE; + } + + return TRUE; +} + +static gboolean +query_params_valid (MuConfig *opts, GError **err) +{ + const gchar *xpath; + + if (!opts->params[1]) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing query"); + return FALSE; + } + + xpath = mu_runtime_path (MU_RUNTIME_PATH_XAPIANDB); + if (mu_util_check_dir (xpath, TRUE, FALSE)) + return TRUE; + + mu_util_g_set_error (err, MU_ERROR_FILE_CANNOT_READ, + "'%s' is not a readable Xapian directory", + xpath); + return FALSE; +} + +MuError +mu_cmd_find (MuStore *store, MuConfig *opts, GError **err) +{ + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_FIND, + MU_ERROR_INTERNAL); + + if (opts->exec) + opts->format = MU_CONFIG_FORMAT_EXEC; /* pseudo format */ + + if (!query_params_valid (opts, err) || + !format_params_valid(opts, err)) + return MU_G_ERROR_CODE (err); + + if (!execute_find (store, opts, err)) + return MU_G_ERROR_CODE(err); + else + return MU_OK; +} diff --git a/mu/mu-cmd-index.c b/mu/mu-cmd-index.c new file mode 100644 index 0000000..5508463 --- /dev/null +++ b/mu/mu-cmd-index.c @@ -0,0 +1,318 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2008-2016 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include "mu-cmd.h" + +#include +#include +#include +#include +#include + +#include "mu-msg.h" +#include "mu-index.h" +#include "mu-store.hh" +#include "mu-runtime.h" + +#include "utils/mu-util.h" +#include "utils/mu-log.h" + +static gboolean MU_CAUGHT_SIGNAL; + +static void +sig_handler (int sig) +{ + if (!MU_CAUGHT_SIGNAL && sig == SIGINT) { /* Ctrl-C */ + g_print ("\n"); + g_warning ("shutting down gracefully, " + "press again to kill immediately"); + } + + MU_CAUGHT_SIGNAL = TRUE; +} + +static void +install_sig_handler (void) +{ + struct sigaction action; + int i, sigs[] = { SIGINT, SIGHUP, SIGTERM }; + + MU_CAUGHT_SIGNAL = FALSE; + + action.sa_handler = sig_handler; + sigemptyset(&action.sa_mask); + action.sa_flags = SA_RESETHAND; + + for (i = 0; i != G_N_ELEMENTS(sigs); ++i) + if (sigaction (sigs[i], &action, NULL) != 0) + g_critical ("set sigaction for %d failed: %s", + sigs[i], strerror (errno));; +} + + +static gboolean +check_params (MuConfig *opts, GError **err) +{ + /* param[0] == 'index' there should be no param[1] */ + if (opts->params[1]) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "unexpected parameter"); + return FALSE; + } + + if (opts->max_msg_size < 0) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "the maximum message size must >= 0"); + return FALSE; + } + + return TRUE; +} + +static MuError +index_msg_silent_cb (MuIndexStats* stats, void *user_data) +{ + return MU_CAUGHT_SIGNAL ? MU_STOP: MU_OK; +} + + + +static void +print_stats (MuIndexStats* stats, gboolean clear, gboolean color) +{ + const char *kars="-\\|/"; + char output[120]; + + static unsigned i = 0; + + if (clear) + fputs ("\r", stdout); + + if (color) + g_snprintf + (output, sizeof(output), + MU_COLOR_YELLOW "%c " MU_COLOR_DEFAULT + "processing mail; " + "processed: " MU_COLOR_GREEN "%u; " MU_COLOR_DEFAULT + "updated/new: " MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT + ", cleaned-up: " MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT, + (unsigned)kars[++i % 4], + (unsigned)stats->_processed, + (unsigned)stats->_updated, + (unsigned)stats->_cleaned_up); + else + g_snprintf + (output, sizeof(output), + "%c processing mail; processed: %u; " + "updated/new: %u, cleaned-up: %u", + (unsigned)kars[++i % 4], + (unsigned)stats->_processed, + (unsigned)stats->_updated, + (unsigned)stats->_cleaned_up); + + fputs (output, stdout); + fflush (stdout); +} + + +struct _IndexData { + gboolean color; +}; +typedef struct _IndexData IndexData; + + +static MuError +index_msg_cb (MuIndexStats* stats, IndexData *idata) +{ + if (stats->_processed % 75) + return MU_OK; + + print_stats (stats, TRUE, idata->color); + + return MU_CAUGHT_SIGNAL ? MU_STOP: MU_OK; +} + +static void +show_time (unsigned t, unsigned processed, gboolean color) +{ + if (color) { + if (t) + g_print ("elapsed: " + MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT + " second(s), ~ " + MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT + " msg/s", + t, processed/t); + else + g_print ("elapsed: " + MU_COLOR_GREEN "%u" MU_COLOR_DEFAULT + " second(s)", t); + } else { + if (t) + g_print ("elapsed: %u second(s), ~ %u msg/s", + t, processed/t); + else + g_print ("elapsed: %u second(s)", t); + } + + g_print ("\n"); +} + +/* when logging to console, print a newline before doing so; this + * makes it more clear when something happens during the + * indexing/cleanup progress output */ +#define newline_before_on() \ + mu_log_options_set(mu_log_options_get() | MU_LOG_OPTIONS_NEWLINE) +#define newline_before_off() \ + mu_log_options_set(mu_log_options_get() & ~MU_LOG_OPTIONS_NEWLINE) + +static MuError +cleanup_missing (MuIndex *midx, MuConfig *opts, MuIndexStats *stats, + GError **err) +{ + MuError rv; + time_t t; + IndexData idata; + gboolean show_progress; + + if (!opts->quiet) + g_print ("cleaning up messages [%s]\n", + mu_runtime_path (MU_RUNTIME_PATH_XAPIANDB)); + + show_progress = !opts->quiet && isatty(fileno(stdout)); + mu_index_stats_clear (stats); + + t = time (NULL); + idata.color = !opts->nocolor; + newline_before_on(); + rv = mu_index_cleanup + (midx, stats, + show_progress ? + (MuIndexCleanupDeleteCallback)index_msg_cb : + (MuIndexCleanupDeleteCallback)index_msg_silent_cb, + &idata, err); + newline_before_off(); + + if (!opts->quiet) { + print_stats (stats, TRUE, !opts->nocolor); + g_print ("\n"); + show_time ((unsigned)(time(NULL)-t),stats->_processed, + !opts->nocolor); + } + + return (rv == MU_OK || rv == MU_STOP) ? MU_OK: MU_G_ERROR_CODE(err); +} + +static MuError +cmd_index (MuIndex *midx, MuConfig *opts, MuIndexStats *stats, GError **err) +{ + IndexData idata; + MuError rv; + gboolean show_progress; + + show_progress = !opts->quiet && isatty(fileno(stdout)); + idata.color = !opts->nocolor; + + newline_before_on(); + + rv = mu_index_run (midx, + opts->rebuild, + opts->lazycheck, stats, + show_progress ? + (MuIndexMsgCallback)index_msg_cb : + (MuIndexMsgCallback)index_msg_silent_cb, + NULL, &idata); + newline_before_off(); + + if (rv == MU_OK || rv == MU_STOP) { + MU_WRITE_LOG ("index: processed: %u; updated/new: %u", + stats->_processed, stats->_updated); + } else + mu_util_g_set_error (err, rv, "error while indexing"); + + return rv; +} + + +static MuIndex* +init_mu_index (MuStore *store, MuConfig *opts, GError **err) +{ + MuIndex *midx; + + if (!check_params (opts, err)) + return NULL; + + midx = mu_index_new (store, err); + if (!midx) + return NULL; + + mu_index_set_max_msg_size (midx, opts->max_msg_size); + + return midx; +} + +MuError +mu_cmd_index (MuStore *store, MuConfig *opts, GError **err) +{ + MuIndex *midx; + MuIndexStats stats; + gboolean rv; + time_t t; + + g_return_val_if_fail (opts, FALSE); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_INDEX, + FALSE); + + /* create, and do error handling if needed */ + midx = init_mu_index (store, opts, err); + if (!midx) + return MU_G_ERROR_CODE(err); + + mu_index_stats_clear (&stats); + install_sig_handler (); + + if (!opts->quiet) + mu_store_print_info (store, opts->nocolor); + + t = time (NULL); + rv = cmd_index (midx, opts, &stats, err); + + if (rv == MU_OK && !opts->nocleanup) { + if (!opts->quiet) + g_print ("\n"); + rv = cleanup_missing (midx, opts, &stats, err); + } + + if (!opts->quiet) { + print_stats (&stats, TRUE, !opts->nocolor); + g_print ("\n"); + show_time ((unsigned)(time(NULL)-t), + stats._processed, !opts->nocolor); + } + + mu_index_destroy (midx); + + return rv; +} diff --git a/mu/mu-cmd-script.c b/mu/mu-cmd-script.c new file mode 100644 index 0000000..e1180c1 --- /dev/null +++ b/mu/mu-cmd-script.c @@ -0,0 +1,204 @@ +/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ + +/* +** Copyright (C) 2012-2013 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include +#include +#include +#include + +#include "mu-cmd.h" +#include "mu-script.h" +#include "mu-runtime.h" + + +#include "utils/mu-util.h" +#include "utils/mu-str.h" + + +#define MU_GUILE_EXT ".scm" +#define MU_GUILE_DESCR_PREFIX ";; INFO: " + +#define COL(C) ((color)?C:"") + +static void +print_script (const char *name, const char *oneline, const char *descr, + gboolean color, gboolean verbose) +{ + g_print ("%s%s%s%s%s%s%s%s", + verbose ? "\n" : " * ", + COL(MU_COLOR_GREEN),name,COL(MU_COLOR_DEFAULT), + oneline ? ": " : "", + COL(MU_COLOR_BLUE),oneline ? oneline :"",MU_COLOR_DEFAULT); + + if (verbose && descr) + g_print ("%s%s%s", + COL(MU_COLOR_MAGENTA),descr,COL(MU_COLOR_DEFAULT)); +} + + +static gboolean +print_scripts (GSList *scripts, gboolean color, + gboolean verbose, const char *rxstr, GError **err) +{ + GSList *cur; + const char *verb; + + if (!scripts) { + g_print ("No scripts available\n"); + return TRUE; /* not an error */ + } + + verb = verbose ? "" : " (use --verbose for details)"; + + if (rxstr) + g_print ("Available scripts matching '%s'%s:\n", + rxstr, verb); + else + g_print ("Available scripts%s:\n", verb); + + for (cur = scripts; cur; cur = g_slist_next (cur)) { + + MuScriptInfo *msi; + const char* descr, *oneline, *name; + + msi = (MuScriptInfo*)cur->data; + name = mu_script_info_name (msi); + oneline = mu_script_info_one_line (msi); + descr = mu_script_info_description (msi); + + /* if rxstr is provide, only consider matching scriptinfos */ + if (rxstr && !mu_script_info_matches_regex (msi, rxstr, err)) { + if (err && *err) + return FALSE; + continue; + } + + print_script (name, oneline, descr, color, verbose); + } + + return TRUE; +} + + +static GSList* +get_script_info_list (const char *muhome, GError **err) +{ + GSList *scripts, *userscripts, *last; + char *userpath; + + scripts = mu_script_get_script_info_list + (MU_SCRIPTS_DIR, MU_GUILE_EXT, + MU_GUILE_DESCR_PREFIX, + err); + + if (err && *err) + return NULL; + + userpath = g_strdup_printf ("%s%c%s", + muhome, G_DIR_SEPARATOR, "scripts"); + + /* is there are userdir for scripts? */ + if (!mu_util_check_dir (userpath, TRUE, FALSE)) { + g_free (userpath); + return scripts; + } + + /* append it to the list we already have */ + userscripts = mu_script_get_script_info_list (userpath, MU_GUILE_EXT, + MU_GUILE_DESCR_PREFIX, + err); + g_free (userpath); + + /* some error, return nothing */ + if (err && *err) { + mu_script_info_list_destroy (userscripts); + mu_script_info_list_destroy (scripts); + return NULL; + } + + /* append the user scripts */ + last = g_slist_last (scripts); + if (last) { + last->next = userscripts; + return scripts; + } else + return userscripts; /* apparently, scripts was NULL */ +} + + + +static gboolean +check_params (MuConfig *opts, GError **err) +{ + if (!mu_util_supports (MU_FEATURE_GUILE)) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "the 'script' command is not available " + "in this version of mu"); + return FALSE; + } + + return TRUE; +} + + +MuError +mu_cmd_script (MuConfig *opts, GError **err) +{ + MuScriptInfo *msi; + GSList *scripts; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_SCRIPT, + MU_ERROR_INTERNAL); + + if (!check_params (opts, err)) + return MU_ERROR; + + scripts = get_script_info_list (opts->muhome, err); + if (err && *err) + goto leave; + + if (g_strcmp0 (opts->cmdstr, "script") == 0) { + print_scripts (scripts, !opts->nocolor, opts->verbose, + opts->script_params[0], err); + goto leave; + } + + msi = mu_script_find_script_with_name (scripts, opts->script); + if (!msi) { + mu_util_g_set_error (err, MU_ERROR_SCRIPT_NOT_FOUND, + "command or script not found"); + goto leave; + } + + /* do it! */ + mu_script_guile_run (msi, mu_runtime_path(MU_RUNTIME_PATH_CACHE), + opts->script_params, err); +leave: + /* this won't be reached, unless there is some error */ + mu_script_info_list_destroy (scripts); + return (err && *err) ? MU_ERROR : MU_OK; +} diff --git a/mu/mu-cmd-server.cc b/mu/mu-cmd-server.cc new file mode 100644 index 0000000..bce9025 --- /dev/null +++ b/mu/mu-cmd-server.cc @@ -0,0 +1,1334 @@ +/* +** Copyright (C) 2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" +#include "mu-cmd.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "mu-runtime.h" +#include "mu-cmd.h" +#include "mu-maildir.h" +#include "mu-query.h" +#include "mu-index.h" +#include "mu-store.hh" +#include "mu-msg-part.h" +#include "mu-contacts.hh" + +#include "utils/mu-str.h" +#include "utils/mu-utils.hh" +#include "utils/mu-command-parser.hh" + +using namespace Mu; +using namespace Command; +using namespace Sexp; + +using DocId = unsigned; + +static std::atomic MuTerminate{false}; + +static void +sig_handler (int sig) +{ + MuTerminate = true; +} + +static void +install_sig_handler (void) +{ + struct sigaction action; + int i, sigs[] = { SIGINT, SIGHUP, SIGTERM, SIGPIPE }; + + MuTerminate = false; + + action.sa_handler = sig_handler; + sigemptyset(&action.sa_mask); + action.sa_flags = SA_RESETHAND; + + for (i = 0; i != G_N_ELEMENTS(sigs); ++i) + if (sigaction (sigs[i], &action, NULL) != 0) + g_critical ("set sigaction for %d failed: %s", + sigs[i], g_strerror (errno));; +} + + +/* + * Markers for/after the length cookie that precedes the expression we write to + * output. We use octal 376, 377 (ie, 0xfe, 0xff) as they will never occur in + * utf8 */ + + +#define COOKIE_PRE '\376' +#define COOKIE_POST '\377' + +static void G_GNUC_PRINTF(1, 2) +print_expr (const char* frm, ...) +{ + char *expr, *expr_orig; + va_list ap; + ssize_t rv; + size_t exprlen, lenlen; + char cookie[16]; + static int outfd = 0; + +#if defined(__CYGWIN__ )&& !defined (_WIN32) + const size_t writestep = 4096 * 16; + size_t bytestowrite = 0; +#endif + + if (outfd == 0) + outfd = fileno (stdout); + + expr = NULL; + + va_start (ap, frm); + exprlen = g_vasprintf (&expr, frm, ap); + va_end (ap); + + /* this cookie tells the frontend where to expect the next + * expression */ + + cookie[0] = COOKIE_PRE; + lenlen = sprintf(cookie + 1, "%x", + (unsigned)exprlen + 1); /* + 1 for \n */ + cookie[lenlen + 1] = COOKIE_POST; + + /* write the cookie, ie. + * COOKIE_PRE COOKIE_POST + */ + rv = write (outfd, cookie, lenlen + 2); + if (rv != -1) { + expr_orig = expr; +#if defined (__CYGWIN__) && !defined(_WIN32) + /* CYGWIN doesn't like big packets */ + while (exprlen > 0) { + bytestowrite = exprlen > writestep ? writestep : exprlen; + rv = write(outfd, expr, bytestowrite); + expr += bytestowrite; + exprlen -= bytestowrite; + } +#else + rv = write (outfd, expr, exprlen); +#endif + g_free (expr_orig); + } + if (rv != -1) + rv = write (outfd, "\n", 1); + if (rv == -1) { + g_critical ("%s: write() failed: %s", + __func__, g_strerror(errno)); + /* terminate ourselves */ + raise (SIGTERM); + } +} + + +G_GNUC_PRINTF(2,3) static MuError +print_error (MuError errcode, const char* frm, ...) +{ + char *msg; + va_list ap; + + va_start (ap, frm); + g_vasprintf (&msg, frm, ap); + va_end (ap); + + print_expr ("(:error %u :message %s)", errcode, quote(msg).c_str()); + g_free (msg); + + return errcode; +} + +static unsigned +print_sexps (MuMsgIter *iter, unsigned maxnum) +{ + unsigned u; + u = 0; + + while (!mu_msg_iter_is_done (iter) && u < maxnum) { + + MuMsg *msg; + msg = mu_msg_iter_get_msg_floating (iter); + + if (mu_msg_is_readable (msg)) { + char *sexp; + const MuMsgIterThreadInfo* ti; + ti = mu_msg_iter_get_thread_info (iter); + sexp = mu_msg_to_sexp (msg, + mu_msg_iter_get_docid (iter), + ti, MU_MSG_OPTION_HEADERS_ONLY); + print_expr ("%s", sexp); + g_free (sexp); + ++u; + } + mu_msg_iter_next (iter); + } + return u; +} + + +struct Context { + Context(){} + Context (MuConfig *opts) { + const auto dbpath{mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB)}; + GError *gerr{}; + store = mu_store_new_writable (dbpath, NULL); + if (!store) { + const auto mu_init = format("mu init %s%s", + opts->muhome ? "--muhome=" : "", + opts->muhome ? opts->muhome : ""); + + if (gerr) { + if ((MuError)gerr->code == MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK) + print_error(MU_ERROR_XAPIAN_CANNOT_GET_WRITELOCK, + "mu database already locked; " + "some other mu running?"); + else + print_error((MuError)gerr->code, + "cannot open database @ %s:%s; already running? " + "if not, please try '%s", dbpath, + gerr->message ? gerr->message : "something went wrong", + mu_init.c_str()); + } else + print_error(MU_ERROR, + "cannot open database @ %s; already running? if not, please try '%s'", + dbpath, mu_init.c_str()); + + throw Mu::Error (Error::Code::Store, &gerr/*consumed*/, + "failed to open database @ %s; already running? if not, please try '%s'", + dbpath, mu_init.c_str()); + } + + query = mu_query_new (store, &gerr); + if (!query) + throw Error(Error::Code::Store, &gerr, "failed to create query"); + } + + ~Context() { + if (query) + mu_query_destroy(query); + if (store) { + mu_store_flush(store); + mu_store_unref(store); + } + } + + Context(const Context&) = delete; + + MuStore *store{}; + MuQuery *query{}; + bool do_quit{}; + + CommandMap command_map; +}; + + +static MuMsgOptions +message_options (const Parameters& params) +{ + const auto extract_images{get_bool_or(params, "extract-images", false)}; + const auto decrypt{get_bool_or(params, "decrypt", false)}; + const auto verify{get_bool_or(params, "verify", false)}; + + int opts{MU_MSG_OPTION_NONE}; + if (extract_images) + opts |= MU_MSG_OPTION_EXTRACT_IMAGES; + if (verify) + opts |= MU_MSG_OPTION_VERIFY | MU_MSG_OPTION_USE_AGENT; + if (decrypt) + opts |= MU_MSG_OPTION_DECRYPT | MU_MSG_OPTION_USE_AGENT; + + return (MuMsgOptions)opts; +} + +/* 'add' adds a message to the database, and takes two parameters: 'path', which + * is the full path to the message, and 'maildir', which is the maildir this + * message lives in (e.g. "/inbox"). response with an (:info ...) message with + * information about the newly added message (details: see code below) + */ +static void +add_handler (Context& context, const Parameters& params) +{ + const auto path{get_string_or(params, "path")}; + + GError *gerr{}; + const auto docid{mu_store_add_path (context.store, path.c_str(), &gerr)}; + if (docid == MU_STORE_INVALID_DOCID) + throw Error(Error::Code::Store, &gerr, "failed to add message at %s", + path.c_str()); + + print_expr ("(:info add :path %s :docid %u)", quote(path).c_str(), docid); + + auto msg{mu_store_get_msg(context.store, docid, &gerr)}; + if (!msg) + throw Error(Error::Code::Store, &gerr, "failed to get message at %s", + path.c_str()); + + auto sexp{mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY)}; + print_expr ("(:update %s :move nil)", sexp); + mu_msg_unref(msg); + g_free (sexp); +} + + +struct _PartInfo { + GSList *attlist; + MuMsgOptions opts; +}; +typedef struct _PartInfo PartInfo; + +static void +each_part (MuMsg *msg, MuMsgPart *part, PartInfo *pinfo) +{ + char *att, *cachefile; + + /* exclude things that don't look like proper attachments, + * unless they're images */ + if (!mu_msg_part_maybe_attachment(part)) + return; + + GError *gerr{}; + cachefile = mu_msg_part_save_temp (msg, + (MuMsgOptions)(pinfo->opts|MU_MSG_OPTION_OVERWRITE), + part->index, &gerr); + if (!cachefile) + throw Error (Error::Code::File, &gerr, "failed to save part"); + + att = g_strdup_printf ("(:file-name %s :mime-type \"%s/%s\")", + quote(cachefile).c_str(), part->type, part->subtype); + pinfo->attlist = g_slist_append (pinfo->attlist, att); + + g_free (cachefile); + +} + + +/* take the attachments of msg, save them as tmp files, and return + * as sexp (as a string) describing them + * + * ((:name :mime-type :disposition + * ) ... ) + * + */ +static gchar* +include_attachments (MuMsg *msg, MuMsgOptions opts) +{ + GSList *cur; + GString *gstr; + PartInfo pinfo; + + pinfo.attlist = NULL; + pinfo.opts = opts; + mu_msg_part_foreach (msg, opts, + (MuMsgPartForeachFunc)each_part, + &pinfo); + + gstr = g_string_sized_new (512); + gstr = g_string_append_c (gstr, '('); + for (cur = pinfo.attlist; cur; cur = g_slist_next (cur)) + g_string_append (gstr, (gchar*)cur->data); + gstr = g_string_append_c (gstr, ')'); + + mu_str_free_list (pinfo.attlist); + + return g_string_free (gstr, FALSE); +} + +enum { NEW, REPLY, FORWARD, EDIT, RESEND, INVALID_TYPE }; +static unsigned +compose_type (const char *typestr) +{ + if (g_str_equal (typestr, "reply")) + return REPLY; + else if (g_str_equal (typestr, "forward")) + return FORWARD; + else if (g_str_equal (typestr, "edit")) + return EDIT; + else if (g_str_equal (typestr, "resend")) + return RESEND; + else if (g_str_equal (typestr, "new")) + return NEW; + else + return INVALID_TYPE; +} + +/* 'compose' produces the un-changed *original* message sexp (ie., the message + * to reply to, forward or edit) for a new message to compose). It takes two + * parameters: 'type' with the compose type (either reply, forward or + * edit/resend), and 'docid' for the message to reply to. Note, type:new does + * not have an original message, and therefore does not need a docid + * + * In returns a (:compose [:original ] [:include] ) + * message (detals: see code below) + * + * Note ':include' t or nil determines whether to include attachments + */ +static void +compose_handler (Context& context, const Parameters& params) +{ + const auto typestr{get_symbol_or(params, "type")}; + const auto ctype{compose_type(typestr.c_str())}; + if (ctype == INVALID_TYPE) + throw Error(Error::Code::InvalidArgument, "invalid compose type"); + + // message optioss below checks extract-images / extract-encrypted + + char *sexp{}, *atts{}; + if (ctype == REPLY || ctype == FORWARD || ctype == EDIT || ctype == RESEND) { + + GError *gerr{}; + const unsigned docid{(unsigned)get_int_or(params, "docid")}; + auto msg{mu_store_get_msg (context.store, docid, &gerr)}; + if (!msg) + throw Error{Error::Code::Store, &gerr, "failed to get message %u", docid}; + + const auto opts{message_options(params)}; + sexp = mu_msg_to_sexp (msg, docid, NULL, opts); + atts = (ctype == FORWARD) ? include_attachments (msg, opts) : NULL; + mu_msg_unref (msg); + } + print_expr ("(:compose %s :original %s :include %s)", + typestr.c_str(), sexp ? sexp : "nil", atts ? atts : "nil"); + + g_free (sexp); + g_free (atts); +} + + +struct SexpData { + GString *gstr; + gboolean personal; + time_t last_seen; + gint64 tstamp; + size_t rank; +}; + + +static void +each_contact_sexp (const char* full_address, + const char *email, const char *name, gboolean personal, + time_t last_seen, unsigned freq, + gint64 tstamp, SexpData *sdata) +{ + sdata->rank++; + + /* since the last time we got some contacts */ + if (sdata->tstamp > tstamp) + return; + + /* (maybe) only include 'personal' contacts */ + if (sdata->personal && !personal) + return; + + /* only include newer-than-x contacts */ + if (sdata->last_seen > last_seen) + return; + + /* only include *real* e-mail addresses (ignore local + * addresses... there's little to complete there anyway...) */ + if (!email || !strstr (email, "@")) + return; + + g_string_append_printf (sdata->gstr, "(%s . %zu)\n", + quote(full_address).c_str(), sdata->rank); +} + +/** + * get all contacts as an s-expression + * + * @param self contacts object + * @param personal_only whether to restrict the list to 'personal' email + * addresses + * + * @return the sexp + */ +static char* +contacts_to_sexp (const MuContacts *contacts, bool personal, + int64_t last_seen, gint64 tstamp) +{ + + g_return_val_if_fail (contacts, NULL); + + SexpData sdata{}; + sdata.personal = personal; + sdata.last_seen = last_seen; + sdata.tstamp = tstamp; + sdata.rank = 0; + + /* make a guess for the initial size */ + sdata.gstr = g_string_sized_new (mu_contacts_count(contacts) * 128); + g_string_append (sdata.gstr, "(:contacts ("); + + const auto cutoff{g_get_monotonic_time()}; + mu_contacts_foreach (contacts, (MuContactsForeachFunc)each_contact_sexp, &sdata); + /* pass a string, elisp doesn't like 64-bit nums */ + g_string_append_printf (sdata.gstr, + ") :tstamp \"%" G_GINT64_FORMAT "\")", cutoff); + + return g_string_free (sdata.gstr, FALSE); +} + + +static void +contacts_handler (Context& context, const Parameters& params) +{ + const auto personal = get_bool_or(params, "personal"); + const auto afterstr = get_string_or(params, "after"); + const auto tstampstr = get_string_or(params, "tstamp"); + + const auto after{afterstr.empty() ? 0 : + g_ascii_strtoll(date_to_time_t_string(afterstr, true).c_str(), {}, 10)}; + const auto tstamp = g_ascii_strtoll (tstampstr.c_str(), NULL, 10); + + const auto contacts{mu_store_contacts(context.store)}; + if (!contacts) + throw Error{Error::Code::Internal, "failed to get contacts"}; + + /* dump the contacts cache as a giant sexp */ + auto sexp = contacts_to_sexp (contacts, personal, after, tstamp); + print_expr ("%s\n", sexp); + g_free (sexp); +} + +static void +save_part (MuMsg *msg, unsigned docid, unsigned index, + MuMsgOptions opts, const Parameters& params) +{ + const auto path{get_string_or(params, "path")}; + if (path.empty()) + throw Error{Error::Code::Command, "missing path"}; + + GError *gerr{}; + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | (int)MU_MSG_OPTION_OVERWRITE), + path.c_str(), index, &gerr)) + throw Error{Error::Code::File, &gerr, "failed to save part"}; + + print_expr ("(:info save :message %s)", quote(path + " has been saved").c_str()); +} + + +static void +open_part (MuMsg *msg, unsigned docid, unsigned index, MuMsgOptions opts) +{ + GError *gerr{}; + char *targetpath{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; + if (!targetpath) + throw Error{Error::Code::File, &gerr, "failed to get cache-path"}; + + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), + targetpath, index, &gerr)) { + g_free(targetpath); + throw Error{Error::Code::File, &gerr, "failed to save to cache-path"}; + } + + if (!mu_util_play (targetpath, TRUE,/*allow local*/ + FALSE/*allow remote*/, &gerr)) { + g_free(targetpath); + throw Error{Error::Code::File, &gerr, "failed to play"}; + } + + print_expr ("(:info open :message %s)", + quote(std::string{targetpath} + " has been opened").c_str()); + g_free (targetpath); +} + +static void +temp_part (MuMsg *msg, unsigned docid, unsigned index, + MuMsgOptions opts, const Parameters& params) +{ + const auto what{get_symbol_or(params, "what")}; + if (what.empty()) + throw Error{Error::Code::Command, "missing 'what'"}; + + const auto param{get_string_or(params, "param")}; + + GError *gerr{}; + char *path{mu_msg_part_get_cache_path (msg, opts, index, &gerr)}; + if (!path) + throw Error{Error::Code::File, &gerr, "could not get cache path"}; + + if (!mu_msg_part_save (msg, (MuMsgOptions)(opts | MU_MSG_OPTION_USE_EXISTING), + path, index, &gerr)) { + g_free(path); + throw Error{Error::Code::File, &gerr, "saving failed"}; + } + + const auto qpath{quote(path)}; + g_free(path); + + if (!param.empty()) + print_expr ("(:temp %s" + " :what \"%s\"" + " :docid %u" + " :param %s" + ")", + qpath.c_str(), what.c_str(), docid, quote(param).c_str()); + else + print_expr ("(:temp %s :what \"%s\" :docid %u)", + qpath.c_str(), what.c_str(), docid); +} + + + +/* 'extract' extracts some mime part from a message */ +static void +extract_handler (Context& context, const Parameters& params) +{ + const auto docid{get_int_or(params, "docid")}; + const auto index{get_int_or(params, "index")}; + const auto opts{message_options(params)}; + + GError *gerr{}; + auto msg{mu_store_get_msg (context.store, docid, &gerr)}; + if (!msg) + throw Error{Error::Code::Store, "failed to get message"}; + + try { + const auto action{get_symbol_or(params, "action")}; + if (action == "save") + save_part (msg, docid, index, opts, params); + else if (action == "open") + open_part (msg, docid, index, opts); + else if (action == "temp") + temp_part (msg, docid, index, opts, params); + else { + throw Error{Error::Code::InvalidArgument, + "unknown action '%s'", action.c_str()}; + } + + } catch (...) { + mu_msg_unref (msg); + throw; + } +} + + +/* get a *list* of all messages with the given message id */ +static std::vector +docids_for_msgid (MuQuery *query, const std::string& msgid, size_t max=100) +{ + if (msgid.size() > MU_STORE_MAX_TERM_LENGTH - 1) { + throw Error(Error::Code::InvalidArgument, + "invalid message-id '%s'", msgid.c_str()); + } + + const auto xprefix{mu_msg_field_xapian_prefix(MU_MSG_FIELD_ID_MSGID)}; + /*XXX this is a bit dodgy */ + auto tmp{g_ascii_strdown(msgid.c_str(), -1)}; + auto rawq{g_strdup_printf("%c%s", xprefix, tmp)}; + g_free(tmp); + + GError *gerr{}; + auto iter{mu_query_run (query, rawq, MU_MSG_FIELD_ID_NONE, max, MU_QUERY_FLAG_RAW, &gerr)}; + g_free (rawq); + if (!iter) + throw Error(Error::Code::Store, &gerr, "failed to run msgid-query"); + if (mu_msg_iter_is_done (iter)) + throw Error(Error::Code::NotFound, + "could not find message(s) for msgid %s", msgid.c_str()); + std::vector docids; + do { + docids.emplace_back(mu_msg_iter_get_docid (iter)); + } while (mu_msg_iter_next (iter)); + mu_msg_iter_destroy (iter); + + return docids; +} + +/* + * creating a message object just to get a path seems a bit excessive maybe + * mu_store_get_path could be added if this turns out to be a problem + */ +static std::string +path_from_docid (MuStore *store, unsigned docid) +{ + GError *gerr{}; + auto msg{mu_store_get_msg (store, docid, &gerr)}; + if (!msg) + throw Error(Error::Code::Store, &gerr, "could not get message from store"); + + auto p{mu_msg_get_path(msg)}; + if (!p) { + mu_msg_unref(msg); + throw Error(Error::Code::Store, + "could not get path for message %u", docid); + } + + std::string msgpath{p}; + mu_msg_unref (msg); + + return msgpath; +} + + +static std::vector +determine_docids (MuQuery *query, const Parameters& params) +{ + auto docid{get_int_or(params, "docid", 0)}; + const auto msgid{get_string_or(params, "msgid")}; + + if ((docid == 0) == msgid.empty()) + throw Error(Error::Code::InvalidArgument, + "precisely one of docid and msgid must be specified"); + + if (docid != 0) + return { (unsigned)docid }; + else + return docids_for_msgid (query, msgid.c_str()); +} + + +static void +find_handler (Context& context, const Parameters& params) +{ + const auto query{get_string_or(params, "query")}; + const auto threads{get_bool_or(params, "threads", false)}; + const auto sortfieldstr{get_symbol_or(params, "sortfield")}; + const auto descending{get_bool_or(params, "descending", false)}; + const auto maxnum{get_int_or(params, "maxnum", -1/*unlimited*/)}; + const auto skip_dups{get_bool_or(params, "skip-dups", false)}; + const auto include_related{get_bool_or(params, "include-related", false)}; + + MuMsgFieldId sort_field{MU_MSG_FIELD_ID_NONE}; + if (!sortfieldstr.empty()) { + sort_field = mu_msg_field_id_from_name ( + sortfieldstr.c_str() + 1, FALSE); // skip ':' + if (sort_field == MU_MSG_FIELD_ID_NONE) + throw Error{Error::Code::InvalidArgument, "invalid sort field %s", + sortfieldstr.c_str()}; + } + + int qflags{MU_QUERY_FLAG_NONE/*UNREADABLE*/}; + if (descending) + qflags |= MU_QUERY_FLAG_DESCENDING; + if (skip_dups) + qflags |= MU_QUERY_FLAG_SKIP_DUPS; + if (include_related) + qflags |= MU_QUERY_FLAG_INCLUDE_RELATED; + if (threads) + qflags |= MU_QUERY_FLAG_THREADS; + + GError *gerr{}; + auto miter{mu_query_run(context.query, query.c_str(), sort_field, maxnum, + (MuQueryFlags)qflags, &gerr)}; + if (!miter) + throw Error(Error::Code::Query, &gerr, "failed to run query"); + + /* before sending new results, send an 'erase' message, so the frontend + * knows it should erase the headers buffer. this will ensure that the + * output of two finds will not be mixed. */ + print_expr ("(:erase t)"); + const auto foundnum{print_sexps (miter, maxnum)}; + print_expr ("(:found %u)", foundnum); + mu_msg_iter_destroy (miter); +} + + +static void +help_handler (Context& context, const Parameters& params) +{ + const auto command{get_symbol_or(params, "command", "")}; + const auto full{get_bool_or(params, "full")}; + + if (command.empty()) { + std::cout << ";; Commands are s-expressions of the form\n" + << ";; ( :param1 val1 :param2 val2 ...)\n" + << ";; For instance:\n;; (help :command quit)\n" + << ";; to get information about the 'quit' command\n;;\n"; + std::cout << ";; The following commands are available:\n"; + } + + std::vector names; + for (auto&& name_cmd: context.command_map) + names.emplace_back(name_cmd.first); + std::sort(names.begin(), names.end()); + + for (auto&& name: names) { + const auto& info{context.command_map.find(name)->second}; + + if (!command.empty() && name != command) + continue; + + if (!command.empty()) + std::cout << ";; " << format("%-10s -- %s\n", name.c_str(), + info.docstring.c_str()); + else + std::cout << ";; " << name.c_str() << ": " + << info.docstring.c_str() << '\n'; + + if (!full) + continue; + + for (auto&& argname: info.sorted_argnames()) { + const auto& arg{info.args.find(argname)}; + std::cout << ";; " + << format("%-17s : %-24s ", arg->first.c_str(), + to_string(arg->second).c_str()); + std::cout << " " << arg->second.docstring << "\n"; + } + std::cout << ";;\n"; + } +} + +static MuError +index_msg_cb (MuIndexStats *stats, void *user_data) +{ + if (MuTerminate) + return MU_STOP; + + if (stats->_processed % 1000) + return MU_OK; + + print_expr ("(:info index :status running " + ":processed %u :updated %u)", + stats->_processed, stats->_updated); + + return MU_OK; +} + + +static MuError +index_and_maybe_cleanup (MuIndex *index, bool cleanup, bool lazy_check) +{ + MuIndexStats stats{}, stats2{}; + mu_index_stats_clear (&stats); + auto rv = mu_index_run (index, FALSE, lazy_check, &stats, + index_msg_cb, NULL, NULL); + if (rv != MU_OK && rv != MU_STOP) + throw Error{Error::Code::Store, "indexing failed"}; + + mu_index_stats_clear (&stats2); + if (cleanup) { + GError *gerr{}; + rv = mu_index_cleanup (index, &stats2, NULL, NULL, &gerr); + if (rv != MU_OK && rv != MU_STOP) + throw Error{Error::Code::Store, &gerr, "cleanup failed"}; + } + + print_expr ("(:info index :status complete " + ":processed %u :updated %u :cleaned-up %u)", + stats._processed, stats._updated, stats2._cleaned_up); + + return rv; +} + + +static void +index_handler (Context& context, const Parameters& params) +{ + GError *gerr{}; + const auto cleanup{get_bool_or(params, "cleanup")}; + const auto lazy_check{get_bool_or(params, "lazy-check")}; + auto index{mu_index_new (context.store, &gerr)}; + if (!index) + throw Error(Error::Code::Index, &gerr, "failed to create index object"); + + try { + index_and_maybe_cleanup (index, cleanup, lazy_check); + } catch (...) { + mu_index_destroy(index); + throw; + } + mu_index_destroy(index); + mu_store_flush(context.store); +} + +static void +mkdir_handler (Context& context, const Parameters& params) +{ + const auto path{get_string_or(params, "path")}; + + GError *gerr{}; + if (!mu_maildir_mkdir(path.c_str(), 0755, FALSE, &gerr)) + throw Error{Error::Code::File, &gerr, "failed to create maildir"}; + + print_expr ("(:info mkdir :message \"%s has been created\")", path.c_str()); +} + + +static MuFlags +get_flags (const std::string& path, const std::string& flagstr) +{ + if (flagstr.empty()) + return MU_FLAG_NONE; /* ie., ignore flags */ + else { + /* if there's a '+' or '-' sign in the string, it must + * be a flag-delta */ + if (strstr (flagstr.c_str(), "+") || strstr (flagstr.c_str(), "-")) { + auto oldflags = mu_maildir_get_flags_from_path (path.c_str()); + return mu_flags_from_str_delta (flagstr.c_str(), oldflags, MU_FLAG_TYPE_ANY); + } else + return mu_flags_from_str (flagstr.c_str(), MU_FLAG_TYPE_ANY, + TRUE /*ignore invalid*/); + } +} + +static void +do_move (MuStore *store, DocId docid, MuMsg *msg, const std::string& maildirarg, + MuFlags flags, bool new_name, bool no_view) +{ + bool different_mdir{}; + auto maildir{maildirarg}; + if (maildir.empty()) { + maildir = mu_msg_get_maildir (msg); + different_mdir = FALSE; + } else /* are we moving to a different mdir, or is it just flags? */ + different_mdir = maildir != mu_msg_get_maildir(msg); + + GError* gerr{}; + if (!mu_msg_move_to_maildir (msg, maildir.c_str(), flags, TRUE, new_name, &gerr)) + throw Error{Error::Code::File, &gerr, "failed to move message"}; + + /* after mu_msg_move_to_maildir, path will be the *new* path, and flags and maildir fields + * will be updated as wel */ + auto rv = mu_store_update_msg (store, docid, msg, &gerr); + if (rv == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::Store, &gerr, "failed to store updated message"}; + + char *sexp = mu_msg_to_sexp (msg, docid, NULL, MU_MSG_OPTION_VERIFY); + /* note, the :move t thing is a hint to the frontend that it + * could remove the particular header */ + print_expr ("(:update %s :move %s :maybe-view %s)", sexp, + different_mdir ? "t" : "nil", + no_view ? "nil" : "t"); + g_free (sexp); +} + +static void +move_docid (MuStore *store, DocId docid, const std::string& flagstr, + bool new_name, bool no_view) +{ + if (docid == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::InvalidArgument, "invalid docid"}; + + GError *gerr{}; + auto msg{mu_store_get_msg (store, docid, &gerr)}; + + try { + if (!msg) + throw Error{Error::Code::Store, &gerr, "failed to get message from store"}; + + const auto flags = flagstr.empty() ? mu_msg_get_flags (msg) : + get_flags (mu_msg_get_path(msg), flagstr); + if (flags == MU_FLAG_INVALID) + throw Error{Error::Code::InvalidArgument, "invalid flags '%s'", flagstr.c_str()}; + + do_move (store, docid, msg, "", flags, new_name, no_view); + + } catch (...) { + if (msg) + mu_msg_unref (msg); + throw; + } + + mu_msg_unref (msg); +} + +/* + * 'move' moves a message to a different maildir and/or changes its + * flags. parameters are *either* a 'docid:' or 'msgid:' pointing to + * the message, a 'maildir:' for the target maildir, and a 'flags:' + * parameter for the new flags. + * + * returns an (:update ) + * + */ +static void +move_handler (Context& context, const Parameters& params) +{ + auto maildir{get_string_or(params, "maildir")}; + const auto flagstr{get_string_or(params, "flags")}; + const auto rename{get_bool_or (params, "rename")}; + const auto no_view{get_bool_or (params, "noupdate")}; + const auto docids{determine_docids (context.query, params)}; + + if (docids.size() > 1) { + if (!maildir.empty()) // ie. duplicate message-ids. + throw Mu::Error{Error::Code::Store, + "can't move multiple messages at the same time"}; + // multi. + for (auto&& docid: docids) + move_docid(context.store, docid, flagstr, rename, no_view); + return; + } + auto docid{docids.at(0)}; + + GError *gerr{}; + auto msg{mu_store_get_msg(context.store, docid, &gerr)}; + if (!msg) + throw Error{Error::Code::InvalidArgument, &gerr, "could not create message"}; + + /* if maildir was not specified, take the current one */ + if (maildir.empty()) + maildir = mu_msg_get_maildir (msg); + + /* determine the real target flags, which come from the flags-parameter + * we received (ie., flagstr), if any, plus the existing message + * flags. */ + MuFlags flags{}; + if (!flagstr.empty()) + flags = get_flags (mu_msg_get_path(msg), flagstr.c_str()); + else + flags = mu_msg_get_flags (msg); + + if (flags == MU_FLAG_INVALID) { + mu_msg_unref(msg); + throw Error{Error::Code::InvalidArgument, "invalid flagse"}; + } + + try { + do_move (context.store, docid, msg, maildir, flags, rename, no_view); + } catch (...) { + mu_msg_unref(msg); + throw; + } + + mu_msg_unref(msg); +} + +static void +ping_handler (Context& context, const Parameters& params) +{ + GError *gerr{}; + const auto storecount = mu_store_count(context.store, &gerr); + if (storecount == (unsigned)-1) + throw Error{Error::Code::Store, &gerr, "failed to read store"}; + + const auto queries = get_string_vec (params, "queries"); + const auto qresults = [&]() -> std::string { + if (queries.empty()) + return {}; + + std::string res{":queries ("}; + for (auto&& q: queries) { + const auto count{mu_query_count_run (context.query, q.c_str())}; + const auto unreadq{format("flag:unread AND (%s)", q.c_str())}; + const auto unread{mu_query_count_run (context.query, unreadq.c_str())}; + res += format("(:query %s :count %zu :unread %zu)", quote(q).c_str(), + count, unread); + } + return res + ")"; + }(); + + const auto personal = [&]() ->std::string { + auto addrs{mu_store_personal_addresses (context.store)}; + std::string res; + if (addrs && g_strv_length(addrs) != 0) { + res = ":personal-addresses ("; + for (int i = 0; addrs[i]; ++i) + res += quote(addrs[i]) + ' '; + res += ")"; + } + g_strfreev(addrs); + return res; + }(); + + print_expr ("(:pong \"mu\" :props (" + ":version \"" VERSION "\" " + "%s " + ":database-path %s " + ":root-maildir %s " + ":doccount %u " + "%s))", + personal.c_str(), + quote(mu_store_database_path(context.store)).c_str(), + quote(mu_store_root_maildir(context.store)).c_str(), + storecount, + qresults.c_str()); +} + +static void +quit_handler (Context& context, const Parameters& params) +{ + context.do_quit = true; +} + + +static void +remove_handler (Context& context, const Parameters& params) +{ + const auto docid{get_int_or(params, "docid")}; + const auto path{path_from_docid (context.store, docid)}; + + if (::unlink (path.c_str()) != 0 && errno != ENOENT) + throw Error(Error::Code::File, "could not delete %s: %s", + path.c_str(), strerror (errno)); + + if (!mu_store_remove_path (context.store, path.c_str())) + throw Error(Error::Code::Store, + "failed to remove message @ %s (%d) from store", + path.c_str(), docid); + + print_expr ("(:remove %u)", docid); +} + + +static void +sent_handler (Context& context, const Parameters& params) +{ + GError *gerr{}; + const auto path{get_string_or(params, "path")}; + const auto docid{mu_store_add_path(context.store, path.c_str(), &gerr)}; + if (docid == MU_STORE_INVALID_DOCID) + throw Error{Error::Code::Store, &gerr, "failed to add path"}; + + print_expr ("(:sent t :path %s :docid %u)", quote(path).c_str(), docid); +} + + +static void +view_handler (Context& context, const Parameters& params) +{ + DocId docid{}; + const auto path{get_string_or(params, "path")}; + + GError *gerr{}; + MuMsg *msg{}; + + if (!path.empty()) + msg = mu_msg_new_from_file (path.c_str(), NULL, &gerr); + else { + docid = determine_docids(context.query, params).at(0); + msg = mu_store_get_msg (context.store, docid, &gerr); + } + + if (!msg) + throw Error{Error::Code::Store, &gerr, "failed to find message for view"}; + + auto sexp{mu_msg_to_sexp(msg, docid, {}, message_options(params))}; + mu_msg_unref(msg); + + print_expr ("(:view %s)\n", sexp); + g_free (sexp); +} + + +static CommandMap +make_command_map (Context& context) +{ + CommandMap cmap; + + cmap.emplace("add", + CommandInfo{ + ArgMap{ {"path", ArgInfo{Type::String, true, "file system path to the message" }}}, + "add a message to the store", + [&](const auto& params){add_handler(context, params);}}); + + cmap.emplace("compose", + CommandInfo{ + ArgMap{{"type", ArgInfo{Type::Symbol, true, + "type of composition: reply/forward/edit/resend/new"}}, + {"docid", ArgInfo{Type::Integer, false,"document id of parent-message, if any"}}, + {"decrypt", ArgInfo{Type::Symbol, false, "whether to decrypt encrypted parts (if any)" }}}, + "get contact information", + [&](const auto& params){compose_handler(context, params);}}); + + cmap.emplace("contacts", + CommandInfo{ + ArgMap{ {"personal", ArgInfo{Type::Symbol, false, + "only personal contacts" }}, + {"after", ArgInfo{Type::String, false, + "only contacts seen after time_t string" }}, + {"tstamp", ArgInfo{Type::String, false, + "return changes since tstamp" }}}, + "get contact information", + [&](const auto& params){contacts_handler(context, params);}}); + + cmap.emplace("extract", + CommandInfo{ + ArgMap{{"docid", ArgInfo{Type::Integer, true, "document for the message" }}, + {"index", ArgInfo{Type::Integer, true, "index for the part to operate on" }}, + {"action", ArgInfo{Type::Symbol, true, "what to do with the part" }}, + {"decrypt", ArgInfo{Type::Symbol, false, + "whether to decrypt encrypted parts (if any)" }}, + {"path", ArgInfo{Type::String, false, "part for saving (for action: save)" }}, + {"what", ArgInfo{Type::Symbol, false, "what to do with the part (feedback)" }}, + {"param", ArgInfo{Type::String, false, "parameter for 'what'" }}}, + "extract mime-parts from a message", + [&](const auto& params){extract_handler(context, params);}}); + + cmap.emplace("find", + CommandInfo{ + ArgMap{ {"query", ArgInfo{Type::String, true, "search expression" }}, + {"threads", ArgInfo{Type::Symbol, false, + "whether to include threading information" }}, + {"sortfield", ArgInfo{Type::Symbol, false, "the field to sort results by" }}, + {"descending", ArgInfo{Type::Symbol, false, + "whether to sort in descending order" }}, + {"maxnum", ArgInfo{Type::Integer, false, + "maximum number of result (hint)" }}, + {"skip-dups", ArgInfo{Type::Symbol, false, + "whether to skip messages with duplicate message-ids" }}, + {"include-related", ArgInfo{Type::Symbol, false, + "whether to include other message related to matching ones" }}}, + "query the database for messages", + [&](const auto& params){find_handler(context, params);}}); + + cmap.emplace("help", + CommandInfo{ + ArgMap{ {"command", ArgInfo{Type::Symbol, false, + "command to get information for" }}, + {"full", ArgInfo{Type::Symbol, false, + "whether to include information about parameters" }}}, + "get information about one or all commands", + [&](const auto& params){help_handler(context, params);}}); + cmap.emplace("index", + CommandInfo{ + ArgMap{ {"my-addresses", ArgInfo{Type::List, false, "list of 'my' addresses"}}, + {"cleanup", ArgInfo{Type::Symbol, false, + "whether to remove stale messages from the store"}}, + {"lazy-check", ArgInfo{Type::Symbol, false, + "whether to avoid indexing up-to-date directories"}}}, + "scan maildir for new/updated/removed messages", + [&](const auto& params){index_handler(context, params);}}); + + cmap.emplace("move", + CommandInfo{ + ArgMap{{"docid", ArgInfo{Type::Integer, false, "document-id"}}, + {"msgid", ArgInfo{Type::String, false, "message-id"}}, + {"flags", ArgInfo{Type::String, false, "new flags for the message"}}, + {"maildir", ArgInfo{Type::String, false, "the target maildir" }}, + {"rename", ArgInfo{Type::Symbol, false, "change filename when moving" }}, + {"no-view", ArgInfo{Type::Symbol, false, + "if set, do not hint at updating the view"}},}, + "move messages and/or change their flags", + [&](const auto& params){move_handler(context, params);}}); + + cmap.emplace("mkdir", + CommandInfo{ + ArgMap{ {"path", ArgInfo{Type::String, true, + "location for the new maildir" }}}, + "create a new maildir", + [&](const auto& params){mkdir_handler(context, params);}}); + cmap.emplace("ping", + CommandInfo{ + ArgMap{ {"queries", ArgInfo{Type::List, false, + "queries for which to get read/unread numbers"}}, + {"skip-dups", ArgInfo{Type::Symbol, false, + "whether to exclude messages with duplicate message-ids"}},}, + "ping the mu-server and get information in response", + [&](const auto& params){ping_handler(context, params);}}); + + cmap.emplace("quit", + CommandInfo{{}, + "quit the mu server", + [&](const auto& params){quit_handler(context, params);}}); + + cmap.emplace("remove", + CommandInfo{ + ArgMap{ {"docid", ArgInfo{Type::Integer, true, + "document-id for the message to remove" }}}, + "remove a message from filesystem and database", + [&](const auto& params){remove_handler(context, params);}}); + + cmap.emplace("sent", + CommandInfo{ + ArgMap{ {"path", ArgInfo{Type::String, true, + "path to the message file" }} + }, + "tell mu about a message that was sent", + [&](const auto& params){sent_handler(context, params);}}); + + cmap.emplace("view", + CommandInfo{ + ArgMap{{"docid", ArgInfo{Type::Integer, false, "document-id"}}, + {"msgid", ArgInfo{Type::String, false, "message-id"}}, + {"path", ArgInfo{Type::String, false, "message filesystem path"}}, + + {"extract-images", ArgInfo{Type::Symbol, false, + "whether to extract images for this messages (if any)"}}, + {"decrypt", ArgInfo{Type::Symbol, false, + "whether to decrypt encrypted parts (if any)" }}, + {"verify", ArgInfo{Type::Symbol, false, + "whether to verify signatures (if any)" }} + + }, + "view a message. exactly one of docid/msgid/path must be specified", + [&](const auto& params){view_handler(context, params);}}); + return cmap; +} + + +static std::string +read_line(bool& do_quit) +{ + std::string line; + std::cout << ";; mu> "; + if (!std::getline(std::cin, line)) + do_quit = true; + return line; +} + +MuError +mu_cmd_server (MuConfig *opts, GError **err) try +{ + if (opts->commands) { + Context ctx{}; + auto cmap = make_command_map(ctx); + invoke(cmap, Sexp::parse("(help :full t)")); + return MU_OK; + } + + Context context{opts}; + context.command_map = make_command_map (context); + + if (opts->eval) { // evaluate command-line command & exit + auto call{Sexp::parse(opts->eval)}; + invoke(context.command_map, call); + return MU_OK; + } + + install_sig_handler(); + std::cout << ";; Welcome to the " << PACKAGE_STRING << " command-server\n" + << ";; Use (help) to get a list of commands, (quit) to quit.\n"; + + while (!MuTerminate && !context.do_quit) { + + std::string line; + try { + line = read_line(context.do_quit); + if (line.find_first_not_of(" \t") == std::string::npos) + continue; // skip whitespace-only lines + + auto call{Sexp::parse(line)}; + + invoke(context.command_map, call); + + } catch (const Error& er) { + std::cerr << ";; error: " << er.what() << "\n"; + print_error ((MuError)er.code(), "%s (line was:'%s')", + er.what(), line.c_str()); + } + } + + return MU_OK; + +} catch (const Error& er) { + g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", er.what()); + return MU_ERROR; +} catch (...) { + g_set_error(err, MU_ERROR_DOMAIN, MU_ERROR, "%s", "caught exception"); + return MU_ERROR; +} diff --git a/mu/mu-cmd.c b/mu/mu-cmd.c new file mode 100644 index 0000000..6c4c53b --- /dev/null +++ b/mu/mu-cmd.c @@ -0,0 +1,732 @@ +/* +** Copyright (C) 2010-2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "mu-msg.h" +#include "mu-msg-part.h" +#include "mu-cmd.h" +#include "mu-maildir.h" +#include "mu-contacts.hh" +#include "mu-runtime.h" +#include "mu-flags.h" + +#include "utils/mu-log.h" +#include "utils/mu-util.h" +#include "utils/mu-str.h" +#include "utils/mu-date.h" + +#define VIEW_TERMINATOR '\f' /* form-feed */ + +static gboolean +view_msg_sexp (MuMsg *msg, MuConfig *opts) +{ + char *sexp; + + sexp = mu_msg_to_sexp (msg, 0, NULL, mu_config_get_msg_options(opts)); + fputs (sexp, stdout); + g_free (sexp); + + return TRUE; +} + + +static void +each_part (MuMsg *msg, MuMsgPart *part, gchar **attach) +{ + char *fname, *tmp; + + if (!mu_msg_part_maybe_attachment (part)) + return; + + fname = mu_msg_part_get_filename (part, FALSE); + if (!fname) + return; + + tmp = *attach; + *attach = g_strdup_printf ("%s%s'%s'", + *attach ? *attach : "", + *attach ? ", " : "", + fname); + g_free (tmp); +} + +/* return comma-sep'd list of attachments */ +static gchar * +get_attach_str (MuMsg *msg, MuConfig *opts) +{ + gchar *attach; + MuMsgOptions msgopts; + + msgopts = mu_config_get_msg_options(opts) | + MU_MSG_OPTION_CONSOLE_PASSWORD; + + attach = NULL; + mu_msg_part_foreach (msg, msgopts, + (MuMsgPartForeachFunc)each_part, &attach); + return attach; +} + +#define color_maybe(C) do { if(color) fputs ((C),stdout);} while(0) + +static void +print_field (const char* field, const char *val, gboolean color) +{ + if (!val) + return; + + color_maybe (MU_COLOR_MAGENTA); + mu_util_fputs_encoded (field, stdout); + color_maybe (MU_COLOR_DEFAULT); + fputs (": ", stdout); + + if (val) { + color_maybe (MU_COLOR_GREEN); + mu_util_fputs_encoded (val, stdout); + } + + color_maybe (MU_COLOR_DEFAULT); + fputs ("\n", stdout); +} + + +/* a summary_len of 0 mean 'don't show summary, show body */ +static void +body_or_summary (MuMsg *msg, MuConfig *opts) +{ + const char *body; + gboolean color; + + color = !opts->nocolor; + body = mu_msg_get_body_text (msg, + mu_config_get_msg_options(opts) | + MU_MSG_OPTION_CONSOLE_PASSWORD); + if (!body) { + if (mu_msg_get_flags (msg) & MU_FLAG_ENCRYPTED) { + color_maybe (MU_COLOR_CYAN); + g_print ("[No body found; " + "message has encrypted parts]\n"); + } else { + color_maybe (MU_COLOR_MAGENTA); + g_print ("[No body found]\n"); + } + color_maybe (MU_COLOR_DEFAULT); + return; + } + + if (opts->summary_len != 0) { + gchar *summ; + summ = mu_str_summarize (body, opts->summary_len); + print_field ("Summary", summ, color); + g_free (summ); + } else { + mu_util_print_encoded ("%s", body); + if (!g_str_has_suffix (body, "\n")) + g_print ("\n"); + } +} + + +/* we ignore fields for now */ +/* summary_len == 0 means "no summary */ +static gboolean +view_msg_plain (MuMsg *msg, MuConfig *opts) +{ + gchar *attachs; + time_t date; + const GSList *lst; + gboolean color; + + color = !opts->nocolor; + + print_field ("From", mu_msg_get_from (msg), color); + print_field ("To", mu_msg_get_to (msg), color); + print_field ("Cc", mu_msg_get_cc (msg), color); + print_field ("Bcc", mu_msg_get_bcc (msg), color); + print_field ("Subject", mu_msg_get_subject (msg), color); + + if ((date = mu_msg_get_date (msg))) + print_field ("Date", mu_date_str_s ("%c", date), + color); + + if ((lst = mu_msg_get_tags (msg))) { + gchar *tags; + tags = mu_str_from_list (lst,','); + print_field ("Tags", tags, color); + g_free (tags); + } + + if ((attachs = get_attach_str (msg, opts))) { + print_field ("Attachments", attachs, color); + g_free (attachs); + } + + body_or_summary (msg, opts); + + return TRUE; +} + + +static gboolean +handle_msg (const char *fname, MuConfig *opts, GError **err) +{ + MuMsg *msg; + gboolean rv; + + msg = mu_msg_new_from_file (fname, NULL, err); + if (!msg) + return FALSE; + + switch (opts->format) { + case MU_CONFIG_FORMAT_PLAIN: + rv = view_msg_plain (msg, opts); + break; + case MU_CONFIG_FORMAT_SEXP: + rv = view_msg_sexp (msg, opts); + break; + default: + g_critical ("bug: should not be reached"); + rv = FALSE; + } + + mu_msg_unref (msg); + + return rv; +} + +static gboolean +view_params_valid (MuConfig *opts, GError **err) +{ + /* note: params[0] will be 'view' */ + if (!opts->params[0] || !opts->params[1]) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "error in parameters"); + return FALSE; + } + + switch (opts->format) { + case MU_CONFIG_FORMAT_PLAIN: + case MU_CONFIG_FORMAT_SEXP: + break; + default: + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "invalid output format"); + return FALSE; + } + + return TRUE; +} + + +static MuError +cmd_view (MuConfig *opts, GError **err) +{ + int i; + gboolean rv; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_VIEW, + MU_ERROR_INTERNAL); + + rv = view_params_valid (opts, err); + if (!rv) + goto leave; + + for (i = 1; opts->params[i]; ++i) { + + rv = handle_msg (opts->params[i], opts, err); + if (!rv) + break; + + /* add a separator between two messages? */ + if (opts->terminator) + g_print ("%c", VIEW_TERMINATOR); + } + +leave: + if (!rv) + return err && *err ? (*err)->code : MU_ERROR; + + return MU_OK; +} + +static MuError +cmd_mkdir (MuConfig *opts, GError **err) +{ + int i; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_MKDIR, + MU_ERROR_INTERNAL); + + if (!opts->params[1]) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing directory parameter"); + return MU_ERROR_IN_PARAMETERS; + } + + for (i = 1; opts->params[i]; ++i) + if (!mu_maildir_mkdir (opts->params[i], opts->dirmode, + FALSE, err)) + return err && *err ? (*err)->code : + MU_ERROR_FILE_CANNOT_MKDIR; + return MU_OK; +} + + +static gboolean +check_file_okay (const char *path, gboolean cmd_add) +{ + if (!g_path_is_absolute (path)) { + g_warning ("path is not absolute: %s", path); + return FALSE; + } + + if (cmd_add && access(path, R_OK) != 0) { + g_warning ("path is not readable: %s: %s", + path, strerror (errno)); + return FALSE; + } + + return TRUE; +} + + +typedef gboolean (*ForeachMsgFunc) (MuStore *store, const char *path, + GError **err); + + +static MuError +foreach_msg_file (MuStore *store, MuConfig *opts, + ForeachMsgFunc foreach_func, GError **err) +{ + unsigned u; + gboolean all_ok; + + /* note: params[0] will be 'add' */ + if (!opts->params[0] || !opts->params[1]) { + g_print ("usage: mu %s []\n", + opts->params[0] ? opts->params[0] : ""); + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing parameters"); + return MU_ERROR_IN_PARAMETERS; + } + + for (u = 1, all_ok = TRUE; opts->params[u]; ++u) { + + const char* path; + + path = opts->params[u]; + + if (!check_file_okay (path, TRUE)) { + all_ok = FALSE; + MU_WRITE_LOG ("not a valid message file: %s", path); + continue; + } + + if (!foreach_func (store, path, err)) { + all_ok = FALSE; + MU_WRITE_LOG ("error with %s: %s", path, + (err&&*err) ? (*err)->message : + "something went wrong"); + g_clear_error (err); + continue; + } + } + + if (!all_ok) { + mu_util_g_set_error (err, MU_ERROR_XAPIAN_STORE_FAILED, + "%s failed for some message(s)", + opts->params[0]); + return MU_ERROR_XAPIAN_STORE_FAILED; + } + + return MU_OK; +} + + +static gboolean +add_path_func (MuStore *store, const char *path, GError **err) +{ + return mu_store_add_path (store, path, err); +} + + +static MuError +cmd_add (MuStore *store, MuConfig *opts, GError **err) +{ + g_return_val_if_fail (store, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_ADD, + MU_ERROR_INTERNAL); + + return foreach_msg_file (store, opts, add_path_func, err); +} + +static gboolean +remove_path_func (MuStore *store, const char *path, GError **err) +{ + if (!mu_store_remove_path (store, path)) { + mu_util_g_set_error (err, MU_ERROR_XAPIAN_REMOVE_FAILED, + "failed to remove %s", path); + return FALSE; + } + + return TRUE; +} + +static MuError +cmd_remove (MuStore *store, MuConfig *opts, GError **err) +{ + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_REMOVE, + MU_ERROR_INTERNAL); + + return foreach_msg_file (store, opts, remove_path_func, err); +} + +static gboolean +tickle_func (MuStore *store, const char *path, GError **err) +{ + MuMsg *msg; + gboolean rv; + + msg = mu_msg_new_from_file (path, NULL, err); + if (!msg) + return FALSE; + + rv = mu_msg_tickle (msg, err); + mu_msg_unref (msg); + + return rv; +} + + +static MuError +cmd_tickle (MuStore *store, MuConfig *opts, GError **err) +{ + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_TICKLE, + MU_ERROR_INTERNAL); + + return foreach_msg_file (store, opts, tickle_func, err); +} + +struct _VData { + MuMsgPartSigStatus combined_status; + char *report; + gboolean oneline; +}; +typedef struct _VData VData; + +static void +each_sig (MuMsg *msg, MuMsgPart *part, VData *vdata) +{ + MuMsgPartSigStatusReport *report; + + report = part->sig_status_report; + if (!report) + return; + + if (vdata->oneline) + vdata->report = g_strdup_printf + ("%s%s%s", + vdata->report ? vdata->report : "", + vdata->report ? "; " : "", + report->report); + else + vdata->report = g_strdup_printf + ("%s%s\t%s", + vdata->report ? vdata->report : "", + vdata->report ? "\n" : "", + report->report); + + if (vdata->combined_status == MU_MSG_PART_SIG_STATUS_BAD || + vdata->combined_status == MU_MSG_PART_SIG_STATUS_ERROR) + return; + + vdata->combined_status = report->verdict; +} + + +static void +print_verdict (VData *vdata, gboolean color, gboolean verbose) +{ + g_print ("verdict: "); + + switch (vdata->combined_status) { + case MU_MSG_PART_SIG_STATUS_UNSIGNED: + g_print ("no signature found"); + break; + case MU_MSG_PART_SIG_STATUS_GOOD: + color_maybe (MU_COLOR_GREEN); + g_print ("signature(s) verified"); + break; + case MU_MSG_PART_SIG_STATUS_BAD: + color_maybe (MU_COLOR_RED); + g_print ("bad signature"); + break; + case MU_MSG_PART_SIG_STATUS_ERROR: + color_maybe (MU_COLOR_RED); + g_print ("verification failed"); + break; + case MU_MSG_PART_SIG_STATUS_FAIL: + color_maybe(MU_COLOR_RED); + g_print ("error in verification process"); + break; + default: g_return_if_reached (); + } + + color_maybe (MU_COLOR_DEFAULT); + if (vdata->report && verbose) + g_print ("%s%s\n", + (vdata->oneline) ? ";" : "\n", + vdata->report); + else + g_print ("\n"); +} + + +static MuError +cmd_verify (MuConfig *opts, GError **err) +{ + MuMsg *msg; + MuMsgOptions msgopts; + VData vdata; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + g_return_val_if_fail (opts->cmd == MU_CONFIG_CMD_VERIFY, + MU_ERROR_INTERNAL); + + if (!opts->params[1]) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing message-file parameter"); + return MU_ERROR_IN_PARAMETERS; + } + + msg = mu_msg_new_from_file (opts->params[1], NULL, err); + if (!msg) + return MU_ERROR; + + msgopts = mu_config_get_msg_options (opts) + | MU_MSG_OPTION_VERIFY + | MU_MSG_OPTION_CONSOLE_PASSWORD; + + vdata.report = NULL; + vdata.combined_status = MU_MSG_PART_SIG_STATUS_UNSIGNED; + vdata.oneline = FALSE; + + mu_msg_part_foreach (msg, msgopts, + (MuMsgPartForeachFunc)each_sig, &vdata); + + if (!opts->quiet) + print_verdict (&vdata, !opts->nocolor, opts->verbose); + + mu_msg_unref (msg); + g_free (vdata.report); + + return vdata.combined_status == MU_MSG_PART_SIG_STATUS_GOOD ? + MU_OK : MU_ERROR; +} + +static MuError +cmd_info (MuStore *store, MuConfig *opts, GError **err) +{ + mu_store_print_info (store, opts->nocolor); + + return MU_OK; +} + + +static MuError +cmd_init (MuConfig *opts, GError **err) +{ + MuStore *store; + const char *path; + + /* not provided, nor could we find a good default */ + if (!opts->maildir) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "missing --maildir parameter and could " + "not determine default"); + return MU_ERROR_IN_PARAMETERS; + } + + path = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB); + store = mu_store_new_create (path, + opts->maildir, + (const char**)opts->my_addresses, + err); + if (!store) + return MU_G_ERROR_CODE(err); + + if (!opts->quiet) { + mu_store_print_info (store, opts->nocolor); + g_print ("\nstore created.\n" + "use 'mu index' to fill the database " + "with your messages.\n" + "see mu-index(1) for details\n"); + } + + mu_store_unref (store); + return MU_OK; +} + + +static void +show_usage (void) +{ + g_print ("usage: mu command [options] [parameters]\n"); + g_print ("where command is one of index, find, cfind, view, mkdir, " + "extract, add, remove, script, verify or server\n"); + g_print ("see the mu, mu- or mu-easy manpages for " + "more information\n"); +} + +typedef MuError (*store_func) (MuStore *, MuConfig *, GError **err); + + +static MuError +with_store (store_func func, MuConfig *opts, gboolean read_only, GError **err) +{ + MuError merr; + MuStore *store; + const char *path; + + path = mu_runtime_path(MU_RUNTIME_PATH_XAPIANDB); + + if (read_only) + store = mu_store_new_readable (path, err); + else + store = mu_store_new_writable (path, err); + + if (!store) + return MU_G_ERROR_CODE(err); + + merr = func (store, opts, err); + mu_store_unref (store); + + return merr; +} + +static MuError +with_readonly_store (store_func func, MuConfig *opts, GError **err) +{ + return with_store (func, opts, TRUE, err); +} + +static MuError +with_writable_store (store_func func, MuConfig *opts, GError **err) +{ + return with_store (func, opts, FALSE, err); +} + +static gboolean +check_params (MuConfig *opts, GError **err) +{ + if (!opts->params||!opts->params[0]) {/* no command? */ + show_usage (); + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "error in parameters"); + return FALSE; + } + + return TRUE; +} + +static void +set_log_options (MuConfig *opts) +{ + MuLogOptions logopts; + + logopts = MU_LOG_OPTIONS_NONE; + + if (opts->quiet) + logopts |= MU_LOG_OPTIONS_QUIET; + if (!opts->nocolor) + logopts |= MU_LOG_OPTIONS_COLOR; + if (opts->log_stderr) + logopts |= MU_LOG_OPTIONS_STDERR; + if (opts->debug) + logopts |= MU_LOG_OPTIONS_DEBUG; +} + +MuError +mu_cmd_execute (MuConfig *opts, GError **err) +{ + MuError merr; + + g_return_val_if_fail (opts, MU_ERROR_INTERNAL); + + if (!check_params(opts, err)) + return MU_G_ERROR_CODE(err); + + set_log_options (opts); + + switch (opts->cmd) { + + /* already handled in mu-config.c */ + case MU_CONFIG_CMD_HELP: return MU_OK; + + /* no store needed */ + + case MU_CONFIG_CMD_MKDIR: merr = cmd_mkdir (opts, err); break; + case MU_CONFIG_CMD_SCRIPT: merr = mu_cmd_script (opts, err); break; + case MU_CONFIG_CMD_VIEW: merr = cmd_view (opts, err); break; + case MU_CONFIG_CMD_VERIFY: merr = cmd_verify (opts, err); break; + case MU_CONFIG_CMD_EXTRACT: merr = mu_cmd_extract (opts, err); break; + + /* read-only store */ + + case MU_CONFIG_CMD_CFIND: + merr = with_readonly_store (mu_cmd_cfind, opts, err); break; + case MU_CONFIG_CMD_FIND: + merr = with_readonly_store (mu_cmd_find, opts, err); break; + case MU_CONFIG_CMD_INFO: + merr = with_readonly_store (cmd_info, opts, err); break; + + /* writable store */ + + case MU_CONFIG_CMD_ADD: + merr = with_writable_store (cmd_add, opts, err); break; + case MU_CONFIG_CMD_REMOVE: + merr = with_writable_store (cmd_remove, opts, err); break; + case MU_CONFIG_CMD_TICKLE: + merr = with_writable_store (cmd_tickle, opts, err); break; + case MU_CONFIG_CMD_INDEX: + merr = with_writable_store (mu_cmd_index, opts, err); break; + + /* commands instantiate store themselves */ + case MU_CONFIG_CMD_INIT: + merr = cmd_init (opts,err); break; + case MU_CONFIG_CMD_SERVER: + merr = mu_cmd_server (opts, err); break; + + default: + merr = MU_ERROR_IN_PARAMETERS; break; + } + + return merr; +} diff --git a/mu/mu-cmd.h b/mu/mu-cmd.h new file mode 100644 index 0000000..b65cd15 --- /dev/null +++ b/mu/mu-cmd.h @@ -0,0 +1,111 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_CMD_H__ +#define __MU_CMD_H__ + +#include +#include +#include + +G_BEGIN_DECLS + +/** + * execute the 'find' command + * + * @param store store object to use + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeds and + * >MU_OK (0) results, MU_EXITCODE_NO_MATCHES if the command + * succeeds but there no matches, some error code for all other errors + */ +MuError mu_cmd_find (MuStore *store, MuConfig *opts, GError **err); + + +/** + * execute the 'extract' command + * + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeds, + * some error code otherwise + */ +MuError mu_cmd_extract (MuConfig *opts, GError **err); + + +/** + * execute the 'script' command + * + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeds, + * some error code otherwise + */ +MuError mu_cmd_script (MuConfig *opts, GError **err); + +/** + * execute the cfind command + * + * @param store store object to use + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeds, + * some error code otherwise + */ +MuError mu_cmd_cfind (MuStore *store, MuConfig *opts, GError **err); + +/** + * execute some mu command, based on 'opts' + * + * @param opts configuration option + * @param err receives error information, or NULL + * + * @return MU_OK if all went wall, some error code otherwise + */ +MuError mu_cmd_execute (MuConfig *opts, GError **err); + +/** + * execute the 'index' command + * + * @param store store object to use + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeded, + * some error code otherwise + */ +MuError mu_cmd_index (MuStore *store, MuConfig *opt, GError **err); + +/** + * execute the server command + * @param opts configuration options + * @param err receives error information, or NULL + * + * @return MU_OK (0) if the command succeeds, some error code otherwise + */ +MuError mu_cmd_server (MuConfig *opts, GError **err); + +G_END_DECLS + +#endif /*__MU_CMD_H__*/ diff --git a/mu/mu-config.c b/mu/mu-config.c new file mode 100644 index 0000000..89538cd --- /dev/null +++ b/mu/mu-config.c @@ -0,0 +1,809 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#if HAVE_CONFIG_H +#include "config.h" +#endif /*HAVE_CONFIG_H*/ + +#include +#include /* memset */ +#include +#include + +#include "mu-config.h" +#include "mu-cmd.h" + + +static MuConfig MU_CONFIG; + +#define color_maybe(C) (MU_CONFIG.nocolor ? "" : (C)) + + +static MuConfigFormat +get_output_format (const char *formatstr) +{ + int i; + struct { + const char* name; + MuConfigFormat format; + } formats [] = { + {"mutt-alias", MU_CONFIG_FORMAT_MUTT_ALIAS}, + {"mutt-ab", MU_CONFIG_FORMAT_MUTT_AB}, + {"wl", MU_CONFIG_FORMAT_WL}, + {"csv", MU_CONFIG_FORMAT_CSV}, + {"org-contact", MU_CONFIG_FORMAT_ORG_CONTACT}, + {"bbdb", MU_CONFIG_FORMAT_BBDB}, + {"links", MU_CONFIG_FORMAT_LINKS}, + {"plain", MU_CONFIG_FORMAT_PLAIN}, + {"sexp", MU_CONFIG_FORMAT_SEXP}, + {"json", MU_CONFIG_FORMAT_JSON}, + {"xml", MU_CONFIG_FORMAT_XML}, + {"xquery", MU_CONFIG_FORMAT_XQUERY}, + {"mquery", MU_CONFIG_FORMAT_MQUERY}, + {"debug", MU_CONFIG_FORMAT_DEBUG} + }; + + for (i = 0; i != G_N_ELEMENTS(formats); i++) + if (strcmp (formats[i].name, formatstr) == 0) + return formats[i].format; + + return MU_CONFIG_FORMAT_UNKNOWN; +} + + +#define expand_dir(D) \ + if ((D)) { \ + char *exp; \ + exp = mu_util_dir_expand((D)); \ + if (exp) { \ + g_free((D)); \ + (D) = exp; \ + } \ + } + + +static void +set_group_mu_defaults (void) +{ + /* If muhome is not set, we use the XDG Base Directory Specification + * locations. */ + if (MU_CONFIG.muhome) + expand_dir(MU_CONFIG.muhome); + + /* check for the MU_NOCOLOR or NO_COLOR env vars; but in any case don't + * use colors unless we're writing to a tty */ + if (g_getenv (MU_NOCOLOR) != NULL || g_getenv ("NO_COLOR") != NULL) + MU_CONFIG.nocolor = TRUE; + + if (!isatty(fileno(stdout)) || !isatty(fileno(stderr))) + MU_CONFIG.nocolor = TRUE; +} + +static GOptionGroup* +config_options_group_mu (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"debug", 'd', 0, G_OPTION_ARG_NONE, &MU_CONFIG.debug, + "print debug output to standard error (false)", NULL}, + {"quiet", 'q', 0, G_OPTION_ARG_NONE, &MU_CONFIG.quiet, + "don't give any progress information (false)", NULL}, + {"version", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.version, + "display version and copyright information (false)", NULL}, + {"muhome", 0, 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.muhome, + "specify an alternative mu directory", ""}, + {"log-stderr", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.log_stderr, + "log to standard error (false)", NULL}, + {"nocolor", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocolor, + "don't use ANSI-colors in output (false)", NULL}, + {"verbose", 'v', 0, G_OPTION_ARG_NONE, &MU_CONFIG.verbose, + "verbose output (false)", NULL}, + + {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, + &MU_CONFIG.params, "parameters", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("mu", "general mu options", "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + +static void +set_group_init_defaults (void) +{ + if (!MU_CONFIG.maildir) + MU_CONFIG.maildir = mu_util_guess_maildir(); + + expand_dir (MU_CONFIG.maildir); +} + +static GOptionGroup* +config_options_group_init (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"maildir", 'm', 0, G_OPTION_ARG_FILENAME, &MU_CONFIG.maildir, + "top of the maildir", ""}, + {"my-address", 0, 0, G_OPTION_ARG_STRING_ARRAY, + &MU_CONFIG.my_addresses, "my e-mail address; can be used multiple times", + "
"}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("init", "Options for the 'index' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + +static gboolean +index_post_parse_func (GOptionContext *context, GOptionGroup *group, + gpointer data, GError **error) +{ + if (!MU_CONFIG.maildir && !MU_CONFIG.my_addresses) + return TRUE; + + g_printerr ("%sNOTE%s: as of mu 1.3.8, 'mu index' no longer uses the\n" + "--maildir/-m or --my-address options.\n\n", + color_maybe(MU_COLOR_RED), color_maybe(MU_COLOR_DEFAULT)); + g_printerr ("Instead, these options should be passed to 'mu init'.\n"); + g_printerr ("See the mu-init(1) or the mu4e reference manual,\n'Initializing the message store' for details.\n\n"); + + return TRUE; +} + + +static GOptionGroup* +config_options_group_index (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + /* only here so we can tell users they are deprecated */ + {"maildir", 'm', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_FILENAME, + &MU_CONFIG.maildir, "top of the maildir", ""}, + {"my-address", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING_ARRAY, + &MU_CONFIG.my_addresses, "my e-mail address; can be used multiple times", + "
"}, + + {"lazy-check", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.lazycheck, + "only check dir-timestamps (false)", NULL}, + {"nocleanup", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.nocleanup, + "don't clean up the database after indexing (false)", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("index", "Options for the 'index' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + g_option_group_set_parse_hooks(og, NULL, (GOptionParseFunc)index_post_parse_func); + + return og; +} + +static void +set_group_find_defaults (void) +{ + /* note, when no fields are specified, we use + * date-from-subject, and sort descending by date. If fields + * *are* specified, we sort in ascending order. */ + if (!MU_CONFIG.fields || !*MU_CONFIG.fields) { + MU_CONFIG.fields = g_strdup ("d f s"); + if (!MU_CONFIG.sortfield) + MU_CONFIG.sortfield = g_strdup ("d"); + } + + if (!MU_CONFIG.formatstr) /* by default, use plain output */ + MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN; + else + MU_CONFIG.format = + get_output_format (MU_CONFIG.formatstr); + + expand_dir (MU_CONFIG.linksdir); +} + +static GOptionGroup* +config_options_group_find (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"fields", 'f', 0, G_OPTION_ARG_STRING, &MU_CONFIG.fields, + "fields to display in the output", ""}, + {"sortfield", 's', 0, G_OPTION_ARG_STRING, + &MU_CONFIG.sortfield, + "field to sort on", ""}, + {"maxnum", 'n', 0, G_OPTION_ARG_INT, &MU_CONFIG.maxnum, + "number of entries to display in the output", ""}, + {"threads", 't', 0, G_OPTION_ARG_NONE, &MU_CONFIG.threads, + "show message threads", NULL}, + {"bookmark", 'b', 0, G_OPTION_ARG_STRING, &MU_CONFIG.bookmark, + "use a bookmarked query", ""}, + {"reverse", 'z', 0, G_OPTION_ARG_NONE, &MU_CONFIG.reverse, + "sort in reverse (descending) order (z -> a)", NULL}, + {"skip-dups", 'u', 0, G_OPTION_ARG_NONE, + &MU_CONFIG.skip_dups, + "show only the first of messages duplicates (false)", NULL}, + {"include-related", 'r', 0, G_OPTION_ARG_NONE, + &MU_CONFIG.include_related, + "include related messages in results (false)", NULL}, + {"linksdir", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.linksdir, + "output as symbolic links to a target maildir", ""}, + {"clearlinks", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.clearlinks, + "clear old links before filling a linksdir (false)", NULL}, + {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr, + "output format ('plain'(*), 'links', 'xml'," + "'sexp', 'xquery')", ""}, + {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len, + "use up to lines for the summary, or 0 for none (0)", + ""}, + {"exec", 'e', 0, G_OPTION_ARG_STRING, &MU_CONFIG.exec, + "execute command on each match message", ""}, + {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after, + "only show messages whose m_time > T (t_time)", + ""}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("find", + "Options for the 'find' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + +static GOptionGroup * +config_options_group_mkdir (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"mode", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.dirmode, + "set the mode (as in chmod), in octal notation", ""}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + /* set dirmode before, because '0000' is a valid mode */ + MU_CONFIG.dirmode = 0755; + + og = g_option_group_new("mkdir", "Options for the 'mkdir' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + +static void +set_group_cfind_defaults (void) +{ + if (!MU_CONFIG.formatstr) /* by default, use plain output */ + MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN; + else + MU_CONFIG.format = get_output_format (MU_CONFIG.formatstr); +} + +static GOptionGroup * +config_options_group_cfind (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr, + "output format (plain(*), mutt-alias, mutt-ab, wl, " + "org-contact, bbdb, csv)", ""}, + {"personal", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.personal, + "whether to only get 'personal' contacts", NULL}, + {"after", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.after, + "only get addresses last seen after T", ""}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("cfind", "Options for the 'cfind' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + +static GOptionGroup * +config_options_group_script (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {G_OPTION_REMAINING, 0,0, G_OPTION_ARG_STRING_ARRAY, + &MU_CONFIG.params, "script parameters", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("script", "Options for the 'script' command", + "", NULL, NULL); + + g_option_group_add_entries(og, entries); + + return og; +} + +static void +set_group_view_defaults (void) +{ + if (!MU_CONFIG.formatstr) /* by default, use plain output */ + MU_CONFIG.format = MU_CONFIG_FORMAT_PLAIN; + else + MU_CONFIG.format = get_output_format (MU_CONFIG.formatstr); +} + + +/* crypto options are used in a few different commands */ +static GOptionEntry* +crypto_option_entries (void) +{ + static GOptionEntry entries[] = { + {"auto-retrieve", 'r', 0, G_OPTION_ARG_NONE, + &MU_CONFIG.auto_retrieve, + "attempt to retrieve keys online (false)", NULL}, + {"use-agent", 'a', 0, G_OPTION_ARG_NONE, &MU_CONFIG.use_agent, + "attempt to use the GPG agent (false)", NULL}, + {"decrypt", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.decrypt, + "attempt to decrypt the message", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + return entries; +} + +static GOptionGroup * +config_options_group_view (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"summary-len", 0, 0, G_OPTION_ARG_INT, &MU_CONFIG.summary_len, + "use up to lines for the summary, or 0 for none (0)", + ""}, + {"terminate", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.terminator, + "terminate messages with ascii-0x07 (\\f, form-feed)", + ""}, + {"format", 'o', 0, G_OPTION_ARG_STRING, &MU_CONFIG.formatstr, + "output format ('plain'(*), 'sexp')", ""}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("view", "Options for the 'view' command", + "", NULL, NULL); + + g_option_group_add_entries(og, entries); + g_option_group_add_entries(og, crypto_option_entries()); + + return og; +} + +static void +set_group_extract_defaults (void) +{ + if (!MU_CONFIG.targetdir) + MU_CONFIG.targetdir = g_strdup ("."); + + expand_dir (MU_CONFIG.targetdir); +} + + +static GOptionGroup* +config_options_group_extract (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"save-attachments", 'a', 0, G_OPTION_ARG_NONE, + &MU_CONFIG.save_attachments, + "save all attachments (false)", NULL}, + {"save-all", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.save_all, + "save all parts (incl. non-attachments) (false)", NULL}, + {"parts", 0, 0, G_OPTION_ARG_STRING, &MU_CONFIG.parts, + "save specific parts (comma-separated list)", ""}, + {"target-dir", 0, 0, G_OPTION_ARG_FILENAME, + &MU_CONFIG.targetdir, + "target directory for saving", ""}, + {"overwrite", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.overwrite, + "overwrite existing files (false)", NULL}, + {"play", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.play, + "try to 'play' (open) the extracted parts", NULL}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + og = g_option_group_new("extract", + "Options for the 'extract' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + g_option_group_add_entries(og, crypto_option_entries()); + + return og; +} + + +static GOptionGroup* +config_options_group_verify (void) +{ + GOptionGroup *og; + og = g_option_group_new("verify", + "Options for the 'verify' command", + "", NULL, NULL); + g_option_group_add_entries(og, crypto_option_entries()); + + return og; +} + + +static GOptionGroup* +config_options_group_server (void) +{ + GOptionGroup *og; + GOptionEntry entries[] = { + {"commands", 0, 0, G_OPTION_ARG_NONE, &MU_CONFIG.commands, + "list the available command and their parameters, then exit", NULL}, + {"eval", 'e', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING, + &MU_CONFIG.eval, "expression to evaluate", ""}, + {NULL, 0, 0, 0, NULL, NULL, NULL} + }; + + og = g_option_group_new("server", + "Options for the 'server' command", + "", NULL, NULL); + g_option_group_add_entries(og, entries); + + return og; +} + + +static MuConfigCmd +cmd_from_string (const char *str) +{ + int i; + struct { + const gchar* name; + MuConfigCmd cmd; + } cmd_map[] = { + { "add", MU_CONFIG_CMD_ADD }, + { "cfind", MU_CONFIG_CMD_CFIND }, + { "extract", MU_CONFIG_CMD_EXTRACT }, + { "find", MU_CONFIG_CMD_FIND }, + { "help", MU_CONFIG_CMD_HELP }, + { "index", MU_CONFIG_CMD_INDEX }, + { "info", MU_CONFIG_CMD_INFO }, + { "init", MU_CONFIG_CMD_INIT }, + { "mkdir", MU_CONFIG_CMD_MKDIR }, + { "remove", MU_CONFIG_CMD_REMOVE }, + { "script", MU_CONFIG_CMD_SCRIPT }, + { "server", MU_CONFIG_CMD_SERVER }, + { "tickle", MU_CONFIG_CMD_TICKLE }, + { "verify", MU_CONFIG_CMD_VERIFY }, + { "view", MU_CONFIG_CMD_VIEW } + }; + + if (!str) + return MU_CONFIG_CMD_UNKNOWN; + + for (i = 0; i != G_N_ELEMENTS(cmd_map); ++i) + if (strcmp (str, cmd_map[i].name) == 0) + return cmd_map[i].cmd; +#ifdef BUILD_GUILE + /* if we don't recognize it and it's not an option, it may be + * some script */ + if (str[0] != '-') + return MU_CONFIG_CMD_SCRIPT; +#endif /*BUILD_GUILE*/ + + return MU_CONFIG_CMD_UNKNOWN; +} + + + +static gboolean +parse_cmd (int *argcp, char ***argvp, GError **err) +{ + MU_CONFIG.cmd = MU_CONFIG_CMD_NONE; + MU_CONFIG.cmdstr = NULL; + + if (*argcp < 2) /* no command found at all */ + return TRUE; + else if ((**argvp)[1] == '-') + /* if the first param starts with '-', there is no + * command, just some option (like --version, --help + * etc.)*/ + return TRUE; + + MU_CONFIG.cmdstr = g_strdup ((*argvp)[1]); + MU_CONFIG.cmd = cmd_from_string (MU_CONFIG.cmdstr); + +#ifndef BUILD_GUILE + if (MU_CONFIG.cmd == MU_CONFIG_CMD_SCRIPT) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "command 'script' not supported"); + return FALSE; + } +#endif /*!BUILD_GUILE*/ + + if (MU_CONFIG.cmdstr && MU_CONFIG.cmdstr[0] != '-' && + MU_CONFIG.cmd == MU_CONFIG_CMD_UNKNOWN) { + mu_util_g_set_error (err, MU_ERROR_IN_PARAMETERS, + "unknown command '%s'", + MU_CONFIG.cmdstr); + return FALSE; + } + + return TRUE; +} + + +static GOptionGroup* +get_option_group (MuConfigCmd cmd) +{ + switch (cmd) { + case MU_CONFIG_CMD_CFIND: + return config_options_group_cfind(); + case MU_CONFIG_CMD_EXTRACT: + return config_options_group_extract(); + case MU_CONFIG_CMD_FIND: + return config_options_group_find(); + case MU_CONFIG_CMD_INDEX: + return config_options_group_index(); + case MU_CONFIG_CMD_INIT: + return config_options_group_init(); + case MU_CONFIG_CMD_MKDIR: + return config_options_group_mkdir(); + case MU_CONFIG_CMD_SERVER: + return config_options_group_server(); + case MU_CONFIG_CMD_SCRIPT: + return config_options_group_script(); + case MU_CONFIG_CMD_VERIFY: + return config_options_group_verify(); + case MU_CONFIG_CMD_VIEW: + return config_options_group_view(); + default: + return NULL; /* no group to add */ + } +} + + + +/* ugh yuck massaging the GOption text output; glib prepares some text + * which has a 'Usage:' for the 'help' command. However, we need the + * help for the command we're asking help for. So, we remove the Usage: + * from what glib generates. :-( */ +static gchar* +massage_help (const char *help) +{ + GRegex *rx; + char *str; + + rx = g_regex_new ("^Usage:.*\n.*\n", + 0, G_REGEX_MATCH_NEWLINE_ANY, NULL); + str = g_regex_replace (rx, help, + -1, 0, "", + G_REGEX_MATCH_NEWLINE_ANY, NULL); + g_regex_unref (rx); + return str; +} + + + +static const gchar* +get_help_string (MuConfigCmd cmd, gboolean long_help) +{ + unsigned u; + + /* this include gets us MU_HELP_STRINGS */ +#include "mu-help-strings.h" + + for (u = 0; u != G_N_ELEMENTS(MU_HELP_STRINGS); ++u) + if (cmd == MU_HELP_STRINGS[u].cmd) { + if (long_help) + return MU_HELP_STRINGS[u].long_help; + else + return MU_HELP_STRINGS[u].usage ; + } + + g_return_val_if_reached (""); + return ""; +} + + +void +mu_config_show_help (MuConfigCmd cmd) +{ + GOptionContext *ctx; + GOptionGroup *group; + char *help, *cleanhelp; + + g_return_if_fail (mu_config_cmd_is_valid(cmd)); + + ctx = g_option_context_new ("- mu help"); + g_option_context_set_main_group (ctx, config_options_group_mu()); + + group = get_option_group (cmd); + if (group) + g_option_context_add_group (ctx, group); + + g_option_context_set_description (ctx, get_help_string (cmd, TRUE)); + help = g_option_context_get_help (ctx, TRUE, group); + cleanhelp = massage_help (help); + + g_print ("usage:\n\t%s%s", + get_help_string (cmd, FALSE), cleanhelp); + + g_free (help); + g_free (cleanhelp); + g_option_context_free (ctx); +} + +static gboolean +cmd_help (void) +{ + MuConfigCmd cmd; + + if (!MU_CONFIG.params) + cmd = MU_CONFIG_CMD_UNKNOWN; + else + cmd = cmd_from_string (MU_CONFIG.params[1]); + + if (cmd == MU_CONFIG_CMD_UNKNOWN) { + mu_config_show_help (MU_CONFIG_CMD_HELP); + return TRUE; + } + + mu_config_show_help (cmd); + + return TRUE; +} + +static gboolean +parse_params (int *argcp, char ***argvp, GError **err) +{ + GOptionContext *context; + GOptionGroup *group; + gboolean rv; + + context = g_option_context_new("- mu general options"); + + g_option_context_set_help_enabled (context, TRUE); + rv = TRUE; + + g_option_context_set_main_group(context, + config_options_group_mu()); + g_option_context_set_ignore_unknown_options (context, FALSE); + + switch (MU_CONFIG.cmd) { + case MU_CONFIG_CMD_NONE: + case MU_CONFIG_CMD_HELP: + /* 'help' is special; sucks in the options of the + * command after it */ + rv = g_option_context_parse (context, argcp, argvp, err) && + cmd_help (); + break; + case MU_CONFIG_CMD_SCRIPT: + /* all unknown commands are passed to 'script' */ + g_option_context_set_ignore_unknown_options (context, TRUE); + group = get_option_group (MU_CONFIG.cmd); + g_option_context_add_group (context, group); + rv = g_option_context_parse (context, argcp, argvp, err); + MU_CONFIG.script = g_strdup (MU_CONFIG.cmdstr); + /* argvp contains the script parameters */ + MU_CONFIG.script_params = (const char**)&((*argvp)[1]); + break; + + default: + group = get_option_group (MU_CONFIG.cmd); + if (group) + g_option_context_add_group (context, group); + + rv = g_option_context_parse (context, argcp, argvp, err); + break; + } + + g_option_context_free (context); + + return rv ? TRUE : FALSE; +} + + +MuConfig* +mu_config_init (int *argcp, char ***argvp, GError **err) +{ + g_return_val_if_fail (argcp && argvp, NULL); + + memset (&MU_CONFIG, 0, sizeof(MU_CONFIG)); + + MU_CONFIG.maxnum = -1; /* By default, output all matching entries. */ + + if (!parse_cmd (argcp, argvp, err)) + goto errexit; + + if (!parse_params(argcp, argvp, err)) + goto errexit; + + /* fill in the defaults if user did not specify */ + set_group_mu_defaults(); + set_group_init_defaults(); + set_group_find_defaults(); + set_group_cfind_defaults(); + set_group_view_defaults(); + set_group_extract_defaults(); + /* set_group_mkdir_defaults (config); */ + + return &MU_CONFIG; + +errexit: + mu_config_uninit (&MU_CONFIG); + return NULL; +} + + +void +mu_config_uninit (MuConfig *opts) +{ + if (!opts) + return; + + g_free (opts->cmdstr); + g_free (opts->muhome); + g_free (opts->maildir); + g_free (opts->fields); + g_free (opts->sortfield); + g_free (opts->bookmark); + g_free (opts->formatstr); + g_free (opts->exec); + g_free (opts->linksdir); + g_free (opts->targetdir); + g_free (opts->parts); + g_free (opts->script); + g_free (opts->eval); + + g_strfreev (opts->params); + + memset (opts, 0, sizeof(MU_CONFIG)); +} + +size_t +mu_config_param_num (MuConfig *opts) +{ + size_t n; + + g_return_val_if_fail (opts && opts->params, 0); + for (n = 0; opts->params[n]; ++n); + + return n; +} + + +MuMsgOptions +mu_config_get_msg_options (MuConfig *muopts) +{ + MuMsgOptions opts; + + opts = MU_MSG_OPTION_NONE; + + if (muopts->decrypt) + opts |= MU_MSG_OPTION_DECRYPT; + if (muopts->verify) + opts |= MU_MSG_OPTION_VERIFY; + if (muopts->use_agent) + opts |= MU_MSG_OPTION_USE_AGENT; + if (muopts->auto_retrieve) + opts |= MU_MSG_OPTION_AUTO_RETRIEVE; + if (muopts->overwrite) + opts |= MU_MSG_OPTION_OVERWRITE; + + return opts; +} diff --git a/mu/mu-config.h b/mu/mu-config.h new file mode 100644 index 0000000..43418ae --- /dev/null +++ b/mu/mu-config.h @@ -0,0 +1,260 @@ +/* +** Copyright (C) 2008-2020 Dirk-Jan C. Binnema +** +** This program is free software; you can redistribute it and/or modify it +** under the terms of the GNU General Public License as published by the +** Free Software Foundation; either version 3, or (at your option) any +** later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software Foundation, +** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +** +*/ + +#ifndef __MU_CONFIG_H__ +#define __MU_CONFIG_H__ + +#include +#include /* for mode_t */ +#include +#include +#include + +G_BEGIN_DECLS + +/* env var; if non-empty, color are disabled */ +#define MU_NOCOLOR "MU_NOCOLOR" + + +typedef enum { + MU_CONFIG_FORMAT_UNKNOWN = 0, + + /* for cfind, find, view */ + MU_CONFIG_FORMAT_PLAIN, /* plain output */ + + /* for cfind */ + MU_CONFIG_FORMAT_MUTT_ALIAS, /* mutt alias style */ + MU_CONFIG_FORMAT_MUTT_AB, /* mutt ext abook */ + MU_CONFIG_FORMAT_WL, /* Wanderlust abook */ + MU_CONFIG_FORMAT_CSV, /* comma-sep'd values */ + MU_CONFIG_FORMAT_ORG_CONTACT, /* org-contact */ + MU_CONFIG_FORMAT_BBDB, /* BBDB */ + MU_CONFIG_FORMAT_DEBUG, + + /* for find, view */ + MU_CONFIG_FORMAT_SEXP, /* output sexps (emacs) */ + MU_CONFIG_FORMAT_JSON, /* output JSON */ + + /* for find */ + MU_CONFIG_FORMAT_LINKS, /* output as symlinks */ + MU_CONFIG_FORMAT_XML, /* output xml */ + MU_CONFIG_FORMAT_XQUERY, /* output the xapian query */ + MU_CONFIG_FORMAT_MQUERY, /* output the mux query */ + + MU_CONFIG_FORMAT_EXEC /* execute some command */ +} MuConfigFormat; + +typedef enum { + MU_CONFIG_CMD_UNKNOWN = 0, + + MU_CONFIG_CMD_ADD, + MU_CONFIG_CMD_CFIND, + MU_CONFIG_CMD_EXTRACT, + MU_CONFIG_CMD_FIND, + MU_CONFIG_CMD_HELP, + MU_CONFIG_CMD_INDEX, + MU_CONFIG_CMD_INFO, + MU_CONFIG_CMD_INIT, + MU_CONFIG_CMD_MKDIR, + MU_CONFIG_CMD_REMOVE, + MU_CONFIG_CMD_SCRIPT, + MU_CONFIG_CMD_SERVER, + MU_CONFIG_CMD_TICKLE, + MU_CONFIG_CMD_VERIFY, + MU_CONFIG_CMD_VIEW, + + MU_CONFIG_CMD_NONE +} MuConfigCmd; + + +#define mu_config_cmd_is_valid(C) \ + ((C) > MU_CONFIG_CMD_UNKNOWN && (C) < MU_CONFIG_CMD_NONE) + + +/* struct with all configuration options for mu; it will be filled + * from the config file, and/or command line arguments */ + +struct _MuConfig { + + MuConfigCmd cmd; /* the command, or + * MU_CONFIG_CMD_NONE */ + char *cmdstr; /* cmd string, for user + * info */ + /* general options */ + gboolean quiet; /* don't give any output */ + gboolean debug; /* spew out debug info */ + gchar *muhome; /* the House of Mu */ + gboolean version; /* request mu version */ + gboolean log_stderr; /* log to stderr (not logfile) */ + gchar** params; /* parameters (for querying) */ + gboolean nocolor; /* don't use use ansi-colors + * in some output */ + gboolean verbose; /* verbose output */ + + /* options for init */ + gchar *maildir; /* where the mails are */ + char** my_addresses; /* 'my e-mail address', for mu + * cfind; can be use multiple + * times */ + /* options for indexing */ + + gboolean nocleanup; /* don't cleanup del'd mails from db */ + gboolean rebuild; /* empty the database before indexing */ + gboolean lazycheck; /* don't check dirs with up-to-date + * timestamps */ + int max_msg_size; /* maximum size for message files */ + + /* options for querying 'find' (and view-> 'summary') */ + gchar *fields; /* fields to show in output */ + gchar *sortfield; /* field to sort by (string) */ + int maxnum; /* max # of entries to print */ + gboolean reverse; /* sort in revers order (z->a) */ + gboolean threads; /* show message threads */ + + gboolean summary; /* OBSOLETE: use summary_len */ + int summary_len; /* max # of lines for summary */ + + gchar *bookmark; /* use bookmark */ + gchar *formatstr; /* output type for find + * (plain,links,xml,json,sexp) + * and view (plain, sexp) and cfind + */ + MuConfigFormat format; /* the decoded formatstr */ + gchar *exec; /* command to execute on the + * files for the matched + * messages */ + gboolean skip_dups; /* if there are multiple + * messages with the same + * msgid, show only the first + * one */ + gboolean include_related; /* included related messages + * in results */ + /* for find and cind */ + time_t after; /* only show messages or + * addresses last seen after + * T */ + /* options for crypto + * ie, 'view', 'extract' */ + gboolean auto_retrieve; /* assume we're online */ + gboolean use_agent; /* attempt to use the gpg-agent */ + gboolean decrypt; /* try to decrypt the + * message body, if any */ + gboolean verify; /* try to crypto-verify the + * message */ + + /* options for view */ + gboolean terminator; /* add separator \f between + * multiple messages in mu + * view */ + + /* options for cfind (and 'find' --> "after") */ + gboolean personal; /* only show 'personal' addresses */ + /* also 'after' --> see above */ + + /* output to a maildir with symlinks */ + gchar *linksdir; /* maildir to output symlinks */ + gboolean clearlinks; /* clear a linksdir before filling */ + mode_t dirmode; /* mode for the created maildir */ + + /* options for extracting parts */ + gboolean save_all; /* extract all parts */ + gboolean save_attachments; /* extract all attachment parts */ + gchar *parts; /* comma-sep'd list of parts + * to save / open */ + gchar *targetdir; /* where to save the attachments */ + gboolean overwrite; /* should we overwrite same-named files */ + gboolean play; /* after saving, try to 'play' + * (open) the attmnt using xdgopen */ + /* for server */ + gboolean commands; /* dump documentations for server + * commands */ + gchar *eval; /* command to evaluate */ + + /* options for mu-script */ + gchar *script; /* script to run */ + const char **script_params; /* parameters for scripts */ +}; +typedef struct _MuConfig MuConfig; + +/** + * initialize a mu config object + * + * set default values for the configuration options; when you call + * mu_config_init, you should also call mu_config_uninit when the data + * is no longer needed. + * + * Note that this is _static_ data, ie., mu_config_init will always + * return the same pointer + * + * @param argcp: pointer to argc + * @param argvp: pointer to argv + * @param err: receives error information + */ +MuConfig *mu_config_init (int *argcp, char ***argvp, GError **err) + G_GNUC_WARN_UNUSED_RESULT; +/** + * free the MuConfig structure + * + * @param opts a MuConfig struct, or NULL + */ +void mu_config_uninit (MuConfig *conf); + + +/** + * execute the command / options in this config + * + * @param opts a MuConfig struct + * + * @return a value denoting the success/failure of the execution; + * MU_ERROR_NONE (0) for success, non-zero for a failure. This is to used for + * the exit code of the process + * + */ +MuError mu_config_execute (MuConfig *conf); + +/** + * count the number of non-option parameters + * + * @param opts a MuConfig struct + * + * @return the number of non-option parameters, or 0 in case of error + */ +size_t mu_config_param_num (MuConfig *conf); + + +/** + * determine MuMsgOptions for command line args + * + * @param opts a MuConfig struct + * + * @return the corresponding MuMsgOptions + */ +MuMsgOptions mu_config_get_msg_options (MuConfig *opts); + + +/** + * print help text for the current command + * + * @param cmd the command to show help for + */ +void mu_config_show_help (MuConfigCmd cmd); + +G_END_DECLS + +#endif /*__MU_CONFIG_H__*/ diff --git a/mu/mu-help-strings.awk b/mu/mu-help-strings.awk new file mode 100644 index 0000000..81f4606 --- /dev/null +++ b/mu/mu-help-strings.awk @@ -0,0 +1,69 @@ +## Copyright (C) 2012 Dirk-Jan C. Binnema +## +## This program is free software; you can redistribute it and/or modify +## it under the terms of the GNU General Public License as published by +## the Free Software Foundation; either version 3 of the License, or +## (at your option) any later version. +## +## This program is distributed in the hope that it will be useful, +## but WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with this program; if not, write to the Free Software Foundation, +## Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +## convert text blobs statements into c-strings + +BEGIN { + in_def=0; + in_string=0; +# srand(); +# guard=int(100000*rand()); +# print "#ifndef __" guard "__" + print "/* Do not edit - auto-generated. */" + print "static const struct {" + print "\tMuConfigCmd cmd;" + print "\tconst char *usage;" + print "\tconst char *long_help;" + print "} MU_HELP_STRINGS[] = {" +} + + +/^#BEGIN/ { + print "\t{ " $2 "," # e.g., MU_CONFIG_CMD_ADD + in_def=1 +} + +/^#STRING/ { + if (in_def== 1) { + if (in_string==1) { + print ","; + } + in_string=1 + } +} + +/^#END/ { + if (in_string==1) { + in_string=0; + } + in_def=0; + print "\n\t},\n" +} + + +!/^#/ { + if (in_string==1) { + printf "\n\t\"" $0 "\\n\"" + } +} + + +END { + print "};" +# print "#endif /*" guard "*/" + print "/* the end */" +} diff --git a/mu/mu-help-strings.txt b/mu/mu-help-strings.txt new file mode 100644 index 0000000..a18a93a --- /dev/null +++ b/mu/mu-help-strings.txt @@ -0,0 +1,198 @@ +#-*-mode:org-*- +# +# Copyright (C) 2012-2020 Dirk-Jan C. Binnema +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + +#BEGIN MU_CONFIG_CMD_ADD +#STRING +mu add [] +#STRING +mu add is the command to add specific measage files to the database. Each of the +files must be specified with an absolute path. +#END + +#BEGIN MU_CONFIG_CMD_CFIND +#STRING +mu cfind [options] [--format=] [--personal] [--after=] [] +#STRING +mu cfind is the mu command to find contacts in the mu database and export them +for use in other programs. + + is one of: + plain + mutt-alias + mutt-ab + wl + csv + org-contact + bbdb + +'plain' is the default. + +If you specify '--personal', only addresses that were found in mails +that include 'my' e-mail address will be listed - so to exclude e.g. +mailing-list posts. Use the --my-address= option in 'mu index' to +specify what addresses are considered 'my' address. + +With '--after=T' you can tell mu to only show addresses that were seen after +T. T is a Unix timestamp. For example, to get only addresses seen after the +beginning of 2012, you could use + --after=`date +%%s -d 2012-01-01` +#END + +#BEGIN MU_CONFIG_CMD_EXTRACT +#STRING +mu extract [options] +#STRING +mu extract is the mu command to display and save message parts +(attachments), and open them with other tools. +#END + +#BEGIN MU_CONFIG_CMD_FIND +#STRING +mu find [options] +#STRING +mu find is the mu command for searching e-mail message that were +stored earlier using mu index(1). + +Some examples: + # get all messages with 'bananas' in body, subject or recipient fields: + $ mu find bananas + + # get all messages regarding bananas from John with an attachment: + $ mu find from:john flag:attach bananas + + # get all messages with subject wombat in June 2009 + $ mu find subject:wombat date:20090601..20090630 + +See the `mu-find' and `mu-easy' man-pages for more information. +#END + +#BEGIN MU_CONFIG_CMD_HELP +#STRING +mu help +#STRING +mu help is the mu command to get help about , where +is one of: + add - add message to database + cfind - find a contact + extract - extract parts/attachments from messages + find - query the message database + help - get help + index - index messages + init - init the mu database + mkdir - create a maildir + remove - remove a message from the database + script - run a script (available only when mu was built with guile-support) + server - start mu server + verify - verify signatures of a message + view - view a specific message +#END + +#BEGIN MU_CONFIG_CMD_INDEX +#STRING +mu index [options] +#STRING +mu index is the mu command for scanning the contents of Maildir +directories and storing the results in a Xapian database.The +data can then be queried using mu-find(1). +#END + +#BEGIN MU_CONFIG_CMD_INIT +#STRING +mu init [options] +#STRING +mu init is the mu command for setting up the mu database. +#END + +#BEGIN MU_CONFIG_CMD_INFO +#STRING +mu init [options] +#STRING +mu info is the command for getting information about a mu database. +#END + +#BEGIN MU_CONFIG_CMD_MKDIR +#STRING +mu mkdir [options] [] +#STRING +mu mkdir is the command for creating Maildirs.It does not +use the mu database. +#END + +#BEGIN MU_CONFIG_CMD_REMOVE +#STRING +mu remove [options] [] +#STRING +mu remove is the mu command to remove messages from the database. +#END + +#BEGIN MU_CONFIG_CMD_SERVER +#STRING +mu server [options] +#STRING +mu server starts a simple shell in which one can query and +manipulate the mu database.The output of the commands is terms +of Lisp symbolic expressions (s-exps). Its main use is for +the mu4e e-mail client. +#END + +#BEGIN MU_CONFIG_CMD_SCRIPT +#STRING +mu script [] [-v] +mu [ + +#+end_html diff --git a/www/mu-small.png b/www/mu-small.png new file mode 100644 index 0000000..da133a6 Binary files /dev/null and b/www/mu-small.png differ diff --git a/www/mu.css b/www/mu.css new file mode 100644 index 0000000..ecbaca0 --- /dev/null +++ b/www/mu.css @@ -0,0 +1,116 @@ +/* stylesheet for mu website */ + +body { + background:#ffffff; + margin: 50px; + padding: 10px; + font-family: arial, Helvetica, 'Bitstream Vera Sans', 'Luxi Sans', Verdana, + Sans-Serif; + font-size: 12px; +} + +.content { + margin:30px; + background: #5fb6de; +} + + +a.menu {font-weight: bold} +a.menu:link {color: #ffffff; text-decoration: none; } +a.menu:active {color: #ff0000; text-decoration: none; } +a.menu:visited {color: #ffffff; text-decoration: none; } +a.menu:hover {color: #ff0000; text-decoration: underline; } + +.mine {color: #ffffff; font-weight: bold} + + + + +/* emacs-code -----------------------*/ + +/* zenburnesque code blocks in for html-exported org mode */ + + +pre.src { + background: #dddddd; + color: #555555; +} + +.org-preprocessor { + color: #8cd0d3; +} + +.org-variable-name { + color: #0084C8; + font-weight: bold; +} + +.org-string { + color: #4E9A06; +} + +.org-type { + color: #2F8B58; + font-weight: bold; +} + +.org-function-name { + color: #00578E; + font-weight: bold +} + +.org-keyword { + color: #A52A2A; + font-weight: bold; +} + +.org-comment { + color: #204A87; +} + +.org-doc { + color: #afd8af; +} + +.org-comment-delimiter { + color: #708070; +} + +.org-constant { + color: #F5666D; +} + +.org-builtin { + color: #A020F0; +} + + +.warning, .org-warning { + color: yellow; + font-weight: bold +} + + +/* emacs other stuff --------------------- */ + .org-date, .org-org-date { + /* org-date */ + color: #00ffff; + text-decoration: underline; + } + .org-hide, .org-org-hide { + /* org-hide */ + color: #000000; + } + .org-level-1, .org-org-level-1 { + /* org-level-1 */ + color: #356da0; + } + .org-level-2,.org-org-level-2 { + /* org-level-2 */ + color: #7685de; + } + .org-todo,.org-org-todo { + /* org-todo */ + color: #ffc0cb; + font-weight: bold; + } diff --git a/www/mu.jpg b/www/mu.jpg new file mode 100644 index 0000000..14d0302 Binary files /dev/null and b/www/mu.jpg differ diff --git a/www/mu.png b/www/mu.png new file mode 100644 index 0000000..dd9e91f Binary files /dev/null and b/www/mu.png differ diff --git a/www/mu4e-1.png b/www/mu4e-1.png new file mode 100644 index 0000000..55988fe Binary files /dev/null and b/www/mu4e-1.png differ diff --git a/www/mu4e-2.png b/www/mu4e-2.png new file mode 100644 index 0000000..eade6d7 Binary files /dev/null and b/www/mu4e-2.png differ diff --git a/www/mu4e-3.png b/www/mu4e-3.png new file mode 100644 index 0000000..23a57bf Binary files /dev/null and b/www/mu4e-3.png differ diff --git a/www/mu4e-splitview-small.png b/www/mu4e-splitview-small.png new file mode 100644 index 0000000..ad05988 Binary files /dev/null and b/www/mu4e-splitview-small.png differ diff --git a/www/mu4e-splitview.png b/www/mu4e-splitview.png new file mode 100644 index 0000000..7e50549 Binary files /dev/null and b/www/mu4e-splitview.png differ diff --git a/www/mu4e.md b/www/mu4e.md new file mode 100644 index 0000000..cd361fc --- /dev/null +++ b/www/mu4e.md @@ -0,0 +1,59 @@ +--- +layout: default +permalink: /code/mu/mu4e.html +--- + +Starting with version 0.9.8, [mu](http://www.djcbsoftware.nl/code/mu) +provides an emacs-based e-mail client which uses `mu` as its back-end: +*mu4e*. + +Through `mu`, `mu4e` sits on top of your Maildir (which you update +with e.g. [`offlineimap`](http://offlineimap.org/), +[`mbsync`](http://isync.sourceforge.net) or +[`fetchmail`](http://www.fetchmail.info/)). `mu4e` is designed to +enable super-efficient handling of e-mail; searching, reading, +replying, moving, deleting. The overall 'feel' is a bit of a mix of +[`dired`](http://www.gnu.org/software/emacs/manual/html_node/emacs/Dired.html) +and [Wanderlust](http://www.gohome.org/wl/). + +Features include: + + - Fully search-based: there are no folders, only queries + - UI optimized for speed with quick key strokes for common actions + - Fully documented, with example configurations + - Asynchronous: heavy actions never block emacs + - Write rich-text e-mails using /org-mode/ (experimental) + - Address auto-completion based on your messages -- no need for + managing address books + - Extendable in many places using custom actions + +For all the details, please see the [manual](mu4e/), or +check the screenshots below. `mu4e` is part of the normal +[mu source package](http://code.google.com/p/mu0/downloads/list) and +also [available on Github](https://github.com/djcb/mu). + +# Screenshots + +## The main view + + +## The headers view + + +## The message view + + +## The message/headers split view (0.9.8.4) + + + +The message/headers split view, and speedbar support. + +## View message as pdf (0.9.8.4) + + + +## License & Copyright + +*mu4e* was designed and implemented by Dirk-Jan C. Binnema, and is +Free Software, licensed under the GNU GPLv3 diff --git a/www/mu4egraph.png b/www/mu4egraph.png new file mode 100644 index 0000000..089a744 Binary files /dev/null and b/www/mu4egraph.png differ diff --git a/www/mug-full.png b/www/mug-full.png new file mode 100644 index 0000000..8aff20f Binary files /dev/null and b/www/mug-full.png differ diff --git a/www/mug-thumb.png b/www/mug-thumb.png new file mode 100644 index 0000000..012c8ab Binary files /dev/null and b/www/mug-thumb.png differ diff --git a/www/mug.org b/www/mug.org new file mode 100644 index 0000000..3737e83 --- /dev/null +++ b/www/mug.org @@ -0,0 +1,39 @@ +#+title: Mug +#+html: +#+style: + + +* Mug +/Mug/ is a toy/demo user-interface for =mu=. It is not installable, you'll need +to run it from its source directory. + +Mug comes in two flavors: + - =mug= (in toys/mug), old simple UI, only adding dependency to GTK+ + - =mug2= (in toys/mug2), the new UI, which requires GTK+, Webkit and a + recent GLib. + +The plan for =mug= is to be a testing ground for the widget-code which will +slowly evolve into a full-featured UI. + +#+html: + +=mug2= supports: + - HTML email + - attachments (including in-place opening, drag & drop to desktop) + - bookmarks (see the =mu-bookmarks= man page, the UI will load these in the + left pane) + - view source + +#+html:
© 2011 Dirk-Jan C. Binnema
+#+begin_html + + +#+end_html + + diff --git a/www/old-news.md b/www/old-news.md new file mode 100644 index 0000000..04178e4 --- /dev/null +++ b/www/old-news.md @@ -0,0 +1,90 @@ +--- +layout: default +permalink: code/mu/old-news.html +--- + +# Old news + +- 2011-07-31: mu *0.9.7-pre* is now available with a number of interesting +new features and fixes, many based on user suggestions. `mu` now supports +/mail threading/ based on the [JWZ-algorithm](http://www.jwz.org/doc/threading.html); output is now automatically +converted to the user-locale; `mu view` can output separators between +messages for easier processing, support for X-Label-tags, and last but not +least, `mu` now has bindings for the [Guile](http://www.gnu.org/s/guile/) (Scheme) programming language - +there is a new toy (`toys/muile`) that allows you to inspect messages and +do all kinds of statistics - see the [README](https://gitorious.org/mu/mu/blobs/master/toys/muile/README) for more information. + +- 2011-06-02: after quite a bit of testing, *0.9.6* has been promoted to be +the next release -- forget about the 'bèta'. Development continues for +the next release. + +- 2011-05-28: *mu-0.9.6* (bèta). A lot of internal changes, but also quite +some new features, for example: +- wild-card searching for most fields: mu find 'car*' +- search for message with certain attachments with 'attach:/a:': mu find +'attach:resume*' +- color for `mu find`, `mu cfind`, `mu extract` and `mu view` +Everything is documented in the man-pages, and there are examples in the [[file:cheatsheet.org][mu +cheatsheet]]. + +- 2011-04-25: *mu-0.9.5* a small, but important, bugfix in maildir-detection, +some small optimizations. + +- 2011-04-12: *mu 0.9.4* released - adds the `cfind` command, to find +contacts (name + e-mail); add `flag:unread` which is a synonym for +`flag:new OR NOT flag:seen`. Updates to the documentation and some internal +updates. This is a *bèta-version*. + +- 2011-02-13: *mu 0.9.3*; fixes a bunch of minor issues in 0.9.2; updated the +web page with pages for [mu cheatsheet](file:mug.org][mug]] (the experimental UI) and the [[file:cheatsheet.org). + +- 2011-02-02: *mu 0.9.2* released, which adds support for matching on message +size, and various new output format. See [NEWS](http://gitorious.org/mu/mu/blobs/master/NEWS) for all the user-visible +changes, also from older releases. + + +- [2010-12-05] *mu version 0.9.1* released; fixes a couple of issues users +found with a missing icon, the unit-tests. +- [2010-12-04] *mu version 0.9* released. Compared to the bèta-release, there +were a number of improvements to the documentation and the unit +tests. Pre-processing queries is a little bit smarter now, making matching +e-mail address more eager. Experimental support for Fedora-14. +- [2010-11-27] *mu version 0.9-beta* released. New features: searching is now +accent-insensitive; you can now search for message priority (`prio:`), +time-interval (`date:`) and message flags (`flag:`). Also, you can now store +('bookmark') often-used queries. To top it off, there is a simple graphical +UI now, called `mug`. Documentation has been update, and all known bugs have +been fixed. +- [2010-10-30] *mu version 0.8* released, with only some small cosmetic +updates compared to 0.8-beta. Hurray! +- [2010-10-23] *mu version 0.8-beta* released. The new version brings `mu +extract` for getting the attachments from messages, improved searching +(matching is a bit more 'greedy'), updated and extended documentation, +including the `mu-easy` manpage with simple examples. All known +non-enhancement bugs were fixed. +- [2010-02-27] *mu version 0.7* released. Compared to the beta version, there +are few changes. The maildir-matching syntax now contains a starting `/`, so +`~/Maildir/foo/bar/(cur|new)/msg` can be matched with `m:/foo/bar`. The +top-level maildir can be matched with `m:/`. Apart from that, there are so +small cosmetic fixes and documentation updates. +- [2010-02-11] *mu version 0.7-beta* released. A lot of changes: +- Automatic database scheme version check, notifies users when an +upgrade is needed +- Adds the `mu view` command, to view mail message files +- Removes the 10K match limit +- Support for unattended upgrades - that is, the database can +automatically be upgraded (`--autoupgrade`). Also, the log file is +automatically cleaned when it gets too big (unless you use +`--nocleanup`) +- Search for a certain Maildir using the `maildir:`,`m:` search +prefixes. For example, you can find all messages located in +`~/Maildir/foo/bar/(cur|new)/msg` with `m:foo/bar`. This replaces the +search for path/p in 0.6 +- Fixes for reported issues #17 and #18 +- A test suite with a growing number of unit tests +- Updated documentation +- Many internal refactoring and other changes +This version has been +tagged as `v0.7-beta` in repository, and must be considered a code-complete +preview of the upcoming release 0.7. Please report any problems you encounter +with it.